インターネットには文章を公開できる場所がたくさんあります。それでも、自分のブログには特別な意味があります。技術メモを少しずつ直し、年末の振り返りを年ごとに読み返し、ページも好きな形に育てていけます。
Altria に今あるのは、この制作記録と三つの年間振り返り記事です。この記事では、実際のリポジトリをもとに、文章がページになる仕組みと、実装済みの閲覧機能を整理します。まずコンテンツに安定した居場所をつくり、書き心地と読み心地を少しずつ整える。 それが出発点です。
デザインの参考元と謝辞
このサイトのレイアウトと視覚的なスタイルは、以下のサイトとオープンソースプロジェクトを参考に、Next.js / React で再現・調整しています。
信也のブログ:作者は
senshinya(信也) さん。
紙鹿摸魚処:対応するオープンソースプロジェクトは
L33Z22L11/blog-v3(Clarity)。
デザインと実装を公開してくださった両作者に感謝します。Altria ではこれらを参考にしながら、自分のサイト情報と記事を使い、この Next.js プロジェクトに合わせてページ、コンテンツの読み込み、操作機能を実装しています。
1. シンプルな構成でコンテンツを支える
このプロジェクトは Next.js 16.3.7、React 19.3.0、TypeScript と App Router を使っています。記事は Markdown、サイト設定は blog.config.ts に保存し、現時点ではデータベースもコンテンツ管理画面もありません。文章とコードの差分や過去の状態は Git で管理できます。数本の記事を公開するために、別の管理システムを保守する必要はありません。
app/(zh)/ 中国語のホーム、アーカイブ、記事ルートapp/[locale]/ 英語、日本語のルートapp/_pages/ 各言語で共有するページ実装content/posts/ 中国語の Markdowncontent/posts/en/ 英語訳content/posts/ja/ 日本語訳lib/posts.ts 記事読込、メタデータ検証、翻訳の関連付けlib/markdown.ts 本文の解析と見出しのアンカーcomponents/ 閲覧、検索、テーマなどの部品blog.config.ts サイト名、著者、ドメインなどの設定記事を書くときは主に content/posts を、読み心地を変えるときは主にコンポーネントとスタイルを編集します。記事を増やすたびにページの実装をコピーする必要はなく、カバー画像を変えても URL は変わりません。
2. 一つの Markdown から書き始める
記事ファイルは冒頭のメタデータと、そのあとの本文でできています。この記事の基本形は次のとおりです。
---title: Next.js で、自分だけの小さな世界をつくるdescription: コンテンツの構成、ページ生成、読み心地についての記録。date: 2026-09-30category: 技术tags: Next.js, React, 博客cover: /images/posts/nextjs-blog/wallhaven-y8137x.jpgfeatured: truelocale: jatranslationKey: nextjs-blog---## コンテンツから始めるここに本文を書き、要点を **太字** にします。frontmatter は現在 YAML パーサーで読み取り、配列、入れ子のオブジェクト、複数行の概要、引用符内のエスケープに対応しています。従来の一行の項目やカンマ区切りのタグも使えます。日付には有効な YYYY-MM-DD、YYYY-MM-DD HH:mm:ss、ISO 形式の時刻を指定でき、アーカイブには元の日付を残します。featured: true は注目記事への掲載対象、cover: false はカバーなしを表します。draft: true の記事は、開発環境でも本番環境でも公開対象から外れます。
参考元の形式にある categories、image、recommend、published、updated も読み取ります。検索用の情報は seoTitle/seoDescription または seo オブジェクトで指定でき、references は参考リンク、authorship は執筆方法の明示に使います。ライセンスは記事やサイトに実際に記入された宣言を使い、既存記事へ自動で付与しません。permalink で既存の URL を上書きすることもありません。
ファイル名が slug になります。中国語の原文 nextjs-blog.md は /posts/nextjs-blog に対応し、中国語の記事では言語の指定を省略できます。タイトルは変更しても、ファイル名はなるべく維持します。ブックマークや共有リンクがその URL に依存するためです。
本文は見出し、段落、リスト、引用、リンク、画像、表、コードフェンスに加え、タスクリスト、取り消し線、脚注、参照リンク、明示的な改行、数式に対応しています。MDC 構文で通知、折りたたみ、タブ、カード、詩、チャット、タイムラインなどの用意された部品を使えます。Mermaid と ABC のフェンスは図と楽譜を描画します。コピーできる例はリポジトリの docs/markdown-examples.md にまとめてあり、通常の記事は Markdown だけでも書けます。
HTML は許可された整形要素と安全な属性に限定し、スクリプト、イベント処理、テンプレート式、任意の iframe、JSX は実行しません。動画には対応する配信元を扱う専用部品を使います。GitHub カードは明示された公開リポジトリだけを照会し、Iconify は指定されたアイコンを読み込みます。通信に失敗してもリンクや説明は残ります。楽譜の音声は再生をクリックしたときだけ流れ、外部音源を取得できなくても楽譜とソースは読めます。
3. 記事はサーバーで読み、操作はブラウザーで扱う
lib/posts.ts はサーバー側でファイルを読み、下書き、不正な日付、言語の不一致を除いてから記事一覧をつくります。ホーム、アーカイブ、記事ページ、フィードが同じ入口を使うため、ある一覧から消した記事が別の場所に残るような食い違いを減らせます。
中国語の記事ルートでは generateStaticParams でビルド時に生成する記事を列挙します。ページ処理の中心は次のようになっています。
import Content from "@/app/_pages/post";import { getPosts } from "@/lib/posts";type Props = { params: Promise<{ slug: string }> };export function generateStaticParams() { return getPosts().map(({ slug }) => ({ slug }));}export default async function Page({ params }: Props) { return <Content slug={(await params).slug} locale="zh" />;}この Next.js のルートパラメーターは Promise なので、先に値を読み取ります。既知の記事はビルド時に事前生成され、存在しない記事は notFound() で 404 になります。ただし記事の事前生成と、プロジェクト全体の静的エクスポート設定は別です。公開済みの内容を更新するには、再ビルドと再デプロイが必要です。
検索、テーマ切り替え、カテゴリー絞り込み、目次の追従、コードのコピー、画像の拡大にはブラウザーのイベントや状態が必要なので、Client Components を使います。ファイル読込と本文解析はサーバー側に残します。"use client" はモジュールの境界を定めるため、操作が必要な部品に絞って付けると、ブラウザーに送るコードを把握しやすくなります。詳しくは Server と Client Components の公式ガイドと generateStaticParams の説明を参照できます。
4. 読み心地を細部から整える
記事ページではカバー、概要、本文にそれぞれの場所があります。文章の幅、行間、スマートフォンでの余白が、長文を読み続けられるかどうかを左右します。魅力的な画像でも、タイトルや本文の場所を奪わないことが大切です。
目次は本文の見出しから生成し、同じアンカーを使います。スクロール中は現在の節を示し、デスクトップではサイドバー、狭い画面では引き出し式の目次から移動できます。上部には読書の進捗も表示します。見出しの変更でアンカーが変わる場合があるため、外部からリンクされている節の名前にも注意します。
コードブロックは Shiki の言語定義を必要に応じて読み込み、明暗テーマ、コピー、折り返し、インデントガイド、折りたたみに対応します。32 行を超えると既定で 16 行まで表示し、フェンスの wrap、expand、indent=2 で調整できます。Shiki のコメント記法では差分、強調、注目行、エラーを示せます。コピーされるのは元のコードで、表示用の行番号は含みません。未知の言語や強調処理の失敗時もソースを表示します。
本文画像は遅延読込し、クリックすると拡大できます。画像だけの段落が続くと、縦横比に合わせて自動で並び、デスクトップでは一行に最大 4 枚、スマートフォンでは最大 2 枚を表示します。順序と比率を保ち、文章、見出し、区切り線で別のグループに分けられます。読込に失敗しても説明文は残るため、意味のある alt は遅い回線やリンク切れ、読み上げでも役立ちます。
5. 検索、翻訳、購読の範囲を明確にする
現在の検索対象はタイトル、概要、カテゴリー、タグです。大文字と小文字を区別せず、連続した文字列の部分一致で探します。⌘ K または Ctrl K で開けます。本文の全文検索はまだありません。 記事の途中だけにある一文は見つからず、節単位の結果、曖昧検索、関連度順の並べ替えもありません。記事が増えてきたら、本文から節ごとの索引をつくる段階に進めます。
画面の言語と記事の翻訳は別に管理します。この記事には実際に維持する英語・日本語ファイルがあり、translationKey: nextjs-blog で関連付けています。中国語の URL はそのまま、訳文は /en/posts/nextjs-blog と /ja/posts/nextjs-blog です。翻訳のない記事に見せかけの訳文ページをつくったり、閲覧時に翻訳サービスを呼んだりはしません。
タイトル、概要、canonical、Open Graph、構造化データは記事とサイト設定から生成し、代替言語リンクには実在する訳文だけを含めます。/sitemap.xml はページの発見を助けます。/rss.xml と各言語のフィードはタイトル、概要、本文へのリンクを配信し、全文配信ではありません。公開前に NEXT_PUBLIC_SITE_URL を正式な URL に設定しないと、共有リンクや検索用メタデータがローカルを指す可能性があります。仕組みの詳細は Next.js のメタデータガイドにあります。