000%

更新

NOTES · SEO / GEO / AEO09 · 2026.10

公開本文・メタデータ・構造化データを一緒に実装する

一つの公開取得関数が返す原稿から本文・タイトル・正規URL・構造化データを作る。同じ例で、要求ごとのSSRとビルド時の保存ページを比べる。

§ 01SEO / GEO / AEO

設定場所を決めてウェブ・サーバー・インフラをつなぐ

目標は一件の利用案内を、タイトル・本文・URL・日付・根拠が一致する公開文書として届けることだ。検索は主題と正規URLを処理し、ウェブ資料を使う回答は事実と条件を参照できる。本文が最新の時間でも要約・JSON-LDが旧時間なら値が衝突するため、以下は同じ公開版から出力を作る。

最初に公開ページが変わる時点を決める。変更の少ない案内はビルド時にHTMLを作り、頻繁に変わる記事はサーバーで現行公開版を読むか公開イベントでキャッシュを更新する。公開ページと管理機能が同じアプリでも、取得権限とキャッシュ条件は別に置く。

以下はCMS付きNext.jsサイトのファイル配置例だ。srcを使うリポジトリではappとlibがその下に入る。getPublicGuideなどの関数名はプロジェクトに合わせる。

app/ layout.tsx 共通画面・既定メタデータ guides/[slug]/page.tsx 公開記事取得・本文・詳細メタデータ robots.ts クロール経路案内 sitemap.ts 公開正規URL一覧 lib/ guides.ts 公開条件とデータ取得 identity-time.mjs 日付・公開状態・正規URL判定 site.ts 運用ドメイン設定 next.config.ts フレームワーク・配信設定 package.json パッケージとbuild・startコマンド

ビルドコマンドはpackage.jsonのscriptsで確認する。Node.jsサーバーを直接運用するならnext buildで作り、next startで実行できる。静的エクスポートは生成ファイルを静的ホスティングへ置く。マネージド環境はプロジェクトのビルド・実行設定を使うため、成果物と対応サーバー機能を確認する。

DNSと証明書で運用ドメインを接続し、CDN・ファイアウォールで公開経路を通す。サーバーは公開記事を取得しHTML・メタデータ・サイトマップを提供する。最後に公開イベントが各キャッシュを更新するか確認する。同じURLで接続から本文配信まで要求すれば、結果を変える設定を探しやすい。

構成別の修正場所
本文・タイトル・リンク
修正場所
page.tsx・本文コンポーネント・CMS原稿
確認結果
応答HTMLの見出し・段落・a href
公開条件・日付
修正場所
サーバー取得関数・DB条件・CMS公開処理
確認結果
公開記事・予約前・撤回資料の取得結果
正規URL・検索指示
修正場所
generateMetadata・layout・robots・サーバーヘッダー
確認結果
文書別canonicalとmeta・HTTP検索指示
発見するURL
修正場所
sitemap.ts・一覧ページ・関連記事取得
確認結果
公開全件とサイトマップ・内部リンクの一致
ドメイン・証明書
修正場所
DNS提供者・ホスティングのドメイン管理
確認結果
正常DNS・HTTPS接続・正規URLへの移動
遮断・中継・キャッシュ
修正場所
CDN・WAF・プロキシ・サーバーキャッシュ設定
確認結果
公開本文・正しいステータスコード・更新時点
実行・秘密情報
修正場所
package.json・環境ビルド・環境変数管理
確認結果
運用ビルド・サーバー実行・秘密情報をブラウザーへ渡さない
障害確認
修正場所
ホスティング・CDN・アプリログ
確認結果
パス別エラー率・処理時間・配信バージョン
§ 02SEO / GEO / AEO

実行バージョンとサンプル資料を固定する

例はNext.js 16.3.8、React 19.2.5、TypeScript 5.9.3、Node.js 22のApp Router構成だ。next.config.tsでcacheComponentsをfalseにし、以下のdynamic設定を使う。Cache Componentsを有効にする場合はuse cache・cacheLifeなど、そのモデル向けに別途構成する。

資料は架空図書館の公開案内一件と下書き一件だ。ネットワークや実DBなしで取得と出力を追えるよう固定配列を使う。運用では配列読み取りをサーバーDB取得へ変え、同じクエリに公開条件を適用する。

空ディレクトリに指定名でコードを保存する。この章のページ・取得ファイルと後のrobots・サイトマップを作り、インストール・ビルドを実行する。TypeScript設定は初回実行時にNext.jsが生成できる。既存プロジェクトでは先に導入バージョンと設定を比べる。

