かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Vercel Next.js 環境ごとに title, favicon を変えたい

Vercel にホストした Next.js のアプリがあり、local環境、preview (ステージング)、本番環境 があり開発中の確認で今どれを見ているのかドメインを見なくても解るように title と favicon を変更して判別しやすいようにした

環境を判定する

Vercel の preview モードの NODE_ENVproduction なので、VERCEL_ENV を使って判定するようにする

VERCEL_ENV
The Environment that the app is deployed and running on. The value can be either production, preview, or development.
cf. System Environment Variables Overview

下記のように場合分けできる

  • local 環境は NODE_ENVdevelopment のとき
  • preview 環境は NODE_ENVproduction かつ VERCEL_ENVpreview のとき
  • production 環境は NODE_ENVproduction かつ VERCEL_ENVproduction のとき

NODE_ENV === 'production'preview 又は production 環境となる

環境を判定する変数を定義する

Next.js を使っているのでクライアントサイドで VERCEL_ENV を使いたい場合は NEXT_PUBLIC_VERCEL_ENV とすれば良い

// config.ts
const getVercelEnv = () => {
  const env = process.env.NEXT_PUBLIC_VERCEL_ENV ?? 'development';
  switch (env) {
    case 'production': {
      return 'production';
    }
    case 'preview': {
      return 'preview';
    }
    default: {
      return 'development';
    }
  }
};

const VERCEL_ENV = getVercelEnv(); // 'production' | 'preview' | 'development'
// production or preview
const isProd = process.env.NODE_ENV === 'production';

export const IS_PRODUCTION = isProd && VERCEL_ENV === 'production';
export const IS_STG = isProd && VERCEL_ENV === 'preview';
export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';

title, favicon を環境ごとに変更する

先ほど作成した変数を利用して title に [DEV], [STG] という prefix を付けたり、favicon の画像を変更してしまえば良い

// SiteHead.tsx
import { FC } from 'react';
import Head from 'next/head';
import {IS_PRODUCTION, IS_PRODUCTION, SITE_TITLE} from '@/configs';

const titlePrefix = IS_PRODUCTION ? '' : IS_STG ? '[STG]' : '[DEV]';
const favicon = IS_PRODUCTION ? '/favicon.ico' : IS_STG ? '/favicon-stg.ico' : '/favicon-dev.ico';

type SiteHeadProps = {
  title?: string;
};

export const SiteHead: FC<SiteHeadProps> = ({ title }) => {
  const pageTitle = title ?? SITE_TITLE;

  return (
    <Head>
      <title>{titlePrefix}{pageTitle}</title>
      <link rel='icon' type='image/png' href={favicon} />
    </Head>
  );
};

これで、環境ごとにブラウザのタブに表示されるタイトルとアイコン (favicon) が替わるようになりました。どのタブがどの環境を見ていたのかぱっと見で解るようになったので認知コストが下がって良くなったと思います!

📝 Tips: 特定のブランチのプレビューだけを判別させる

Vercel では特定のブランチを preview モードでホストすることが出来ます
今回は main ブランチを preview でホストしてステージング環境としていました Vercel hosting

GitHub上に作られる preview 環境と main ブランチがホスティングされている preview 環境とで場合分けする方法メモ

特定のブランチを指定した環境変数を設定する

Vercel の管理画面から作成できる環境変数preview ではブランチを指定することができます
cf. Environments Variables per Git branch – Vercel

Vercel Environment Variables

preview モードかつ main ブランチの時だけの環境変数を作成すれば、main ブランチがホストされてるステージング環境だと判定できます

Vercel Environment Variables

今回は NEXT_PUBLIC_STAGING という環境変数を作成しました。この環境変数main ブランチの preview モードのみ存在し、それ以外の時は存在しません

const IS_MAIN_STAGING = process.env.NEXT_PUBLIC_STAGING ? true : false;

アプリ側で環境変数の有無を判定させれば特定のブランチの preview モードかどうかを判別させることも出来ました!

おわり ₍ᐢ. ̫.ᐢ₎


[参考]

Next.js 構造化されたパンくずリストを作りたい

昨年メディアのような SEO が大切になる Next.js の案件があり、ページの構造を表すパンくずリスト (BreadcrumbList) も重要なのでちゃんと構造化されたものを作ってみたのでそのメモ。

