@k8ordo/static

ビルドと配信

vite build は全ページを描いて dist/client/ に書きます。そのディレクトリがサイトで、どんな静的ホスティングにも置けます。このページは、出力の形、ページの届き方と遷移の仕方、ホスティングに求めること、404.htmlsitemap.xml を説明します。

出力

dist/client/ がサイトです。ページごとに、HTML と、同じページを RSC のペイロードにした index.rsc が並びます。dist/rsc/dist/ssr/ はそれを作った仕組みで、配信するものではありません。

dist/
  client/
    index.html
    index.rsc
    products/
      index.html
      index.rsc
      1/
        index.html
        index.rsc
    old/
      index.html
    404.html
    sitemap.xml
    assets/
  rsc/
  ssr/

ビルドは最後に、書いたルートの数と、404.htmlsitemap.xml を書いたかを 1 行で報告します。数にはリダイレクトのページも含まれます。

k8ordo: wrote 4 routes and 404.html and sitemap.xml

ページの届き方と遷移

ページは、描画の元になったペイロードを埋め込んだ HTML として届きます。hydration はそのペイロードを読むので、ページをもう一度求めることはありません。そこから先、別のページへのリンクは文書を読み込み直さず、そのページの index.rsc を取りに行きます。サイトがファイルの山でも、遷移はクライアント側に留まります。

ペイロードがヘッダーやクエリではなくパスにあるのは、静的ホスティングがそのどちらによっても答えを変えないからです。

ホスティングに求めること

dist/client/ をそのまま配信できれば、どのホスティングでも構いません。求めるのは次の 3 つです。

  • /products/1 のような URL に products/1/index.html で答える
  • index.rsc をそのまま返す。content type は HTML 以外なら何でも受け付けます
  • 持っていない URL に 404.html で答える。その答えに 404 のステータスを付けるかどうかは、ホスティングの設定です

サイトが持たない URL は、ブラウザ側では描かれません。答えがペイロードではないので、遷移は普通の文書の読み込みになり、ホスティングが 404.html で答えます。サイトの横に置いたファイルも同じで、/robots.txt へのリンクはルーターに飲み込まれず、ファイルそのものを取りに行きます。

ホスティングがダウンロードとして返すファイルへのリンクには、download を付けます(<a href="/report.csv" download>)。そうすれば navigate イベントの時点でダウンロードだと分かるので、ルーターはそれをブラウザに任せます。Content-Disposition は答えと一緒にしか届かず、そのときには URL がもう確定しています。

404.html

not-found.tsx404.html になります。多くの静的ホスティングが、知らない URL に返すファイルです。何も動いていないこのモードでも not-found のページを宣言する意味があるのは、そのためです。

ホスティングの 404 は 1 つしか無いので、表せる not-found.tsx は 1 つだけです。どこに置いても構いません(ロケールの区間の下でも)。2 つ宣言した表は、黙ってどちらかを選ばず、ビルドを止めます。

a static host answers every unknown URL from one file, so only one not-found.tsx can be represented this table declares /docs/*, /*

この 1 枚は、サイトが持たない pathname に対して描かれます。404 とはもともとそういうものです。pathname はそうした URL で、not-found.tsx の上にパラメータがあれば、そこにはどのルートも宣言していない区間が入ります。つまりそこでの params.<name> は、アプリが名付けた値ではありません。@k8ordo/server/:locale/*/fr/anything にも一致するときと同じように扱い、値を確かめます。訪問者が実際に開いた URL は、hydration の後にクライアントコンポーネントで usePathname() から読みます。JavaScript の無い訪問者には、ビルド時の描画がそのまま残ります。

このサイトの LocaleShell がその形です(下はその部分の抜粋です)。受け取った値を locales.is() で確かめ、ロケールでなければ URL から読み直します。そのため 404.html は日本語で配信され、/en/… の URL で hydrate した瞬間に英語になります。

// src/routes/[locale]/_parts/locale-shell.tsx
'use client';

import { usePathname } from '@k8ordo/router';
import { UIProvider } from '@k8ordo/ui';
import { dictionaries } from '@k8ordo/ui/i18n';
import type { ReactNode } from 'react';

import { locales } from '../../../i18n';

export function LocaleShell({
  locale: param,
  children,
}: {
  locale: string;
  children: ReactNode;
}) {
  const pathname = usePathname();
  const locale = locales.is(param)
    ? param
    : (locales.delocalize(pathname).locale ?? locales.default);
  return <UIProvider messages={dictionaries[locale]}>{children}</UIProvider>;
}

not-found.tsx が 1 つも無ければ、404.html は書かれず、知らない URL への答えはホスティング次第です。

sitesitemap.xml

site にサイトの配信元(https://example.com)を渡すと、ビルドは書いたページをすべて並べた sitemap.xml も書きます。リダイレクトと 404.html はページではないので載りません。配信元が無ければサイトマップも書きません。相対 URL のサイトマップは、サイトマップではないからです。

// vite.config.ts
import { framework } from '@k8ordo/static';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    framework({
      paths: () => ['/products/1'],
      site: 'https://example.com',
    }),
  ],
});
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
  <url><loc>https://example.com/products</loc></url>
  <url><loc>https://example.com/products/1</loc></url>
</urlset>

URL は並べ替えられ、site の末尾のスラッシュは無視されます。

framework() のオプション

オプションの型は StaticOptions です。

オプション既定値意味
routesDir'src/routes'ルートのディレクトリ。プロジェクトのルートからの相対パスです。変えても生成されるファイルは .k8ordo/ に書かれ、問題の行は routes/ から始まります。
pathsなしパラメータ付きのパターンを受け取り、具体的な pathname(またはその Promise)を返す関数。
siteなしサイトの配信元。渡すと sitemap.xml を書きます。
// vite.config.ts
import { framework } from '@k8ordo/static';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [framework({ routesDir: 'app/routes' })],
});

ビルドが止まるとき

ページが欠けた、あるいは本番で壊れるサイトを書き出すくらいなら、ビルドは止まります。主な理由は次のとおりです。

  • routes/ の文法の問題と、隠されたルート routes/
  • パラメータ付きルートの pathname の不足と、どのルートも使わない pathname、スキーマが拒む pathname、復号できないか出力の外を指す pathname パラメータ
  • 非同期に検証する paramsSchema パラメータ
  • 2 つ以上の not-found.tsx エラーとリダイレクト
  • 自分のパターンに無いパラメータを行き先で名指す redirect.ts エラーとリダイレクト
  • 'use server' を宣言したモジュール Get Started
  • ビルド中に throw したコンポーネント(Server Component はいつでも、クライアントコンポーネントは上に Suspense の境界が無いとき) エラーとリダイレクト
  • クライアントに届いた server-only のモジュール 実行境界

static にできないこと

リクエストを必要とするものすべてです。Server Action とそこからの redirect()@k8ordo/server の下でページが読む request、そしてアプリが決めるステータスコード。ファイルはフォームの送信を受け取れず、404.html を 200 ではなく 404 で返すかどうかはホスティングの設定です。ビルドはページを書けても、応答は書けません。

どれかが要るなら、アプリが求めているのは @k8ordo/server で、ほかの部分はそのまま使えます。 @k8ordo/server