次の三ブロックは順にpackage.json、next.config.ts、ターミナルコマンドだ。Node.js 22を使い、インストール後のpackage-lock.jsonを保存する。同じ環境の再現には指定バージョンとロックファイルを併用する。

{ "name": "seo-guide-sample", "private": true, "scripts": { "build": "next build --webpack", "start": "next start", "typecheck": "tsc --noEmit" }, "dependencies": { "next": "16.3.8", "react": "19.2.5", "react-dom": "19.2.5" }, "devDependencies": { "typescript": "5.9.3", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19" } }

import type { NextConfig } from 'next' const config: NextConfig = { cacheComponents: false } export default config

npm install npm run build npm run start -- --port 3027

npm installは宣言したパッケージを導入し、buildは運用成果物、startはその実行を行う。実習URLはhttp://localhost:3027で、コードのexample.comは配信時の正規URL例だ。運用ではSITE_ORIGINを確定ドメインへ変える。

§ 03SEO / GEO / AEO

公開取得と要求内の結果再利用を実装する

lib/site.tsに正規ドメイン、lib/guides.tsに公開条件と取得関数を置く。React cacheは一回のサーバーレンダリング要求で同じ引数の結果を再利用する。訪問をまたいで保存するCDN・データキャッシュとは寿命が違う。

export const SITE_ORIGIN = 'https://example.com'

import { cache } from 'react' import { describeDocument } from './identity-time.mjs' import { SITE_ORIGIN } from './site' export type Guide = { id: string previousSlugs: string[] publicFrom: string | null slug: string title: string summary: string paragraphs: string[] sourceUrl: string | null validFrom: string | null validUntil: string | null status: 'draft' | 'published' | 'withdrawn' publishedAt: string | null contentModifiedAt: string | null deletedAt: string | null revision: number } // 実習用の固定資料。運用では読み取りをDB取得へ置き換える。 const guides: Guide[] = [ { id: 'hours-current', previousSlugs: ['library-hours'], publicFrom: '2026-10-01T00:00:00+09:00', slug: 'opening-hours', title: '図書館の開館時間', summary: '日曜日の開館時間と休館日を案内します。', paragraphs: [ '日曜日は午前10時から午後5時まで開館します。', '毎月第1日曜日と祝日は休館します。この時間表は2026年10月から適用します。', ], sourceUrl: '/policies/opening-hours', validFrom: '2026-10-01T00:00:00+09:00', validUntil: null, status: 'published', publishedAt: '2026-10-01', contentModifiedAt: '2026-10-23T09:00:00+09:00', deletedAt: null, revision: 2, }, { id: 'draft-example', previousSlugs: [], publicFrom: null, slug: 'draft-guide', title: '非公開の案内', summary: '下書き', paragraphs: ['公開応答に含めてはならない実習用下書きです。'], sourceUrl: null, validFrom: null, validUntil: null, status: 'draft', publishedAt: null, contentModifiedAt: null, deletedAt: null, revision: 1, }, ] function toPublic(g: Guide, asOf: string) { const info = describeDocument({ ...g, path: `/guides/${g.slug}`, eventDate: null }, asOf, SITE_ORIGIN) return info ? { ...g, info } : null } // 一要求の本文・metadataが同じ時刻と公開版を読む。 const snapshot = cache(async () => { const asOf = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') return guides.map(g => toPublic(g, asOf)).filter(g => g !== null) }) export const getPublicGuide = cache(async (slug: string) => { return (await snapshot()).find(g => g.slug === slug || g.previousSlugs.includes(slug)) ?? null }) export const listPublicGuides = snapshot

lib/guides.tsは固定資料を読み、describeDocumentでID・正規URL・日付・公開状態・適用状態を計算する。snapshotのReact cacheは一要求で計算した結果を再利用し、getPublicGuideとlistPublicGuidesが共有する。長期保存するDB・CDNキャッシュとは範囲が異なる。React cache

sourceUrlは実際に開ける根拠の経路で、ここでは後で作る/policies/opening-hoursを指す。validFrom・validUntilは時間表の適用期間で、サーバー公開開始のpublicFromとは独立する。

まず文書と時間のidentity-time.mjsをlib/identity-time.mjsへ保存する。guides.tsはdescribeDocumentを実際に呼ぶ。snapshotは要求時刻を一度決め、公開原稿と説明を返す。旧slugのlibrary-hoursも同じ文書を読むが、pageとgenerateMetadataでpermanentRedirectを呼びopening-hoursへ308移動する。@idは文書ID hours-currentから作るためURL変更後も維持される。

SITE_ORIGINはdescribeDocumentの第3引数にも渡す。正規URLと安定した@idが同じ設定からできるため、運用ドメイン変更時に日付モジュール内の例示URLを別に直す必要はない。

snapshot共有は重複取得のコストと内容不一致を減らすためだ。タイトルを読んだ直後に公開版が変わって本文を再取得すると、一応答へ別原稿が混ざりうる。この例は要求内の公開一覧を再利用し、title・本文・日付が同じ資料を説明する。検索へ渡す一文書の意味をそろえる処理だ。

§ 04SEO / GEO / AEO

SSR詳細ページで同じ公開版を出力する

app/layout.tsxは文書言語と基本URLを指定する。app/guides/[slug]/page.tsxはslugに対応する公開資料を取得する。force-dynamicは、このCache Components無効構成で要求時レンダリングを選ぶ。

import type { Metadata } from 'next' import type { ReactNode } from 'react' import { SITE_ORIGIN } from '../lib/site' export const metadata: Metadata = { metadataBase: new URL(SITE_ORIGIN) } export default function Layout({ children }: { children: ReactNode }) { return <html lang="ja"><body>{children}</body></html> }

import type { Metadata } from 'next' import { notFound, permanentRedirect } from 'next/navigation' import { getPublicGuide } from '../../../lib/guides' import { SITE_ORIGIN } from '../../../lib/site' import { formatContentDate } from '../../../lib/identity-time.mjs' export const dynamic = 'force-dynamic' type Props = { params: Promise<{ slug: string }> } export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params const guide = await getPublicGuide(slug) if (!guide) notFound() if (slug !== guide.slug) permanentRedirect(`/guides/${guide.slug}`) const canonical = guide.info.canonical return { title: guide.title, description: guide.summary, alternates: { canonical }, robots: { index: true, follow: true }, openGraph: { title: guide.title, description: guide.summary, url: canonical }, } } export default async function Page({ params }: Props) { const { slug } = await params const guide = await getPublicGuide(slug) if (!guide) notFound() if (slug !== guide.slug) permanentRedirect(`/guides/${guide.slug}`) const canonical = guide.info.canonical const structured = { '@context': 'https://schema.org', '@type': 'WebPage', '@id': guide.info.id, url: canonical, name: guide.title, description: guide.summary, ...(guide.info.datePublished ? { datePublished: guide.info.datePublished } : {}), ...(guide.sourceUrl ? { citation: new URL(guide.sourceUrl, SITE_ORIGIN).href } : {}), ...(guide.info.dateModified ? { dateModified: guide.info.dateModified } : {}), } const format = formatContentDate return <main><article data-revision={guide.revision}> <h1>{guide.title}</h1><p>{guide.summary}</p> {guide.publishedAt && <p data-date="datePublished"><span data-label>公開日</span> <time dateTime={guide.publishedAt}> {format(guide.publishedAt)}</time></p>} {guide.contentModifiedAt && <p data-date="dateModified"><span data-label>修正日</span> <time dateTime={guide.contentModifiedAt}> {format(guide.contentModifiedAt)}</time></p>} <p>適用状態:{({ current: "現在", historical: "過去", scheduled: "適用予定", unknown: "開始日不明" })[guide.info.applicability]}</p> {guide.validFrom && <p>適用開始 <time dateTime={guide.validFrom}> {format(guide.validFrom)}</time></p>} {guide.validUntil && <p>適用終了時刻 <time dateTime={guide.validUntil}> {guide.validUntil}</time>(この時刻は含まない)</p>} {guide.paragraphs.map((text, i) => <p key={i}>{text}</p>)} {guide.sourceUrl && <p>根拠:<a href={guide.sourceUrl}>図書館開館時間案内の原文</a></p>} <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structured).replace(/</g, '\\u003c'), }} /> </article></main> }

