作成: 2026-08-20 / 対象リポジトリ: pacenotesio/synon-co-jp(ブランチ develop / main)
前提資料: studio-to-cloudflare-workers-migration.md(2026-08-19)— 本書はその続編。前回は「どう作るか」の計画、本書はすでに出来ているリポジトリを実際に公開するための実行手順。
前回資料の時点では「これから作る」だったが、サイトはすでに出来ている。実測した現在地は以下。
| 領域 | 状態 |
|---|---|
| サイト本体 | ✅ 完成。site/content/*.json → site/build/build-visual.mjs → dist-visual/ に14ページ生成 |
| Worker | ✅ 完成。worker/src/index.ts に 静的配信 + /mcp(リモートMCP)+ /api/content/*(WebMCP用)+ /api/inquiry(フォーム) |
| wrangler 設定 | ⚠️ 要修正。このままでは wrangler deploy が通らない/誤ったものが出る(§2 の B-1・B-4・B-5・B-6) |
| GitHub Actions | ❌ 存在しない(.github/ ディレクトリなし) |
| DNS | ❌ Xserver のまま。Cloudflare ゾーンなし |
| 301リダイレクト | ❌ 未着手。_redirects なし |
| STUDIO 側 | 稼働中(全20URL) |
つまり 残っているのは「配線」だけで、作り直しは発生しない。
Step 0 DNS移管(Xserver → Cloudflare) … 半日 + 待ち最大24h ← 唯一の他社依存
Step 1 リポジトリの整理(v1削除・URL方式の統一) … 2〜3時間 ← 🔴 SEO直結
Step 2 wrangler.jsonc の確定・KV作成 … 1時間
Step 3 _redirects(旧20URL → 新URL) … 1時間
Step 4 _headers / 404 / SEO仕上げ … 1時間
Step 5 GitHub Actions … 1〜2時間
Step 6 www で先行検証 → apex 切替 … 半日 ×2日
Step 7 切替後72時間の監視 … 3日
Step 8 STUDIO 解約(切替の1ヶ月後) … 30分
Step 0 と Step 1〜5 は並行できる。 Step 1〜5 は Cloudflare ゾーンが無くても wrangler dev と workers.dev で全部やれるので、DNS移管の申請を出した当日から着手してよい。
dist-visual/ の全ページが .html 付きURLで作られていること。 Cloudflare Workers の静的アセットは既定で /services.html を /services へ 307リダイレクトする。canonical も sitemap も内部リンクも全部 .html なので、このまま公開すると全ページが「リダイレクトされたURL」として Search Console に載り、インデックスされない。 → §2 B-2 と §4。
synon-co-jp/
├── site/
│ ├── content/*.json … 全文言データ(10ファイル)
│ ├── assets/ … style.css / nav.js / webmcp.js / favicon.svg
│ ├── assets-v2/ … style-visual.css + img/(v2専用)
│ └── build/
│ ├── build.mjs … v1 → dist/ (11ページ)
│ ├── build-visual.mjs … v2 → dist-visual/ (14ページ)★本番
│ ├── layout.mjs … <head>・ヘッダー・フッター共通
│ ├── pages/ … v1のページ定義(8ファイル)
│ └── pages-visual/ … v2のページ定義(11ファイル)★本番
├── worker/
│ ├── src/index.ts … ルーティング(/mcp, /api/*, それ以外は ASSETS)
│ ├── src/tools.ts … MCPツール定義 + 問い合わせ保存
│ ├── wrangler.jsonc … ⚠️ 要修正
│ └── wrangler.dev.jsonc … ローカル用(.gitignore 済み)
├── scripts/ … 画像収集・スクショ(移行後は不要)
├── design/ archive/ screenshots/ plan/ … 制作過程の資産
└── .gitignore … dist/ dist-visual/ node_modules/ .env を除外
dist/ と dist-visual/ は git 管理外 → CI でビルドが必須(成果物をコミットする運用ではない)https://github.com/pacenotesio/synon-co-jp.git、ブランチは main(既定)と develop(現在のHEAD)git status クリーン)dist-visual/)| 生成ファイル | 公開URL(Step 1 実施後) | 内容 |
|---|---|---|
index.html |
/ |
トップ |
fde.html |
/fde |
FDE サービス |
services.html |
/services |
サービス一覧(#ai-development #system-development #products のアンカーあり) |
works.html |
/works |
実績 |
company.html |
/company |
会社概要 |
news.html |
/news |
お知らせ一覧 |
news/coding-labo.html |
/news/coding-labo |
記事 |
news/renewal.html |
/news/renewal |
記事 |
contact.html |
/contact |
お問い合わせ |
proposal.html |
/proposal |
製造業向け提案(フッターのみ) |
partner.html |
/partner |
パートナープログラム(フッターのみ) |
syncsync-form.html |
/syncsync-form |
シンシンフォーム(フッターのみ) |
privacy.html |
/privacy |
プライバシーポリシー |
terms.html |
/terms |
利用規約 |
加えて sitemap.xml と robots.txt を自動生成。
| 旧URL | <title> |
実体 |
|---|---|---|
/ |
シンオン株式会社 | トップ |
/1 |
シンオン株式会社 | トップの複製 |
/about |
シンオン株式会社 | COMPANY | 会社概要 |
/company-org |
シンオン株式会社 | COMPANY | 会社概要の別ページ |
/company-1 |
シンオン株式会社 | COMPANY | 複製 |
/service |
シンオン株式会社 | SERVICE | サービス |
/service-1 |
シンオン株式会社 | SERVICE | 複製 |
/development |
シンオン株式会社 | DEVELOPMENT | 開発 |
/development-1 |
(未取得) | 複製と推定 |
/works-system |
シンオン株式会社 | WORK | 実績(システム) |
/work-app |
シンオン株式会社 | WORK-APP | 実績(アプリ) |
/work-1 |
(未取得) | 複製と推定 |
/topics |
シンオン株式会社 | NEWS | お知らせ一覧 |
/topics/dify |
— | CMS記事(2025-01-11) |
/topics/info |
— | CMS記事(2025-01-10) |
/posts/zsFXXiMM |
— | CMS記事(2026-01-07・自動生成スラッグ) |
/posts/renew |
— | CMS記事(2025-01-10) |
/contact |
— | お問い合わせ |
/privacy |
— | プライバシーポリシー |
/termsofuse |
シンオン株式会社 | TERMS OF USE | 利用規約 |
STUDIO はクライアントサイドレンダリングのため、外部からは <title> と meta しか取得できない。/development-1 /work-1 の中身と、CMS4記事の本文は、STUDIO 管理画面にログインして目視で確認すること。 特に /topics/dify は新サイトに対応記事が無いので、残す価値があるなら site/content/news.json に移植した上でリダイレクト先を変えるべき。
NS ns1〜ns5.xserver.jp
A @ 203.0.113.10 ← STUDIO(Google Cloud の共有IP)
A * 203.0.113.10 ← ワイルドカード
A www 198.51.100.10 ← Xserver(apex と別基盤)
MX Google Workspace ×5
CAA / AAAA / DS なし
MX が apex を指していないので、apex の A を差し替えてもメールは無関係。 ここが syncsync.jp との決定的な違いで、apex を安全に切り替えられる根拠。
すべて worker/wrangler.jsonc と site/build/ の修正で、コード量は多くない。
"assets": { "directory": "../dist", ... } // ← v1(11ページ・旧デザイン)
本番は v2(dist-visual/)。このまま wrangler deploy すると旧デザインの11ページが本番に出る。
→ §5.1 の完成形で ../dist-visual に変更。あわせて v1 系(build.mjs / pages/ / dist/)は削除する。2系統を残すと「どっちをビルドしたか」で必ず事故る。
.html 付き=全部307リダイレクトされるこれが本件で最も影響が大きい。実機(wrangler 4.124.0)で検証した結果:
リクエスト 応答
/services.html 307 → /services
/services 200 (dist-visual/services.html を配信)
/news/renewal.html 307 → /news/renewal
/news/renewal 200
Cloudflare Workers 静的アセットの既定 html_handling: "auto-trailing-slash" の仕様どおりの挙動。ところが現在の生成物は:
<link rel="canonical" href="https://synon.co.jp/services.html"> … リダイレクトされるURLを正規URLに指定しているsitemap.xml の14URLすべてが .html … Search Console で「ページにリダイレクトがあります」となりインデックスされない.html … クリックのたびに307が1往復増える対処は2案あるが、案Aを強く推奨。
案A(推奨):生成側を拡張子なしURLに統一する。 site/build/layout.mjs と build-visual.mjs の URL 生成を .html なしに変える。ファイル名は services.html のままでよく、URLの表記だけ変える。修正箇所は「ナビ定義」「フッター定義」「各ページの path」「sitemap生成」の4か所。
案B(非推奨):html_handling: "none" にする。 .html 付きURLが200で返るようになるが、実測で / が404になった(none は完全一致しか見ないため、/ から /index.html への解決すら行わない)。トップページが死ぬので採用不可。
# html_handling: "none" の実測
/services.html 200
/services 404
/ 404 ← これで没
404.html が存在しないnot_found_handling: "404-page" を指定しているのに dist-visual/404.html が無い。この場合 本文が空の404 が返る。site/build/pages-visual/ に notfound.mjs を追加して生成すること。
"kv_namespaces": [{ "binding": "INQUIRIES", "id": "PLACEHOLDER_REPLACE_WITH_KV_ID" }]
→ §5.2 で実際に作成して差し替え。この状態では wrangler deploy が通らない。
routes 指定が Custom Domain になっていない"routes": [
{ "pattern": "synon.co.jp/*", "zone_name": "synon.co.jp" }, // ← Routes 方式
{ "pattern": "www.synon.co.jp/*", "zone_name": "synon.co.jp" }
]
Routes 方式は「Cloudflare がプロキシしている DNS レコードが別途必要」で、レコードが無いと ERR_NAME_NOT_RESOLVED になる。Worker 自体がオリジンなら Custom Domain 方式が正解で、こちらは DNS レコードと証明書を Cloudflare が自動作成する。
"routes": [
{ "pattern": "synon.co.jp", "custom_domain": true },
{ "pattern": "www.synon.co.jp", "custom_domain": true }
]
どちらの方式も「Cloudflare 上のアクティブなゾーン」が前提なので、Step 0(DNS移管)が終わるまでこの行はコメントアウトしておき、workers.dev で検証する。
unsafe バインディングのまま"unsafe": { "bindings": [{ "name": "INQUIRY_LIMITER", "type": "ratelimit", ... }] }
Rate Limiting バインディングは 2025-09-19 に GA(安定版)になり、ratelimits という正式キーがある(Wrangler 4.36.0 以降)。unsafe 経由は「移行のために動き続けているだけ」の扱い。namespace_id を同じ値のままキー名だけ移せばカウンタも維持される。
worker/package.json の依存宣言が実体とずれている// コードの import
import { McpServer } from "@modelcontextprotocol/server"; // v2 系
// package.json の宣言
"@modelcontextprotocol/sdk": "^1.30.0" // v1 系
コードのほうが正しい。 MCP TypeScript SDK は 2026-07-27 に v2 で分割パッケージ化され、@modelcontextprotocol/server@2.0.0 が現行。今は agents の peerDependencies 経由で npm が自動インストールしているためたまたま動いている(実際にクリーンインストール + tsc --noEmit で検証済み・エラー0)。
問題は agents@0.20.x の peer が 完全一致ピン で "@modelcontextprotocol/sdk": "1.30.0" を要求している点。package.json 側が ^1.30.0 なので、SDK が 1.31.0 をリリースした瞬間に npm install が ERESOLVE で落ちる。 CI が無言で止まるタイプの時限爆弾なので、明示宣言に直す(§5.3)。
前回資料の Step 1 と同じ内容だが、.co.jp 固有の注意を加えた実行版。
.jp / .co.jp に非対応。 レジストラは Xserver Domain のまま。本作業はネームサーバーの変更のみで、更新・Whois・組織情報は引き続き Xserver 側。Xserver の DNS レコード設定で、A(@) A(www) MX TXT の TTL を 300秒 に変更。移行の48時間以上前に実施すること(現在の TTL が反映されきるまで待つ必要があるため)。
親(.co.jp)が持つ 委任NSレコードのTTLは自分では下げられない。ここは待つしかない。
synon.co.jp → Free プランfor t in A AAAA MX TXT NS SOA CAA SRV; do
echo "--- $t ---"; dig @ns1.xserver.jp synon.co.jp $t +noall +answer
done
# サブドメインも個別に
for h in www mail ftp smtp _dmarc google._domainkey; do
dig @ns1.xserver.jp $h.synon.co.jp ANY +noall +answer
done
MX … Google Workspace ×5
- TXT の SPF(v=spf1 ... include:_spf.google.com ...)
- TXT の google-site-verification
- google._domainkey の DKIM … セレクタ名は推測不能なのでスキャンでは絶対に取れない
- _dmarc の TXT
- STUDIO / Xserver の所有権確認用 TXTA *(ワイルドカード)はこの時点で登録しない。STUDIO 解約後も共有IPを指し続けると、他人のサイトが なんとか.synon.co.jp で表示されうるCloudflare が割り当てたネームサーバーへ直接問い合わせて、Xserver と同じ答えが返ることを確認する。ここを飛ばすと、切り替えた瞬間にメールが落ちる。
CFNS=xxx.ns.cloudflare.com # ダッシュボードで割り当てられた値
for t in A MX TXT; do
diff <(dig @ns1.xserver.jp synon.co.jp $t +short | sort) \
<(dig @$CFNS synon.co.jp $t +short | sort) && echo "$t OK"
done
secure.xserver.ne.jp/xapanel/login/xserver/)または XServer ドメイン管理画面にログインsynon.co.jp → 「ネームサーバー設定」→「設定変更」ns3〜ns5.xserver.jp を消し忘れると、Cloudflare のゾーンが永遠に "Pending" のまま止まるCloudflare は増加間隔で再チェックする。数分〜数時間が通常、24時間が上限。ゾーンが Active になるまで Step 6 には進めない(Step 1〜5 は並行可)。
Active 後にやること:
- メールの往復テスト(受信・送信の両方)
- https://synon.co.jp が まだ STUDIO で表示されることを確認(= サイトは無停止で移管できている)
git checkout -b chore/prepare-production develop
git rm -r site/build/pages site/build/build.mjs
# dist/ は .gitignore 済みなのでローカル削除のみ
rm -rf dist
.gitignore から dist-visual/ の行は残したまま、ビルド出力先を dist/ に統一するのが最終的には分かりやすい。ただし今回は差分を小さく保つため dist-visual/ のままで進め、統一は移行完了後の整理タスクとする(本書は dist-visual/ 前提で書く)。
site/build/layout.mjs と build-visual.mjs の中で、URL文字列として使われている .html を落とす。ファイル出力名は変えない。
修正対象は次の4系統:
| 場所 | 現状 | 変更後 |
|---|---|---|
layout.mjs のナビ定義(header() 内) |
href="/services.html" |
href="/services" |
layout.mjs のフッター定義 + setFooterExtras() の呼び出し |
/proposal.html 等 |
/proposal 等 |
各 pages-visual/*.mjs が layout() に渡す path |
"/services.html" |
"/services" |
build-visual.mjs の sitemap 生成 |
.map(p => p === "index.html" ? "/" : "/"+p) |
末尾 .html を除去する1行を追加 |
sitemap 生成側の最小差分:
const urls = written
.map((w) => w.relPath)
.map((p) => (p === "index.html" ? "/" : `/${p.replace(/\.html$/, "")}`)) // ← ここ
.map((loc) => ` <url><loc>${SITE_URL}${loc}</loc></url>`)
.join("\n");
layout() は path から canonical を組み立てているので、path を直せば canonical と og:url も自動的に直る。
検証(必須)
node site/build/build-visual.mjs
# .html を含むURLが残っていないこと(アンカー付きも含めて確認)
grep -ro 'href="/[^"]*\.html' dist-visual/ | sort -u # → 0件になること
grep -o '<loc>[^<]*</loc>' dist-visual/sitemap.xml # → 拡張子なしであること
grep -ho 'rel="canonical" href="[^"]*"' dist-visual/*.html # → 拡張子なしであること
site/build/pages-visual/notfound.mjs を作り、build-visual.mjs から emit("404.html", ...) する。中身は既存の pageHero を使った簡素なもので十分。
sitemap には含めないこと。 build-visual.mjs は written 配列から sitemap を作っているので、404 だけは written.push() せずに emit() だけ呼ぶか、除外条件を入れる。
cd worker && npm ci && cd ..
node site/build/build-visual.mjs
npx wrangler dev --config worker/wrangler.dev.jsonc
# 別ターミナル
for u in / /services /company /news /news/renewal /contact /nope; do
printf "%-18s " "$u"
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:8787$u"
done
# → 全部200、/nope だけ404(本文つき)
worker/wrangler.jsonc 完成形wrangler 4.124.0 で --dry-run 検証済み(バインディング3つが正しく認識されることを確認)。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "synon-site",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"compatibility_flags": ["nodejs_compat"],
// 🔴 本番は workers.dev を閉じる(重複コンテンツ対策)
// Cloudflare は workers.dev に noindex を付けないため、開けたままだと
// synon-site.<subdomain>.workers.dev がインデックスされうる。
"workers_dev": false,
// workers_dev:false にすると preview_urls も既定で off になるため明示的に開ける
"preview_urls": true,
"assets": {
"directory": "../dist-visual",
"binding": "ASSETS",
"html_handling": "auto-trailing-slash", // 既定値。明示して意図を残す
"not_found_handling": "404-page", // dist-visual/404.html が必要(B-3)
"run_worker_first": ["/mcp", "/api/*"]
},
"kv_namespaces": [
{ "binding": "INQUIRIES", "id": "<§5.2 で作成した ID>" }
],
// Rate Limiting は GA 済み。unsafe.bindings から移行(B-6)
"ratelimits": [
{ "name": "INQUIRY_LIMITER", "namespace_id": "1001", "simple": { "limit": 5, "period": 60 } }
],
"observability": { "enabled": true }
// 🔴 Step 0(ゾーンが Active)まではコメントアウトのまま。
// Custom Domain は「アクティブな Cloudflare ゾーン」が前提で、
// DNSレコードと証明書は Cloudflare が自動作成する。
// ,"routes": [
// { "pattern": "synon.co.jp", "custom_domain": true },
// { "pattern": "www.synon.co.jp", "custom_domain": true }
// ]
}
設定値の根拠
| キー | 値 | 理由 |
|---|---|---|
html_handling |
auto-trailing-slash |
none にすると / が404になる(実測済み) |
not_found_handling |
404-page |
最も近い 404.html を探して404で返す。無ければ本文空の404 |
run_worker_first |
["/mcp", "/api/*"] |
Wrangler 4.20.0 以降で配列形式に対応。/mcp は完全一致 |
workers_dev |
false |
ダッシュボードで無効化しても、設定ファイルに書いていないと次のデプロイで復活する |
ratelimits |
正式キー | Wrangler 4.36.0 以降 |
⚠️ run_worker_first に一致するパスには _redirects も _headers も適用されない。/mcp と /api/* のヘッダーは Worker のコード側で付けること(現状のコードは付けている)。
⚠️ Free プランでは、run_worker_first 一致パスが上限超過時に静的アセットへフォールバックせず 429 を返す。ただし /mcp /api/* のみが該当し、静的アセットへのリクエストは課金対象外・無制限。
cd worker
npx wrangler kv namespace create INQUIRIES
# 出力される id を wrangler.jsonc の "<§5.2 で作成した ID>" に貼る
# プレビュー用も作っておく(PRデプロイで本番KVを汚さないため)
npx wrangler kv namespace create INQUIRIES --preview
worker/package.json の修正(B-7)"dependencies": {
"@modelcontextprotocol/server": "2.0.0", // ← 明示。agents の peer は完全一致ピン
"@modelcontextprotocol/client": "2.0.0", // ← agents が内部で参照する
"agents": "^0.21.0",
"zod": "^4.4.3" // v2 は zod >= 4.2.0 必須(v3不可)
},
"devDependencies": {
"@cloudflare/workers-types": "^5.20260818.1",
"typescript": "^7.0.2",
"wrangler": "^4.124.0"
}
"@modelcontextprotocol/sdk" は削除する(コードは v2 パッケージしか import していない)。
cd worker && rm -rf node_modules package-lock.json && npm install && npx tsc --noEmit
# → エラー0 であること(検証済み)
git add package.json package-lock.json
node site/build/build-visual.mjs
cd worker && npx wrangler deploy
# → https://synon-site.<subdomain>.workers.dev
このときだけ一時的に workers_dev: true にする。検証が済んだら false に戻して再デプロイすること(戻し忘れが最も多い事故)。
確認項目:
- 14ページすべてが200
- /mcp に MCP Inspector か Claude から接続できる
- /api/content/company.json が JSON を返す
- /contact のフォーム送信 → wrangler kv key list --binding INQUIRIES に入る
- 存在しないURLで404ページが出る
実測で確認した落とし穴:
# _redirects に「/service /services.html 301」と書いた場合
GET /service → 301 → /services.html
GET /services.html → 307 → /services ← 2ホップになる
GET /services → 200
# 「/service /services 301」と書いた場合
GET /service → 301 → /services
GET /services → 200 ← 1ホップ
_redirects は html_handling より先に評価される(実測)。だから宛先を .html にすると必ず2ホップの連鎖になる。Google はリダイレクトチェーンを辿るが評価は目減りするので、1ホップに収めること。
site/assets/_redirects を作るbuild-visual.mjs の cp(site/assets → dist-visual/assets) は assets/ 配下にコピーするため、_redirects はアセットディレクトリの直下(dist-visual/_redirects)に置く必要がある。site/ 直下に置いて build-visual.mjs から明示コピーするのが素直。
// build-visual.mjs の末尾に追加
await cp(path.join(SITE, "_redirects"), path.join(DIST, "_redirects"));
await cp(path.join(SITE, "_headers"), path.join(DIST, "_headers"));
site/_redirects の内容# ── STUDIO 旧URL → 新URL(2026-08-20 作成)────────────────
# 宛先は必ず拡張子なし。.html を書くと307が連鎖して2ホップになる。
# 会社概要
/about /company 301
/company-org /company 301
/company-1 /company 301
# サービス・開発
/service /services 301
/service-1 /services 301
/development /services 301
/development-1 /services 301
# 実績
/works-system /works 301
/work-app /works 301
/work-1 /works 301
# トップの複製
/1 / 301
# お知らせ(STUDIO CMS の2系統)
/topics /news 301
/posts/renew /news/renewal 301
/topics/dify /news 301
/topics/info /news 301
/posts/zsFXXiMM /news 301
# 上記に無い旧CMS記事を拾うワイルドカード(必ず個別指定の後に置く)
/topics/* /news 301
/posts/* /news 301
# 利用規約はスラッグが変わる
/termsofuse /terms 301
# /contact /privacy は旧新で同じパスなので指定しない
実測での動作確認結果(wrangler dev):
/service 301 → /services
/about 301 → /company
/topics 301 → /news
/topics/dify 301 → /news
/posts/zsFXXiMM 301 → /news
/1 301 → /
/topics/dify(Dify の記事・2025-01-11) — 一覧ページへの301は、Google に「ソフト404」と見なされる可能性がある。記事に価値があるなら site/content/news.json に本文を移植し、/topics/dify → /news/dify の301に変えるのが正しい。移植しないなら /news でよい。同じ判断を /topics/info /posts/zsFXXiMM にも。/development → /services — 新サイトの /services には #system-development アンカーがある。_redirects はフラグメントを宛先に書けるので /development /services#system-development 301 も可能。ただしフラグメントはブラウザ側の処理なので SEO 上の効果はない。ユーザー体験を優先するなら付ける価値はある。_redirects は 静的2,000 + 動的100 = 計2,100行、1行1,000文字まで。今回は20行なので余裕。410 は返せない(3xx のみ対応)ので、完全に消したいURLがあっても404にするしかない。site/_headers# セキュリティヘッダー(全ページ)
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
# 内容ハッシュを持たないアセットは短めに
/assets/*
Cache-Control: public, max-age=3600
/assets-v2/*
Cache-Control: public, max-age=3600
⚠️ _headers は Worker が生成したレスポンスには適用されない。/mcp /api/* にヘッダーを足したい場合は worker/src/index.ts 側で付ける。
⚠️ 上限は 100ルール・1行2,000文字。
⚠️ Strict-Transport-Security は一度入れると取り消しが効きにくい。includeSubDomains を付けると *.synon.co.jp の全サブドメインが HTTPS 必須になるので、社内システムやステージングに http のホストが無いことを確認してから入れること。不安なら初回は max-age=300 で様子を見る。
前回資料の指摘どおり、STUDIO 版には JSON-LD が無かった。layout.mjs は既に jsonLd を受け取る作りになっているので、トップと会社概要に Organization を入れる。
// pages-visual/index.mjs の render() 内
jsonLd: {
"@context": "https://schema.org",
"@type": "Organization",
name: company.name,
url: SITE_URL,
logo: `${SITE_URL}/assets/favicon.svg`,
telephone: company.tel,
address: { "@type": "PostalAddress", addressCountry: "JP", /* company.json から */ },
sameAs: [ /* 各SNS */ ]
}
前回の実測で、STUDIO 版の OGP 画像は storage.googleapis.com(STUDIO のストレージ)にあった。STUDIO を解約すると消える。 新サイトの layout.mjs には現在 og:image の出力自体が無いので、
site/assets-v2/img/ogp.png として配置(1200×630)layout.mjs に <meta property="og:image" content="${SITE_URL}/assets-v2/img/ogp.png"> を追加synon.co.jp のプロパティが登録済みか確認(無ければ DNS TXT で登録 → Cloudflare のゾーンに追加)sitemap.xml を送信build-visual.mjs が Sitemap: https://synon.co.jp/sitemap.xml を出力しており、STUDIO 版と同じパスなのでそのままでよい。STUDIO のインデックス形式(子3ファイル)は不要で、単一ファイルで問題ない。
Cloudflare ダッシュボード → My Profile → API Tokens → Create Custom Token
| スコープ | 権限 |
|---|---|
| Account → Workers Scripts | Edit |
| Account → Workers KV Storage | Edit |
| Account → Account Settings | Read |
| User → Memberships | Read |
Zone → Workers Routes(synon.co.jp) |
Edit |
production)に格納し、必須レビュアーを設定GitHub リポジトリ → Settings → Secrets → CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID
.github/workflows/deploy.ymlname: Deploy synon.co.jp
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with: { node-version: '22' }
- name: Install worker deps
run: npm ci
working-directory: worker
- name: Typecheck worker
run: npx tsc --noEmit
working-directory: worker
- name: Build site
run: node site/build/build-visual.mjs
# デプロイ前の自己検証。壊れた成果物を本番に送らないための関所。
- name: Verify build output
run: |
set -e
test -f dist-visual/index.html
test -f dist-visual/404.html
test -f dist-visual/_redirects
test -f dist-visual/sitemap.xml
# URL に .html が残っていないこと(B-2 の再発防止)
if grep -rq 'href="/[^"]*\.html' dist-visual/; then
echo "::error::拡張子つきの内部リンクが残っています"; exit 1
fi
if grep -q '\.html<' dist-visual/sitemap.xml; then
echo "::error::sitemap に .html が残っています"; exit 1
fi
- uses: actions/upload-artifact@v7
with: { name: dist, path: dist-visual/ }
deploy-preview:
needs: build
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v7
with: { name: dist, path: dist-visual/ }
- run: npm ci
working-directory: worker
- uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: worker
command: versions upload --preview-alias pr-${{ github.event.number }}
# → https://pr-123-synon-site.<subdomain>.workers.dev
# 本番トラフィックには一切影響しない
deploy-production:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # ← 承認ゲート
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v7
with: { name: dist, path: dist-visual/ }
- run: npm ci
working-directory: worker
- uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: worker
command: deploy
- name: Smoke test
run: |
set -e
sleep 15
for p in / /services /company /news /contact; do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://synon.co.jp$p")
echo "$p -> $code"
[ "$code" = "200" ] || exit 1
done
# 代表的な301を確認
loc=$(curl -s -o /dev/null -w '%{redirect_url}' "https://synon.co.jp/about")
echo "/about -> $loc"
case "$loc" in *"/company") ;; *) exit 1 ;; esac
- name: Rollback on failure
if: failure()
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: worker
command: rollback --message "smoke test failed on ${{ github.sha }}"
注意点
actions/* のバージョンは 2026-08 時点(checkout v6 / setup-node v6 / upload-artifact v7 / download-artifact v7)。Dependabot を有効にして追随すること。メジャーが古いと突然失敗するcloudflare/wrangler-action は v4(v4.0.0 で Wrangler v4 が既定に)。Cloudflare の公式ドキュメントの例はまだ @v3 のままだが、新規は v4 でよいwrangler rollback に -y / --yes は無い。--message を渡すと確認プロンプトがスキップされるのが非対話化の唯一の手段preview_urls: true が設定に入っていないと --preview-alias の URL が出ない(§5.1 で設定済み)エイリアス + Worker名 が63文字以内、直近1,000個まで保持www → apex の順。 www を先に Workers へ向ければ、本番URL(apex)に影響を与えずに実ドメイン・実証明書で検証できる。
現状 www は Xserver(198.51.100.10)を指しているだけで、実質使われていない。ここを踏み台にする。
// wrangler.jsonc — www だけ有効化
"routes": [
{ "pattern": "www.synon.co.jp", "custom_domain": true }
]
cd worker && npx wrangler deploy
Custom Domain 作成時に Cloudflare が DNS レコードと証明書を自動発行する。その前に既存の www A 198.51.100.10 を削除しておくこと。 Cloudflare は「既に CNAME レコードがあるホスト名には Custom Domain を作れない」と明記しており、A レコードについても既存レコードと衝突させる意味がないため、先に消すのが安全。
検証(実ドメイン・実証明書)
for p in / /services /company /works /fde /news /news/renewal /contact /privacy /terms /proposal /partner /syncsync-form; do
printf "%-20s " "$p"; curl -s -o /dev/null -w "%{http_code}\n" "https://www.synon.co.jp$p"
done
# 301の確認
for p in /about /service /development /works-system /topics /termsofuse /1; do
printf "%-16s " "$p"
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" "https://www.synon.co.jp$p"
done
# 証明書
echo | openssl s_client -connect www.synon.co.jp:443 -servername www.synon.co.jp 2>/dev/null | openssl x509 -noout -dates -issuer
/mcp に Claude から接続www で問題が出なかったら apex に進む。MX は apex を指していないので、メールへの影響はない。
"routes": [
{ "pattern": "synon.co.jp", "custom_domain": true },
{ "pattern": "www.synon.co.jp", "custom_domain": true }
]
A @ 203.0.113.10(STUDIO)を削除A * 203.0.113.10(ワイルドカード)も削除wrangler deploy切替直後の確認(10分以内)
dig synon.co.jp A +short # Cloudflare の IP になっていること
curl -sI https://synon.co.jp/ | head -3 # 200
curl -s https://synon.co.jp/sitemap.xml # 新しい14URL
curl -s https://synon.co.jp/robots.txt
両方が Custom Domain のままだと同じ内容が2つのURLで出る=重複コンテンツ。www は Cloudflare の Single Redirect ルール(ゾーンレベル・Free で利用可)で apex に飛ばす。
wrangler.jsonc から www.synon.co.jp の Custom Domain を削除 → wrangler deployHostname equals www.synon.co.jp
- 動作: Dynamic redirect → concat("https://synon.co.jp", http.request.uri.path)
- ステータス: 301、クエリ文字列を保持curl -sI https://www.synon.co.jp/services → 301 Location: https://synon.co.jp/services_redirects ファイルはドメインレベルのリダイレクトに非対応なので、ここは Redirect Rules を使う。
3段階。上ほど速い。
| 段階 | 手段 | 所要 | 使う場面 |
|---|---|---|---|
| ① | wrangler rollback --message "" |
即時 | デプロイした版が壊れている |
| ② | Custom Domain を削除 → A @ 203.0.113.10 をグレー雲で再作成 |
数分 | Workers 全体がおかしい/STUDIO に戻したい |
| ③ | Xserver でNSを ns1〜ns5.xserver.jp に戻す |
最大24時間 | Cloudflare ごと切り戻す |
| 項目 | 見る場所 | 何を見るか |
|---|---|---|
| エラー率 | Workers Logs(observability.enabled: true で有効) |
5xx が出ていないか |
| リクエスト数 | Workers Analytics | 静的アセットは無制限・課金対象外。/mcp /api/* の実行回数が Free の100,000/日に対してどうか |
| 404 | Workers Logs | 想定外の旧URLへのアクセス → _redirects に追加 |
| インデックス | Search Console | 「ページにリダイレクトがあります」が出ていないか(= B-2 の取りこぼし) |
| フォーム | KV | 実際に届いているか(毎日確認) |
| メール | Gmail | 送受信できているか |
| 頻度 | 作業 |
|---|---|
| 毎日(切替後1週間) | KV に問い合わせが溜まっていないか確認 |
| 月次 | Search Console のカバレッジ確認 |
| 年次 | 🔴 Cloudflare API トークンのローテーション(TTL切れで自動デプロイが無言で停止する。サイトは動き続けるので気づかない) |
| 都度 | お知らせ追加は site/content/news.json を編集して push(CMS なし・開発者運用) |
| # | 内容 | 誰が決めるか |
|---|---|---|
| 1 | /development-1 /work-1 の中身 — STUDIO 管理画面で目視確認。単なる複製なら §6.3 の表どおりでよい |
実さん |
| 2 | CMS 4記事を新サイトに移植するか — 移植するなら news.json に追記して /news/<slug> へ301。しないなら /news へ |
実さん |
| 3 | /contact フォームの現在の送信先 — STUDIO のフォーム機能なら解約で止まる。新サイトは KV に保存するだけで通知が飛ばない。運用として「KV を毎日見る」で足りるか、通知が要るか |
実さん |
| 4 | フォーム通知の実装 — MailChannels は 2024-08-31 提供終了。Cloudflare Email Sending は Workers 有料プラン限定。Free のままなら Resend / SendGrid の API を Worker から叩くのが現実的 | 要決定 |
| 5 | HSTS の includeSubDomains — 社内システムやステージングに http のホストが無いか確認してから |
情シス |
| 6 | dist-visual/ → dist/ へのリネーム — 移行完了後の整理タスク。今回は触らない |
後日 |
| 7 | Turnstile の導入 — 問い合わせフォームのスパム対策。無料・検証無制限。Rate Limit だけでは不十分になったら | 後日 |
/development-1 /work-1 の中身を確認したstorage.googleapis.com から回収した/contact の現在の送信先を確認したdig @ns1.xserver.jp で全レコードの正解表を作ったgoogle._domainkey)が入っていることを確認したdig @<CF-NS> と dig @ns1.xserver.jp の答えが一致したbuild.mjs / pages/)を削除した.html を落とした(grep -ro 'href="/[^"]*\.html' dist-visual/ が0件)404.html を生成した(sitemap には入れない)_redirects を作った(宛先はすべて拡張子なし)_headers を作ったwrangler.jsonc を §5.1 の完成形にしたpackage.json の依存を修正し npx tsc --noEmit がエラー0og:image を入れたworkers.dev で14ページ全て200を確認したworkers_dev: false に戻した.github/workflows/deploy.yml を追加し、PR でプレビューが出たA @ と A * を削除し Custom Domain にした以下は wrangler 4.124.0(wrangler dev --local)での実測結果。ドキュメントに明記が無かった _redirects と html_handling の評価順序を確認するために取得した。
# 既定(html_handling: auto-trailing-slash)
/services.html 307 -> /services
/services 200
/news/renewal.html 307 -> /news/renewal
/news/renewal 200
/nope 404 (404.html の本文つき)
# _redirects の宛先を .html にした場合 → 2ホップ
/service 301 -> /services.html
(続けて) 307 -> /services
(続けて) 200
# _redirects の宛先を拡張子なしにした場合 → 1ホップ
/service 301 -> /services
(続けて) 200
/about 301 -> /company
/topics 301 -> /news
/topics/dify 301 -> /news (ワイルドカード /topics/*)
/posts/zsFXXiMM 301 -> /news (ワイルドカード /posts/*)
/1 301 -> /
# html_handling: "none" にした場合 → トップが死ぬ
/services.html 200
/services 404
/ 404 ← 採用不可
wrangler deploy --dry-run で §5.1 の設定が正しく解釈されることも確認済み。
Binding Resource
env.INQUIRIES (…) KV Namespace
env.INQUIRY_LIMITER (5 requests/60s) Rate Limit
env.ASSETS Assets
.jp / .co.jp は非対応)