構成

  • Next.js v13 (Pages Router)
  • React v18
  • TypeScript v5.2

パンくずリスト( BreadcrumbList) の構造化データ

Google が公開しているページを参考にした 古の昔はパンくずリストの HTML にプロパティを付ける RDFa や microdata のような形式が主流だった記憶だが、案件を実装当時 (2023年末) では構造を <script type="application/ld+json"> として別途マークアップする JSON-LD が推奨されているらしい

構造化データに関する一般的なガイドライン
リッチリザルトを表示できるようにするには、サポートされている 3 つの形式のいずれかを使用して、サイトのページをマークアップする必要があります。
JSON-LD(推奨)
cf. Google 検索上の構造化データガイドライン | Google 検索セントラル  |  ドキュメント  |  Google for Developers

パンくずリストJSON-LD

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [{
    "@type": "ListItem",
    "position": 1,
    "name": "Books",
    "item": "https://example.com/books"
  },{
    "@type": "ListItem",
    "position": 2,
    "name": "Science Fiction",
    "item": "https://example.com/books/sciencefiction"
  },{
    "@type": "ListItem",
    "position": 3,
    "name": "Award Winners"
  }]
}

cf. パンくずリスト(BreadcrumbList)のマークアップを追加する方法 | Google 検索セントラル  |  ドキュメント  |  Google for Developers

"@context": "https://schema.org", "@type": "BreadcrumbList" の部分は固定なので itemListElement 内のリストを動的に作成すればOK、更に現在のページの URL (item) は不要なのでサイトトップと現在のページの親のデータを持たせておけば良さそうです

JSON-LD なパンくずリストの実装

パンくずリストJSON-LD の型を作成

// types/breadcrumb.d.ts

export type BreadcrumbItem = {
  '@type': 'ListItem';
  position: number;
  name: string; // title
  item?: string; // URL
};

export interface BreadcrumbJsonLd {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: BreadcrumbItem[],
}

パンくずリストの HTML と JSON-LD を出力するコンポーネント

// /components/BreadcrumbList.tsx
import { FC } from 'react';
import Link from 'next/link';
import Script from 'next/script';
import { BreadcrumbJsonLd, BreadcrumbItem } from '@/types/breadcrumb';

type BreadcrumbListProps = {
  breadcrumbData: BreadcrumbItem[];
};

export const BreadcrumbList: FC<BreadcrumbListProps> = ({ breadcrumbData }) => {
  return (
    <>
      <JsonLd breadcrumbData={breadcrumbData} />
      <ol className='breadcrumb'>
        {breadcrumbData.map((item) => (
          <li key={item.position} className="breadcrumbItem">
            <BreadcrumbItem {…item} />
          </li>
        )}
      </ol>
    </>
  );
};

const BreadcrumbItem: FC<BreadcrumbItem> = ({ name, item }) => {
  const label = name === SITE_NAME ? 'Home' : name;
  if (item) {
    return <Link href={item} className='breadcrumbLink'>{title}</Link>;
  }
  // current
  return <span className="breadcrumbLink current">{label}<span>;
}

// JSON-LD を出力するコンポーネント
type JsonLdProps = {
  breadcrumbData: BreadcrumbItem[];
};

const JsonLd: FC<JsonLdProps> = ({ breadcrumbData }) => {
  const jsonLdSchema: BreadcrumbJsonLd = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: breadcrumbData,
  };

  return (
    <Script
      id='BreadcrumbList-JSON-LD'
      type='application/ld+json'
      strategy='beforeInteractive'
    >
      {JSON.stringify(jsonLDSchema)}
    </Script>
  );
};

パンくずリスのデータを各ページから渡して出力すればOK

import { BreadcrumbItem } from '@/types/breadcrumb';
import { BreadcrumbList } from '@/components/BreadcrumbList';

const breadcrumbData: BreadcrumbItem[] = [
  {
    '@type': 'ListItem',
    position: 1,
    name: SITE_NAME,
    item: SITE_HOME_URL,
  },
  {
    '@type': 'ListItem',
    position: 2,
    name: PARENT_PAGE_NAME,
    item: PARENT_PAGE_URL,
  },
  {
    '@type': 'ListItem',
    position: 3,
    name: THIS_PAGE_TITLE
  }
] ;