generateMetadataがタイトル・説明・正規URLを返し、Pageは同じ資料の段落を出す。data-revisionは出力版を照合する印だ。案内文書にはWebPageを使った。記事サービスではArticleなど、実資料に合う型と著者・日付・画像を構成する。

公開資料がなければnotFoundを呼ぶ。ローディング境界やストリーミングでステータスが変わりうるため、実応答の404・noindex・本文を確認する。原文取得例外は記事不存在と区別し、サーバーエラーとして追う。

第1ブロックはapp/layout.tsx、第2はapp/guides/[slug]/page.tsxだ。要求slugを取得へ渡し、同じguideからcanonical・タイトル・本文・日付・根拠リンクを作る。formatは確認済みの日付をこの言語版の形式で表示する。dateTimeは日付のみかタイムゾーン付き時刻を元の精度で保つ。contentModifiedAtを変えると可視修正日とJSON-LDのdateModifiedが共に変わる。

実行後、/guides/opening-hoursに時間・第1日曜休館・公開日・修正日・根拠リンクが出る必要がある。日付がなければguideのnull値とtime分岐を見る。JSON-LDがあっても本文条件がなければparagraphsを直す。不明slugとdraft-guideは404・noindexを確認する。

公開日・修正日の行にdata-date、各ラベルにdata-labelを付けた。検査は行の意味、timeのdatetime、可視日付を別々に照合する。info.applicabilityも本文に出し、新revisionの過去記録を現行案内と誤解させない。日付のみのpublishedAtはformatContentDateが時差移動なしで表示する。

このPageのp要素を作るmapは、開館時間を可視HTMLへ変える。generateMetadataは主題と正規URLを別に提供する。役割が異なるためtitleへ時間と例外をすべて詰める必要はない。本文で答えを完結させ、タイトルと説明で正確に要約する。修正後は本文とheadが同じ案内を指すか読む。

§ 05SEO / GEO / AEO

公開一覧から詳細記事を発見できるようにする

一覧にも同じ公開条件を使う。app/page.tsxのLinkは実詳細URLを持つリンクを作る。下書きは一覧になく、直接URLでも公開本文を返してはならない。

import Link from 'next/link' import { listPublicGuides } from '../lib/guides' export const dynamic = 'force-dynamic' export default async function Home() { const guides = await listPublicGuides() return <main><h1>図書館の案内</h1><ul> {guides.map(g => <li key={g.slug}> <Link href={`/guides/${g.slug}`}>{g.title}</Link> </li>)} </ul></main> }

guides配列をmapで回し実hrefのLinkを作る。app/page.tsxへ保存し、ホームの「図書館の開館時間」から詳細へ進む。非公開案内が出たらlistPublicGuidesを確認する。一覧と直接URLの両方を検査すると、画面だけで下書きを隠す誤りを見つけられる。

ホームのLinkが実hrefを出せば、クローラーは詳細URLを発見できる。検索入力やクリックイベントだけで記事を開く構成では、その動作をしない収集経路へURLが現れない場合がある。一覧とサイトマップを併用すれば、新記事の場所を知らせる経路を作れる。クロール可能なリンク

§ 06SEO / GEO / AEO

固定した過去記録を静的ページにする

保存資料一件の実習ではapp/archive/opening-2025/page.tsxを固定パスとして作る。要求ごとの取得なしでファイル内容を出すため、ビルドで静的ページと確認できる。/archive/missingにはルートがなく既定404となる。

import type { Metadata } from 'next' import { SITE_ORIGIN } from '../../../lib/site' import { describeDocument, example } from '../../../lib/identity-time.mjs' // 保存版の初回公開・修正日は不明。未確認の日付を出力しない。 const info = describeDocument({ ...example, publishedAt: null, contentModifiedAt: null }, '2026-10-24T09:00:00+09:00', SITE_ORIGIN)! export const metadata: Metadata = { title: '2025年の図書館開館時間記録', alternates: { canonical: info.canonical }, } export default function Archive() { const structured = { '@context': 'https://schema.org', '@type': 'WebPage', '@id': info.id, url: info.canonical, name: '2025年の図書館開館時間記録' } return <main><article> <h1>2025年の図書館開館時間記録</h1> <p>2025年1月1日から12月31日まで適用した架空の時間表です。</p> <p>当時の日曜日は午前11時から午後4時まで開館していました。</p> <p>適用状態:{info.applicability === 'historical' ? '過去' : '適用期間の確認が必要'}</p> <p>この資料の初回公開日と修正日は不明です。</p> <a href="/guides/opening-hours">現在の開館時間と休館日</a> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structured).replace(/</g, '\\u003c'), }} /> </article></main> }

ファイルパスが過去記録URLを決め、metadataが自己canonicalを作る。本文に2025年の期間を残し現行案内へリンクする。publishedAtとcontentModifiedAtはnullなので、公開・修正日表示を飛ばす。