export default function Page() {
  return (
    <PageLayout>
      <BreadcrumbList breadcrumbData={breadcrumbData} />
      <PageContent>
    </PageLayout>
  );
}

動的にページが決まる場合は breadcrumbData を動的に作成してしまえば OK。
こんな感じで構造化された JSON-DL のパンくずリストを作成することができました!

今回は基本的に SSG をする仕様だったので JSON-DL を出力する Scriptstrategy='beforeInteractive' を指定してページが表示される前に JSON-LD のスクリプトがロードされるようにしたが、JSON-LD と next/script の strategy の仕様に明るくないので strategy の最適な指定方法が理解できているわけではないです。

おわり


[参考]

app router も Remix もまだ全然さわれてないのでそろそろ危機感覚えてきてる…

CSS 日本語で <wbr /> が効かないにハマる

レスポンシブなサイトを実装する際に、モバイルなどエリアが小さくなる時にいい感じにテキストを改行させたい・デザイナーの要望で改行位置を固定させたいケースがあります。改行位置のためにメディアクエリで display を切り替えるのはイケてないので避けたく <wbr /> を使うことが多いのですが日本語だと<wbr /> でいい感じに改行されない問題に当たったのでメモ。

<wbr> は HTML の要素で、改行可能位置 — テキスト内でブラウザーが任意で改行してよい位置を表しますが、この改行規則は必要のない場合は改行を行いません。
cf. <wbr>: 改行可能要素 - HTML: ハイパーテキストマークアップ言語 | MDN

日本語だと <wbr /> が無視されて改行されてしまう問題

日本語のテキスト内に <wbr /> を置くだけだと、ブラウザに関わらず画面幅に応じてどこでもテキストが改行されてしまう。
英文なら <wbr /> が効くが、日本語文章の場合は <wbr /> があろうがなかろうが関係なく文字が折り返されてしまう。
これは単純にブラウザが英文なら単語などを認識できるが、日本語だと単語などを認識していないために起こっている現象だと思われる

結論 word-break: keep-all + overflow-wrap: break-word を指定すれば OK

word-break: keep-all だけだと、コンテナより長い語句があるとコンテナからはみ出してしまうので、overflow-wrap: break-word をあわせて指定するといい感じになる。逆に overflow-wrap: break-word だけだと <wbr /> 関係なしに折り返されてしまう。

日本語で &lt;wbr /&gt; を効かせる方法

Sample

See the Pen wbr test by KIKIKI (@kikiki_kiki) on CodePen.

word-break と overflow-wrap

  • word-break … 表示エリアの端に文字が来た時に、その文字が単語の途中かどうか関係なしに途中で改行させるかどうか
  • overflow-wrap … 表示エリアに収まらない単語が来たときに、単語の途中で改行させるかどうか

word-break とは対照的に、 overflow-wrap は単語全体があふれずに行内に配置できない場合にのみ、改行を生成します。
cf. overflow-wrap - CSS: カスケーディングスタイルシート | MDN

word-break

normal … 既定の改行規則を使用します。
break-all … CJK (中国語、台湾語、日本語、韓国語) 以外のテキストにおいて、単語中などでの文字の改行に関する禁則処理を解除し、どの文字の間でも改行するようにします。
keep-all … CJK テキストの改行を許可しません。 CJK 以外のテキストについては normal と同じ挙動となります。
cf. word-break - CSS: カスケーディングスタイルシート | MDN

overflow-wrap

以前は word-wrap だったが、overflow-wrap に解明されたらしい

normal … 通常の単語の分割位置 (2 つの単語の間の空白など) でのみ改行することを示します。
anywhere … あふれることを避けるために、行内にその他の分割可能な位置がない場合、その他の分割できない文字列 — 長い単語や URL — が任意の場所で分割されることがあります。分割位置にハイフン文字は挿入されません。
break-wordanywhere の値と同様に、行内にその他の分割可能な位置がない場合、通常は分割可能でない単語が任意の場所で分割されますが、コンテンツの最小固有寸法を計算する時に、単語分割によって導入された折り返し可能位置が考慮されません。
cf. overflow-wrap - CSS: カスケーディングスタイルシート | MDN

日本語鬼門。
おわり


[参考]