ビルドで/archive/opening-2025が静的、/guides/[slug]が要求時生成か確認する。固定ファイル修正後は再ビルドが必要だ。保存文書が多ければgenerateStaticParamsでルート一覧を作れる。その場合は未登録slug、新規文書、撤回・再生成を別に検査する。静的ルート生成

この保存版は「文書と時間」のexampleへ公開・修正日不明値を入れdescribeDocumentを呼ぶ。不明日付は本文とJSON-LDから共に省く。終了時点は既知なので過去状態と自己URLを保つ。固定基準日は終了済み記録を静的生成する実習用だ。適用状態が今後変わるページは要求時判定か境界時刻での再生成を行う。

§ 07SEO / GEO / AEO

根拠リンク先の文書も実際に提供する

次をapp/policies/opening-hours/page.tsxへ保存する。開館時間・休館日・開始日を提供し、詳細のsourceUrlがここを指す。

import type { Metadata } from 'next' import { SITE_ORIGIN } from '../../../lib/site' export const metadata: Metadata = { title: '図書館開館時間案内の原文 — 実習資料', alternates: { canonical: new URL('/policies/opening-hours', SITE_ORIGIN).href }, } export default function Policy() { return <main><article><h1>図書館開館時間案内の原文 — 実習資料</h1> <p>架空の図書館の2026年10月1日以降の開館案内です。</p> <p>日曜日10:00〜17:00。毎月第1日曜日と祝日は休館します。</p> <p>告知日 <time dateTime="2026-09-30">2026年9月30日</time></p> <a href="/guides/opening-hours">利用案内へ戻る</a> </article></main> }

詳細のsourceUrlがこの経路につながる。開いた後、時間・例外・開始日が一致するか読む。200でも別の年や支館の資料なら根拠は合わない。根拠文書にも自己URLと案内へ戻るリンクを置く。

§ 08SEO / GEO / AEO

メタデータのストリーミング結果を読む

詳細のtitle・description・canonicalは本文と同じデータを使う。タイトルには主題、要約には対象と提供情報を書く。Next.jsのメタデータストリーミングでは、取得完了後にhead情報が加わる場合がある。全応答を受けてから文書別の値を比べる。Next.jsのメタデータ

§ 09SEO / GEO / AEO

本文配列・Markdown・メディアを扱う

guide.paragraphsは本文段落配列で、mapがp要素へ変え、keyがReactの項目識別値になる。波括弧内の通常文字列はテキストとして扱われる。sourceUrlには検証済み根拠URLを入れ、実際に開いて同じ資料か確認する。

Markdownは見出し・リスト・リンクを簡単な記号で書く形式だ。HTML・Markdown CMSは保存原稿をHTMLへ変えるレンダラーを使う。外部入力には許可タグ・属性・URL規則を適用し、スクリプト注入を防ぐ。無限スクロールは追加記事の固有URLとリンクを提供する。「もっと見る」だけで現れる資料には、クローラーの発見経路があるか確認する。

画像には本文に関連する説明と適切な代替テキストを入れる。理解に必要な時間表・料金表は画像だけにせず、読めるテキストや表も置く。画面別サイズと幅・高さで過剰転送と配置の移動を減らす。実動画のあるページだけに動画メタデータを付け、再生URL・サムネイル・アクセス可否を確認する。

§ 10SEO / GEO / AEO

構造化データを本文から派生する

「キム・ミンス、10月24日」だけでは、人物が著者か紹介対象か、日付が公開日か催事日か分からないことがある。構造化データはauthor・about・datePublishedなど関係名を付ける。Googleは内容理解に使い、対応型では検索の追加表示にも利用できる。構造化データの役割

本文の「第1日曜日休館」は答えの事実で、JSON-LDのpublisherとdateModifiedは発行主体と修正時点を示す。同じ原稿から作れば誰がいつどんな案内を提供したか読める。WebPageを付けるだけで休館条件が生まれるわけではないので、条件文はp要素へそのまま出す。

記事はArticle、利用案内はWebPageなど実内容に合う型を選ぶ。次のarticle-jsonld.mjsは一記事から可視タイトル・公開日・修正日・本文画像・JSON-LDを作る。前編のidentity-time.mjsと同じ場所へ保存する。node article-jsonld.mjsでJSONを出し、画像実習サーバーの/news/new-hoursで完成HTMLを読む。

import { pathToFileURL } from 'node:url'; import { describeDocument, formatContentDate } from './identity-time.mjs'; export const article = { id: 'new-hours', path: '/news/new-hours', status: 'published', deletedAt: null, title: '図書館の日曜日の開館時間変更', summary: '2026年10月から日曜日は10〜17時開館します。第1日曜日と祝日は休館します。', publishedAt: '2026-09-30T09:00:00+09:00', publicFrom: '2026-09-30T09:00:00+09:00', contentModifiedAt: '2026-10-01T09:00:00+09:00', eventDate: null, validFrom: '2026-10-01T00:00:00+09:00', validUntil: null, }; export const escapeHtml = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]); export function renderArticle(origin, image, now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')) { const identity = describeDocument(article, now, origin); if (!identity) throw new Error('Article is not public'); // imageには同じ公開画像のURLと実変換幅・高さを渡す。 const imageUrl = new URL(image.path, origin).href; const structured = { '@context': 'https://schema.org', '@type': 'Article', '@id': identity.id, headline: article.title, mainEntityOfPage: identity.canonical, datePublished: identity.datePublished, dateModified: identity.dateModified, image: [imageUrl], author: { '@type': 'Organization', name: '架空図書館の編集チーム' }, publisher: { '@type': 'Organization', name: '架空の図書館' }, }; const h = escapeHtml; const json = JSON.stringify(structured).replace(/</g, '\\u003c'); const html = `<!doctype html><html lang="ja"><head><meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>${h(article.title)}</title><link rel="canonical" href="${h(identity.canonical)}"> <meta property="og:image" content="${h(imageUrl)}"> <script type="application/ld+json">${json}</script></head> <body><article><h1>${h(article.title)}</h1> <p>公開 <time datetime="${h(identity.datePublished)}">${formatContentDate(identity.datePublished)}</time> ・修正 <time datetime="${h(identity.dateModified)}">${formatContentDate(identity.dateModified)}</time></p> <p>${h(article.summary)}</p><figure> <img src="${h(imageUrl)}" width="${image.width}" height="${image.height}" style="max-width:100%;height:auto" alt="図書館案内に使う実習画像"> <figcaption>画像配信経路を比較するための例です。</figcaption> </figure><p>架空図書館の編集チーム</p></article></body></html>`; return { structured, html }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(renderArticle('https://example.com', { path: '/media/photo?w=1280', width: 1280, height: 800, }).structured, null, 2)); }

@contextは語彙のURL、@typeは文書型、headlineは記事名、mainEntityOfPageは代表ページだ。datePublishedは公開時刻、dateModifiedは内容変更時刻を示す。Articleは記事、NewsArticleは報道記事、SocialMediaPostingはSNS投稿、VideoObjectは動画を表す。実際の形式に合わせ型と属性を選ぶ。

JSON-LDはapplication/ld+jsonスクリプトで出す。利用者入力を含む場合、安全にシリアライズしscript終了文字列を注入できないようにする。Next.jsのJSON-LD例はシリアライズした文字列の<文字をUnicodeエスケープへ変える方法を示す。出力後はJSON解析と本文照合の両方を行う。

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structured).replace(/</g, '\\u003c') }} />

出したJSON-LDの型と属性を検査し、タイトル・著者・日付を本文と照合する。検索ツールがエラーを報告したら、該当属性の値と型を直す。構造化データの要件

JSON.stringifyはオブジェクトをJSON文字列へ、replaceは文字を置換する。例のUnicodeエスケープ\u003cはJSON内で<文字を表す。dangerouslySetInnerHTMLは文字列をHTMLへ直接入れるReact機能なので、安全にシリアライズした構造化データだけを渡す。

renderArticleはdescribeDocumentで公開状態と日付を確認し、画像の絶対URLを作る。同じURLを本文img・og:image・Article.imageへ入れる。escapeHtmlはHTML属性と本文の特殊文字を処理し、JSONの<置換はscript終了文字列がHTMLとして読まれるのを防ぐ。後のJSXは同じ処理をReactページへ適用する形だ。

画像サーバーの実幅・高さをimgへ渡すため、ファイル到着前に場所を確保できる。本文の公開・修正ラベルは別にし、適用時点は記事文へ書いた。検索には写真と日付の関係を示し、読者には変更を確認する資料を提供する。GoogleのArticleガイドは記事に関連するクロール可能な代表画像を推奨する。

小さなカードには320pxが合っても、検索用代表写真には細部が読める大きなファイルが必要な場合がある。この実習は同じ1280px派生版で関係を示す。実記事では内容と切り取りを検討し、必要に応じ1:1・4:3・16:9などを用意する。本文にない場面や別記事の写真を既定画像にしない。

— FIN —