PageSourceSearch

https://ken109.dev/articles/edge-first-architecture

html ken109.dev collected 2026-10-05 22:52:26 UTC 49,898 bytes, 58 lines download raw bytes

1<!DOCTYPE html><html lang="ja"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/assets/styles-D6MIEP-t.css" data-precedence="default"/><link rel="stylesheet" href="/assets/index-DtxX27Jz.css" type="text/css" data-precedence="default"/><title>個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ — Kensuke Kubo</title><meta name="theme-color" content="#efe9dc"/><meta description="フロントエンドからインフラまで幅広く対応するプロダクトエンジニア。設計・実装の知見を記事として発信しています。"/><meta property="og:site_name" content="Kensuke Kubo"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:url" content="https://ken109.dev"/><meta name="description" content="TanStack Start を Cloudflare Workers で SSR し、外部 API 連携をエッジキャッシュに束ねる構成の実装。fetch の cf オプション、ConnectRPC を GET で叩く方法、実際にハマった罠を解説する。"/><meta property="og:type" content="article"/><meta property="og:title" content="個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ"/><meta property="og:description" content="TanStack Start を Cloudflare Workers で SSR し、外部 API 連携をエッジキャッシュに束ねる構成の実装。fetch の cf オプション、ConnectRPC を GET で叩く方法、実際にハマった罠を解説する。"/><meta property="og:image" content="https://ken109.dev/og/articles/edge-first-architecture"/><meta property="og:url" content="https://ken109.dev/articles/edge-first-architecture"/><meta property="article:published_time" content="2026-06-04"/><meta property="article:author" content="Kensuke Kubo"/><meta property="article:tag" content="ConnectRPC"/><meta property="twitter:title" content="個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ"/><meta property="twitter:description" content="TanStack Start を Cloudflare Workers で SSR し、外部 API 連携をエッジキャッシュに束ねる構成の実装。fetch の cf オプション、ConnectRPC を GET で叩く方法、実際にハマった罠を解説する。"/><meta property="twitter:image" content="https://ken109.dev/og/articles/edge-first-architecture"/><link rel="modulepreload" href="/assets/index-ClIfQcG2.js"/><link rel="modulepreload" href="/assets/articles._slug-x3piR9Zp.js"/><link rel="modulepreload" href="/assets/article-list-B3ruWv1x.js"/><link rel="icon" href="/favicon.ico" sizes="48x48 32x32 16x16"/><link rel="icon" href="/icon.png" type="image/png" sizes="512x512"/><link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180"/><link rel="manifest" href="/manifest.json"/><link rel="sitemap" type="application/xml" href="https://ken109.dev/sitemap.xml"/><link rel="canonical" href="https://ken109.dev/articles/edge-first-architecture"/>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Kensuke Kubo","alternateName":"ken109","url":"https://ken109.dev","jobTitle":"Product Engineer","sameAs":["https://github.com/ken109","https://x.com/ken109_","https://zenn.dev/ken109","https://dev.to/ken109"]}</script>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","name":"Kensuke Kubo","description":"フロントエンドからインフラまで幅広く対応するプロダクトエンジニア。設計・実装の知見を記事として発信しています。","url":"https://ken109.dev","inLanguage":"ja","author":{"@type":"Person","name":"Kensuke Kubo","url":"https://ken109.dev"}}</script>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ","description":"TanStack Start を Cloudflare Workers で SSR し、外部 API 連携をエッジキャッシュに束ねる構成の実装。fetch の cf オプション、ConnectRPC を GET で叩く方法、実際にハマった罠を解説する。","datePublished":"2026-06-04","keywords":"Cloudflare Workers,TanStack Start,ConnectRPC","image":"https://ken109.dev/og/articles/edge-first-architecture","inLanguage":"ja","mainEntityOfPage":{"@type":"WebPage","@id":"https://ken109.dev/articles/edge-first-architecture"},"author":{"@type":"Person","name":"Kensuke Kubo","url":"https://ken109.dev"}}</script>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://ken109.dev"},{"@type":"ListItem","position":2,"name":"Articles","item":"https://ken109.dev/articles/"},{"@type":"ListItem","position":3,"name":"個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ","item":"https://ken109.dev/articles/edge-first-architecture"}]}</script>
1</head><body><div class="min-h-dvh flex flex-col"><div aria-hidden="true"><div class="paper-sheet"></div><div class="paper-grain"></div><i class="reg reg-tl"></i><i class="reg reg-tr"></i><i class="reg reg-bl"></i><i class="reg reg-br"></i></div><header class="relative z-10"><div class="relative mx-auto max-w-[1440px] px-(--gutter)"><div class="flex flex-col gap-2 pt-5 pb-3.5 border-b border-rule sm:flex-row sm:items-baseline sm:justify-between sm:gap-5"><a tabindex="0" href="/" class="text-sm transition-all decoration-1 hover:decoration-2 decoration-border underline-offset-4 flex flex-wrap gap-x-4 caption font-normal no-underline text-ink-700 hover:text-klein"><span class="hidden sm:inline">Specimen Sheet Nº 109</span><span>ken109.dev</span><time>2026-10-06</time></a><nav aria-label="メインナビゲーション"><ol class="flex items-baseline justify-between gap-3 sm:gap-6"><li><a tabindex="0" href="/#journey" class="transition-all decoration-1 hover:decoration-2 decoration-border underline-offset-4 group font-mono text-[11px] font-medium uppercase tracking-[0.06em] no-underline text-foreground hover:text-klein"><span class="mr-1.5 hidden text-klein sm:inline">01</span>Journey</a></li><li><a tabindex="0" href="/#articles" class="transition-all decoration-1 hover:decoration-2 decoration-border underline-offset-4 group font-mono text-[11px] font-medium uppercase tracking-[0.06em] no-underline text-foreground hover:text-klein"><span class="mr-1.5 hidden text-klein sm:inline">02</span>Articles</a></li><li><a tabindex="0" href="/#now" class="transition-all decoration-1 hover:decoration-2 decoration-border underline-offset-4 group font-mono text-[11px] font-medium uppercase tracking-[0.06em] no-underline text-foreground hover:text-klein"><span class="mr-1.5 hidden text-klein sm:inline">03</span>Now</a></li><li><a tabindex="0" href="/#contact" class="transition-all decoration-1 hover:decoration-2 decoration-border underline-offset-4 group font-mono text-[11px] font-medium uppercase tracking-[0.06em] no-underline text-foreground hover:text-klein"><span class="mr-1.5 hidden text-klein sm:inline">04</span>Contact</a></li></ol></nav></div></div></header><main class="flex-1 w-full flex flex-col"><!--$--><!--$--><div class="relative mx-auto max-w-[1440px] px-(--gutter) py-16 sm:py-24"><article class="grid grid-cols-12 gap-x-6"><header class="col-span-12 lg:col-span-3 mb-12 lg:mb-0 rise" style="--rise-index:0"><div class="lg:sticky lg:top-24 space-y-8 font-mono text-xs"><div><div class="caption text-muted-foreground mb-1.5">Published</div><div class="text-foreground">2026.06.04</div></div><div><div class="caption text-muted-foreground mb-1.5">Views</div><div>91</div></div></div></header><div class="col-span-12 lg:col-span-9 lg:border-l lg:border-rule lg:pl-10 rise" style="--rise-index:1"><h1 class="text-2xl sm:text-4xl font-bold tracking-tight leading-snug">個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ</h1><div class="mt-5 mb-10 flex flex-wrap items-center gap-2"><span class="border border-rule px-2.5 py-1 font-mono text-[10px] tracking-[0.15em] uppercase text-foreground/70">Cloudflare Workers</span><span class="border border-rule px-2.5 py-1 font-mono text-[10px] tracking-[0.15em] uppercase text-foreground/70">TanStack Start</span><span class="border border-rule px-2.5 py-1 font-mono text-[10px] tracking-[0.15em] uppercase text-foreground/70">ConnectRPC</span></div><div><p class="text-[15px] sm:text-base leading-8 text-foreground/90 mb-10">このサイトはオリジンサーバーを持たない。SSR も外部 API 連携も Cloudflare Workers だけで完結し、運用コストはほぼゼロ。鍵は<mark class="bg-foreground
1/8 text-inherit px-1 py-0.5 box-decoration-clone">「外部 API への fetch をすべてエッジキャッシュに束ねる」</mark>設計で、 その実装と実際にハマった罠をこの記事にまとめる。</p><ul class="my-5 space-y-2"><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>対象読者</strong>: 個人サイトや小規模サービスを Cloudflare Workers で SSR 運用したい人。React と fetch API の基本を前提とする</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>環境</strong>: TanStack Start 1.167 / @cloudflare/vite-plugin 1.39 / wrangler 4.95</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>わかること</strong>: 外部 API をエッジキャッシュで束ねる実装パターン、ConnectRPC を CF キャッシュに乗せる方法、SSR バンドルで実際に踏んだ罠</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>扱わないこと</strong>: Workers の課金体系、D1/KV を使った永続化</li></ul><h2 id="section-01" data-toc-index="01" class="mt-14 mb-6 pt-6 pb-4 border-t border-b border-rule flex items-baseline gap-4 scroll-mt-24"><span class="font-mono text-xs text-muted-foreground">01</span><span data-toc-title="true" class="text-xl sm:text-2xl font-bold tracking-tight">結論: リクエストはエッジで完結させる</span></h2><p class="text-[15px] leading-8 text-foreground/85 my-5">構成の要点は3つ。<strong>① SSR は Worker、静的アセットは Workers Static Assets</strong>(Worker の起動すら発生しない)、<strong>② 外部 API(Zenn / GitHub / Bluesky / 自前の ConnectRPC)はすべて SSR の loader から fetch し、エッジに 1〜24 時間キャッシュ</strong>、<strong>③ どの連携が落ちてもセクションが消えるだけ</strong>でページは壊れない。DB も cron も持たないため、<span class="underline decoration-klein/70 decoration-2 underline-offset-[5px]">障害点は実質 Worker 1 つ</span>になる。</p><figure class="my-8 border border-rule p-5 sm:p-6"><div><div class="group relative"><div class="w-full font-mono text-(--dg-ink) transition-opacity opacity-0" style="--dg-ink:var(--diagram-ink, var(--foreground));--dg-stroke:var(--diagram-stroke, var(--foreground));--dg-fill:var(--diagram-fill, var(--background));--dg-inverse-ink:var(--diagram-inverse-ink, var(--background));--dg-inverse-fill:var(--diagram-inverse-fill, var(--foreground));
1--dg-accent:var(--diagram-accent, var(--destructive));--dg-accent-ink:var(--diagram-accent-ink, var(--destructive-foreground));--dg-muted-ink:var(--diagram-muted-ink, var(--muted-foreground));--dg-muted-fill:var(--diagram-muted-fill, var(--muted));--dg-muted-stroke:var(--diagram-muted-stroke, var(--muted-foreground));--dg-line:var(--diagram-line, var(--border));height:320px"><div data-testid="rf__wrapper" style="width:100%;height:100%;overflow:hidden;position:relative;z-index:0" class="react-flow light" role="application"><div class="react-flow__renderer" style="position:absolute;width:100%;height:100%;top:0;left:0"><div class="react-flow__pane" style="position:absolute;width:100%;height:100%;top:0;left:0"><div class="react-flow__viewport xyflow__viewport react-flow__container" style="transform:translate(0px,0px) scale(1)"><div class="react-flow__edges"></div><div class="react-flow__edgelabel-renderer"></div><div class="react-flow__nodes" style="position:absolute;width:100%;height:100%;top:0;left:0"></div><div class="react-flow__viewport-portal"></div></div></div></div><div id="react-flow__node-desc-1" style="display:none">Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.</div><div id="react-flow__edge-desc-1" style="display:none">Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.</div><div id="react-flow__aria-live-1" aria-live="assertive" aria-atomic="true" style="position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;overflow:hidden;clip:rect(0px, 0px, 0px, 0px);clip-path:inset(100%)"></div></div></div><button aria-label="図を全画面で開く" class="absolute top-2 right-2 z-10 flex size-7 cursor-zoom-in items-center justify-center border border-(--dg-stroke) bg-(--dg-fill) text-(--dg-ink) opacity-50 transition-opacity hover:opacity-100 group-hover:opacity-80" style="--dg-ink:var(--diagram-ink, var(--foreground));--dg-stroke:var(--diagram-stroke, var(--foreground));--dg-fill:var(--diagram-fill, var(--background));--dg-inverse-ink:var(--diagram-inverse-ink, var(--background));--dg-inverse-fill:var(--diagram-inverse-fill, var(--foreground));
1--dg-accent:var(--diagram-accent, var(--destructive));--dg-accent-ink:var(--diagram-accent-ink, var(--destructive-foreground));--dg-muted-ink:var(--diagram-muted-ink, var(--muted-foreground));--dg-muted-fill:var(--diagram-muted-fill, var(--muted));--dg-muted-stroke:var(--diagram-muted-stroke, var(--muted-foreground));--dg-line:var(--diagram-line, var(--border))" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 size-3.5" aria-hidden="true"><path d="M15 3h6v6"></path><path d="m21 3-7 7"></path><path d="m3 21 7-7"></path><path d="M9 21H3v-6"></path></svg></button></div></div><figcaption class="mt-4 border-t border-border pt-2.5 font-mono text-[10px] tracking-[0.1em] text-muted-foreground uppercase">Fig. 1 — リクエストの流れ。外部 API は loader から fetch しエッジに乗せる</figcaption></figure><h2 id="section-02" data-toc-index="02" class="mt-14 mb-6 pt-6 pb-4 border-t border-b border-rule flex items-baseline gap-4 scroll-mt-24"><span class="font-mono text-xs text-muted-foreground">02</span><span data-toc-title="true" class="text-xl sm:text-2xl font-bold tracking-tight">外部 API は fetch の cf オプションで束ねる</span></h2><p class="text-[15px] leading-8 text-foreground/85 my-5">
1結論から言うと、<mark class="bg-foreground/8 text-inherit px-1 py-0.5 box-decoration-clone">自前のキャッシュ層を書く必要はない</mark>。Workers の <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">fetch</code> は<code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">cf</code> <!-- -->オプションを渡すだけでレスポンスをエッジキャッシュに乗せられる。このサイトの全連携は次の 1 パターンで実装している。</p><figure class="group relative overflow-hidden border my-6 rounded-none border-rule bg-transparent"><figcaption class="flex items-center justify-between gap-2 pl-4 pr-2 py-1.5 border-b border-border font-mono text-[10px] tracking-wider text-muted-foreground">features/articles/lib/external-articles.ts (抜粋)<button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*=&#x27;size-&#x27;])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent cursor-pointer size-7 text-muted-foreground hover:text-foreground" type="button" aria-label="コードをコピー"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy size-3.5" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></figcaption><pre class="p-4 overflow-x-auto"><code class="hljs font-mono text-[13px] leading-6 bg-transparent"><span class="hljs-keyword">const</span> <span class="hljs-variable constant_">FETCH_INIT</span> = {
2  <span class="hljs-attr">headers</span>: { <span class="hljs-string">&quot;User-Agent&quot;</span>: <span class="hljs-string">&quot;ken109.dev (https://ken109.dev)&quot;</span> },
3  <span class="hljs-attr">cf</span>: { <span class="hljs-attr">cacheTtl</span>: <span class="hljs-number">3600</span>, <span class="hljs-attr">cacheEverything</span>: <span class="hljs-literal">true</span> }
4} <span class="hljs-keyword">as</span> <span class="hljs-title class_">RequestInit</span>;
5
6<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-variable constant_">ZENN_API</span>, <span class="hljs-variable constant_">FETCH_INIT</span>);</code></pre></figure><p class="text-[15px] leading-8 text-foreground/85 my-5">最初はモジュールスコープの変数に TTL 付きで持つ「メモリキャッシュ」を書いたが、すぐ捨てた。比較すると:</p><div dir="ltr" data-slot="scroll-area" class="flex flex-col relative overflow-hidden my-6" style="position:relative;--radix-scroll-area-corner-width:0px;--radix-scroll-area-corner-height:0px"><style>[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}</style><div style="overflow-x:hidden;overflow-y:hidden;min-width:100%;display:table" data-radix-scroll-area-viewport="" data-slot="scroll-area-viewport" class="focus-visible:ring-ring/50 rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 flex-grow w-full !block"><div class=""><table class="w-full border border-rule text-sm"><thead><tr class="bg-foreground"><th class="px-4 py-2.5 text-left caption text-background/90 font-bold">方式</th><th class="px-4 py-2.5 text-left caption text-background/90 font-bold">生存期間</th><th class="px-4 py-2.5 text-left caption text-background/90 font-bold">コスト</th><th class="px-4 py-2.5 text-left caption text-background/90 font-bold">備考</th></tr></thead><tbody><tr class="border-b border-rule last:border-b-0 even:bg-foreground
6/3"><td class="px-4 py-2.5 align-top leading-6">モジュールスコープ変数</td><td class="px-4 py-2.5 align-top leading-6">isolate が生きている間だけ</td><td class="px-4 py-2.5 align-top leading-6">無料</td><td class="px-4 py-2.5 align-top leading-6">いつ消えるか保証がなく、colo 毎・isolate 毎にバラバラ</td></tr><tr class="border-b border-rule last:border-b-0 even:bg-foreground/3"><td class="px-4 py-2.5 align-top leading-6">cf オプション (採用)</td><td class="px-4 py-2.5 align-top leading-6">TTL まで (colo 単位)</td><td class="px-4 py-2.5 align-top leading-6">無料</td><td class="px-4 py-2.5 align-top leading-6">コード 1 行で済む。GET / HEAD のみ対象</td></tr><tr class="border-b border-rule last:border-b-0 even:bg-foreground/3"><td class="px-4 py-2.5 align-top leading-6">Workers KV</td><td class="px-4 py-2.5 align-top leading-6">明示的に消すまで</td><td class="px-4 py-2.5 align-top leading-6">無料枠あり</td><td class="px-4 py-2.5 align-top leading-6">colo を跨いで共有したい場合はこちら</td></tr></tbody></table></div></div></div><p class="text-[15px] leading-8 text-foreground/85 my-5">注意点は3つ。</p><ul class="my-5 space-y-2"><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">cf.cacheTtl</code> が効くのは <strong>GET リクエストのみ</strong>。POST は素通りする</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">JSON API のレスポンスは既定ではキャッシュ対象外のため <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">cacheEverything: true</code> <!-- -->で対象化する。オリジンの <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">Cache-Control</code> を上書きして TTL を強制するのは <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">cacheTtl</code> <!-- -->の役割</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">キャッシュは <strong>colo(データセンター)単位</strong>でグローバル共有ではない。世界中からのアクセスは colo 毎に 1 回ずつ API を叩く。それでも個人サイトなら GitHub API の未認証レート制限(60 req/h/IP)に収まるし、 気になる場合は Smart Tiered Cache(全プラン無料・オプトイン)で上位 colo に集約できる</li></ul><p class="text-[15px] leading-8 text-foreground/85 my-5">もう1つ、実際に踏んだ罠: dev.to API が User-Agent なしのリクエストを拒否し、<code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">Promise.allSettled</code> で握りつぶしていたため「Zenn だけ表示されて dev.to が消える」状態に気づくのが遅れたã€
6‚<mark class="bg-foreground/8 text-inherit px-1 py-0.5 box-decoration-clone">複数ソースを束ねるときは、片方だけ失敗した不完全な結果をキャッシュしない</mark>こと。</p><h2 id="section-03" data-toc-index="03" class="mt-14 mb-6 pt-6 pb-4 border-t border-b border-rule flex items-baseline gap-4 scroll-mt-24"><span class="font-mono text-xs text-muted-foreground">03</span><span data-toc-title="true" class="text-xl sm:text-2xl font-bold tracking-tight">ConnectRPC は NO_SIDE_EFFECTS で GET にする</span></h2><p class="text-[15px] leading-8 text-foreground/85 my-5">自前 API(ConnectRPC)の読み取り RPC も同じパターンに乗せたい。しかし Connect の RPC はデフォルトで POST なのでエッジキャッシュに乗らない。結論:<!-- --> <mark class="bg-foreground/8 text-inherit px-1 py-0.5 box-decoration-clone">proto に <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">idempotency_level = NO_SIDE_EFFECTS</code> を付けると GET で叩けるようになる</mark>。</p><figure class="group relative overflow-hidden border my-6 rounded-none border-rule bg-transparent"><figcaption class="flex items-center justify-between gap-2 pl-4 pr-2 py-1.5 border-b border-border font-mono text-[10px] tracking-wider text-muted-foreground">stats.proto<button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*=&#x27;size-&#x27;])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent cursor-pointer size-7 text-muted-foreground hover:text-foreground" type="button" aria-label="コードをコピー"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy size-3.5" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></figcaption><pre class="p-4 overflow-x-auto"><code class="hljs font-mono text-[13px] leading-6 bg-transparent"><span class="hljs-keyword">service </span><span class="hljs-title class_">StatsService</span> {
7  <span class="hljs-function"><span class="hljs-keyword">rpc</span> GetPublicStats(GetPublicStatsRequest) <span class="hljs-keyword">returns</span> (GetPublicStatsResponse) </span>{
8    <span class="hljs-keyword">option</span> idempotency_level = NO_SIDE_EFFECTS;
9  }
10}</code></pre></figure><p class="text-[15px] leading-8 text-foreground/85 my-5">クライアント側はコード生成すら不要で、素の fetch で叩ける:</p><figure class="group relative overflow-hidden border my-6 rounded-none border-rule bg-transparent"><figcaption class="flex items-center justify-between gap-2 pl-4 pr-2 py-1.5 border-b border-border font-mono text-[10px] tracking-wider text-muted-foreground">Connect GET プロトコルの URL<button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*=&#x27;size-&#x27;])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent cursor-pointer size-7 text-muted-foreground hover:text-foreground" type="button" aria-label="コードをコピー"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy size-3.5" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></figcaption><pre class="p-4 overflow-x-auto"><code class="hljs font-mono text-[13px] leading-6 bg-transparent">GET https://api.example.com/pkg.v1.StatsService/GetPublicStats
11    ?encoding=json&amp;message=%7B%7D&amp;connect=v1
12// → {&quot;publicWallpaperCount&quot;:19, &quot;creatorCount&quot;:6, &quot;totalLicenseCount&quot;:&quot;81&quot;}</code></pre></figure>
12<p class="text-[15px] leading-8 text-foreground/85 my-5">ここに罠が1つ。レスポンス末尾を見ると <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">totalLicenseCount</code> だけ文字列になっている。<span class="underline decoration-klein/70 decoration-2 underline-offset-[5px]">proto の <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">int64</code> は JSON では精度保護のため文字列にシリアライズされる</span>仕様で、<code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">Number()</code> での変換を忘れると <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">NaN</code> や文字列連結バグになる。</p><figure class="my-8 border border-rule p-5 sm:p-6"><div class="group relative"><div class="w-full font-mono text-(--dg-ink) transition-opacity opacity-0" style="--dg-ink:var(--diagram-ink, var(--foreground));--dg-stroke:var(--diagram-stroke, var(--foreground));--dg-fill:var(--diagram-fill, var(--background));--dg-inverse-ink:var(--diagram-inverse-ink, var(--background));--dg-inverse-fill:var(--diagram-inverse-fill, var(--foreground));
12--dg-accent:var(--diagram-accent, var(--destructive));--dg-accent-ink:var(--diagram-accent-ink, var(--destructive-foreground));--dg-muted-ink:var(--diagram-muted-ink, var(--muted-foreground));--dg-muted-fill:var(--diagram-muted-fill, var(--muted));--dg-muted-stroke:var(--diagram-muted-stroke, var(--muted-foreground));--dg-line:var(--diagram-line, var(--border));height:320px"><div data-testid="rf__wrapper" style="width:100%;height:100%;overflow:hidden;position:relative;z-index:0" class="react-flow light" role="application"><div class="react-flow__renderer" style="position:absolute;width:100%;height:100%;top:0;left:0"><div class="react-flow__pane" style="position:absolute;width:100%;height:100%;top:0;left:0"><div class="react-flow__viewport xyflow__viewport react-flow__container" style="transform:translate(0px,0px) scale(1)"><div class="react-flow__edges"></div><div class="react-flow__edgelabel-renderer"></div><div class="react-flow__nodes" style="position:absolute;width:100%;height:100%;top:0;left:0"></div><div class="react-flow__viewport-portal"></div></div></div></div><div id="react-flow__node-desc-1" style="display:none">Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.</div><div id="react-flow__edge-desc-1" style="display:none">Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.</div><div id="react-flow__aria-live-1" aria-live="assertive" aria-atomic="true" style="position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;overflow:hidden;clip:rect(0px, 0px, 0px, 0px);clip-path:inset(100%)"></div></div></div><button aria-label="図を全画面で開く" class="absolute top-2 right-2 z-10 flex size-7 cursor-zoom-in items-center justify-center border border-(--dg-stroke) bg-(--dg-fill) text-(--dg-ink) opacity-50 transition-opacity hover:opacity-100 group-hover:opacity-80" style="--dg-ink:var(--diagram-ink, var(--foreground));--dg-stroke:var(--diagram-stroke, var(--foreground));--dg-fill:var(--diagram-fill, var(--background));--dg-inverse-ink:var(--diagram-inverse-ink, var(--background));--dg-inverse-fill:var(--diagram-inverse-fill, var(--foreground));
12--dg-accent:var(--diagram-accent, var(--destructive));--dg-accent-ink:var(--diagram-accent-ink, var(--destructive-foreground));--dg-muted-ink:var(--diagram-muted-ink, var(--muted-foreground));--dg-muted-fill:var(--diagram-muted-fill, var(--muted));--dg-muted-stroke:var(--diagram-muted-stroke, var(--muted-foreground));--dg-line:var(--diagram-line, var(--border))" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 size-3.5" aria-hidden="true"><path d="M15 3h6v6"></path><path d="m21 3-7 7"></path><path d="m3 21 7-7"></path><path d="M9 21H3v-6"></path></svg></button></div><figcaption class="mt-4 border-t border-border pt-2.5 font-mono text-[10px] tracking-[0.1em] text-muted-foreground uppercase">Fig. 2 — エッジキャッシュの hit / miss。2 回目以降は API に到達しない</figcaption></figure><h2 id="section-04" data-toc-index="04" class="mt-14 mb-6 pt-6 pb-4 border-t border-b border-rule flex items-baseline gap-4 scroll-mt-24"><span class="font-mono text-xs text-muted-foreground">04</span><span data-toc-title="true" class="text-xl sm:text-2xl font-bold tracking-tight">SSR バンドルのハマりどころ</span></h2><p class="text-[15px] leading-8 text-foreground/85 my-5">結論: Worker にバンドルされるのは「実行されるコード」ではなく<mark class="bg-foreground
12/8 text-inherit px-1 py-0.5 box-decoration-clone">「import されるコード」全部</mark>。具体例を2つ。</p><ul class="my-5 space-y-2"><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>巨大依存が SSR バンドルに入る</strong>: 図表描画に使っている elkjs はクライアントで動的 import しているが、チャンクとしては Worker 側にも含まれ <span class="underline decoration-klein/70 decoration-2 underline-offset-[5px]">gzip 537KB</span> を占める。Workers のサイズ上限(無料 3MB / 有料 10MB gzip)に近づいたら最初に疑う場所</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground"><strong>環境変数の取り回し</strong>: <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">process.env</code> は <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">nodejs_compat</code> フラグ + 新しめの互換日付(本サイトは 2025-09-02 で確認)で Worker の env から自動的に展開される。secret は<!-- --> <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">wrangler secret put</code>、ローカルは <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">.dev.vars</code>(gitignore 必須)</li></ul><p class="text-[15px] leading-8 text-foreground/85 my-5">もう1つ設計面で効いたのは <strong>graceful degradation</strong>。各連携の取得関数は失敗時に <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">null</code> / 空配列を返し、UI 側は「セクションごと非表示」に倒す。外部 API が全部死んでもページは壊れず、表示が減るだけになる。</p><figure class="group relative overflow-hidden border my-6 rounded-none border-rule bg-transparent"><figcaption class="flex items-center justify-between gap-2 pl-4 pr-2 py-1.5 border-b border-border font-mono text-[10px] tracking-wider text-muted-foreground">失敗時はセクション非表示に倒す<button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*=&#x27;size-&#x27;])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent cursor-pointer size-7 text-muted-foreground hover:text-foreground" type="button" aria-label="コードをコピー"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy size-3.5" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></figcaption><pre class="p-4 overflow-x-auto"><code class="hljs font-mono text-[13px] leading-6 bg-transparent"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fetchStats = <span class="hljs-title function_">createServerFn</span>({ <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;GET&quot;</span> }).<span class="hljs-title function_">handler</span>(<span class="hljs-title function_">async</span> () =&gt; {
13  <span class="hljs-keyword">try</span> {
14    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-variable constant_">API</span>, <span class="hljs-variable constant_">FETCH_INIT</span>);
15    <span class="hljs-keyword">if</span> (!response.<span class="hljs-property">ok</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;   <span class="hljs-comment">// ← throw しない</span>
16    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> response.<span class="hljs-property">json</span>&lt;<span class="hljs-title class_">Stats</span>&gt;();
17  } <span class="hljs-keyword">catch</span> {
18    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
18                     <span class="hljs-comment">// ← UI 側: {stats &amp;&amp; &lt;StatsBand /&gt;}</span>
19  }
20});</code></pre></figure><h2 id="section-05" data-toc-index="05" class="mt-14 mb-6 pt-6 pb-4 border-t border-b border-rule flex items-baseline gap-4 scroll-mt-24"><span class="font-mono text-xs text-muted-foreground">05</span><span data-toc-title="true" class="text-xl sm:text-2xl font-bold tracking-tight">この構成が向くケース・向かないケース</span></h2><p class="text-[15px] leading-8 text-foreground/85 my-5"><strong>向くケース</strong></p><ul class="my-5 space-y-2"><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">個人サイト・ドキュメントサイト・LP: コンテンツがコードと API 集約で完結する</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">表示する外部データに「数時間の鮮度」で十分なもの(記事一覧・統計・SNS フィード)</li></ul><p class="text-[15px] leading-8 text-foreground/85 my-5"><strong>向かないケース</strong></p><ul class="my-5 space-y-2"><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">書き込みが主体のアプリ: キャッシュ戦略よりデータストア(D1 / 外部 DB)の設計が支配的になる</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">リアルタイム性が必要なデータ(価格・在庫など): TTL キャッシュの恩恵がない</li><li class="text-[15px] leading-7 text-foreground/85 pl-5 relative before:content-[&#x27;—&#x27;] before:absolute before:left-0 before:text-muted-foreground">colo を跨いだ厳密なキャッシュ一貫性が必要な場合: cf オプションでは制御できず KV / DO が必要</li></ul><p class="text-[15px] leading-8 text-foreground/85 my-5">逆に言えば、読み取り中心で鮮度要件が緩いサイトなら、この構成は「無料枠・デプロイ 1 分・運用ゼロ」で回る。 まず<!-- --> <code class="font-mono text-[13px] bg-secondary px-1.5 py-0.5">cf: <!-- -->{ cacheTtl, cacheEverything }</code> の 1 行から試してみてほしい。</p></div><footer class="mt-20 pt-8 border-t border-rule"><div class="flex items-baseline justify-between mb-8"><div class="caption text-foreground">More Articles</div><a tabindex="0" class="text-sm decoration-1 hover:decoration-2 decoration-border underline-offset-4 no-underline caption font-normal text-muted-foreground hover:text-foreground transition-colors active" href="/articles" data-status="active" aria-current="page">All Articles →</a></div><ul class="border-t border-rule"><li data-dimmed="false" class="border-b border-rule"><a href="https://zenn.dev/ken109/articles/cloud-tasks-emulator" target="_blank" rel="noopener noreferrer" class="group grid grid-cols-12 gap-x-6 gap-y-1 py-5 px-4 -mx-4 items-baseline transition-colors duration-200 hover:bg-klein-wash"><span class="col-span-12 sm:col-span-2 caption text-muted-foreground">2026.06.21</span><span class="col-span-12 sm:col-span-8 text-lg sm:text-xl font-extrabold leading-snug transition-colors duration-200 group-hover:text-klein">Cloud Tasks にもローカルエミュレータが欲しかったので作った</span><span class="hidden sm:flex col-span-2 items-baseline justify-end caption tracking-[0.12em] text-muted-foreground group-hover:text-klein transition-colors duration-200">Zenn<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3 ml-1 self-center" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></a></li><li data-dimmed="false" class="border-b border-rule"><a tabindex="0" href="/articles/testing-strategy-ai-e2e" class="text-sm decoration-1 hover:decoration-2 decoration-border underline-offset-4 group grid grid-cols-12 gap-x-6 gap-y-1 py-5 px-4 -mx-4 items-baseline transition-colors duration-200 hover:bg-klein-wash no-underline font-normal"><span class="col-span-12 sm:col-span-2 caption text-muted-foreground">2026.06.10</span><span class="col-span-12 sm:col-span-8 text-lg sm:text-xl font-extrabold leading-snug transition-colors duration-200 group-hover:text-klein">AI でテストの執筆コストが 0 になったら全部 E2E でいいのか — testcontainers とテストごと DB 分離で考える</span></a></li><li data-dimmed="false" class="border-b border-rule"><a href="https://zenn.dev/fluxlay/articles/fluxlay-claude-code-plugin" target="_blank" rel="noopener noreferrer" class="group grid grid-cols-12 gap-x-6 gap-y-1 py-5 px-4 -mx-4 items-baseline transition-colors duration-200 hover:bg-klein-wash"><span class="col-span-12 sm:col-span-2 caption text-muted-foreground">2026.05.04</span><span class="col-span-12 sm:col-span-8 text-lg sm:text-xl font-extrabold leading-snug transition-colors duration-200 group-hover:text-klein">Claude Code に「動く壁紙」を書かせるプラグインを作った</span><span class="hidden sm:flex col-span-2 items-baseline justify-end caption tracking-[0.12em] text-muted-foreground group-hover:text-klein transition-colors duration-200">Zenn<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3 ml-1 self-center" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></a></li></ul><div class="mt-10 flex justify-end"><a tabindex="0" href="/#contact" class="text-sm decoration-1 hover:decoration-2 decoration-border underline-offset-4 no-underline caption font-normal text-muted-foreground hover:text-foreground transition-colors">お仕事のご相談はこちら →</a></div></footer></div></article></div><!--/$--><!--/$--></main><footer class="relative mt-30"><div class="relative mx-auto max-w-[1440px] px-(--gutter)"><div class="border-t border-ink-900 pt-3.5 pb-12"><div class="flex flex-wrap items-baseline justify-between gap-x-5 gap-y-1.5 caption"><span>© <!-- -->2026<!-- --> Kensuke Kubo — ken109.dev</span><span class="flex flex-wrap gap-x-4"><a href="https://github.com/ken109" target="_blank" rel="noopener noreferrer" class="border-b border-rule hover:border-klein hover:text-klein">GitHub</a><a href="https://x.com/ken109_" target="_blank" rel="noopener noreferrer" class="border-b border-rule hover:border-klein hover:text-klein">X</a><a href="https://zenn.dev/ken109" target="_blank" rel="noopener noreferrer" class="border-b border-rule hover:border-klein hover:text-klein">Zenn</a><a href="https://dev.to/ken109" target="_blank" rel="noopener noreferrer" class="border-b border-rule hover:border-klein hover:text-klein">DEV</a></span></div></div></div></footer></div>
20<script class="$tsr" id="$tsr-stream-barrier">(self.$R=self.$R||{})["tsr"]=[];self.$_TSR={h(){this.hydrated=!0,this.c()},e(){this.streamEnded=!0,this.c()},c(){this.hydrated&&this.streamEnded&&(delete self.$_TSR,delete self.$R.tsr)},p(e){this.initialized?e():this.buffer.push(e)},buffer:[]};$_TSR.router=($R=>$R[0]={manifest:$R[1]={routes:$R[2]={__root__:$R[3]={preloads:$R[4]=["/assets/index-ClIfQcG2.js"],assets:$R[5]=[$R[6]={tag:"link",attrs:$R[7]={rel:"stylesheet",href:"/assets/index-DtxX27Jz.css",type:"text/css"}},$R[8]={tag:"script",attrs:$R[9]={type:"module",async:!0},children:"import(\"/assets/index-ClIfQcG2.js\")"}]},"/articles/$slug":$R[10]={preloads:$R[11]=["/assets/articles._slug-x3piR9Zp.js","/assets/article-list-B3ruWv1x.js"]}}},matches:$R[12]=[$R[13]={i:"__root__�",u:1791240746638,s:"success",ssr:!0},$R[14]={i:"�articles�$slug�articles�edge-first-architecture",u:1791240746851,s:"success",l:$R[15]={slug:"edge-first-architecture",title:"個人サイトをオリジンサーバーなしで運用する — Cloudflare Workers SSR の構成とハマりどころ",description:"TanStack Start を Cloudflare Workers で SSR し、外部 API 連携をエッジキャッシュに束ねる構成の実装。fetch の cf オプション、ConnectRPC を GET で叩く方法、実際にハマった罠を解説する。",publishedAt:"2026-06-04",tags:$R[16]=["Cloudflare Workers","TanStack Start","ConnectRPC"],views:91},ssr:!0}],lastMatchId:"�articles�$slug�articles�edge-first-architecture",dehydratedData:$R[17]={queryStream:$R[18]=($R[19]=(e) => new ReadableStream({ start: (r) => {
21	e.on({
22		next: (a) => {
23			try {
24				r.enqueue(a);
25			} catch (t) {}
26		},
27		throw: (a) => {
28			r.error(a);
29		},
30		return: () => {
31			try {
32				r.close();
33			} catch (a) {}
34		}
35	});
36} }))($R[20]=($R[21]=() => {
37	let e = [], r = [], t = !0, n = !1, a = 0, s = (l, g, S) => {
38		for (S = 0; S < a; S++) r[S] && r[S][g](l);
39	}, i = (l, g, S, d) => {
40		for (g = 0, S = e.length; g < S; g++) d = e[g], !t && g === S - 1 ? l[n ? "return" : "throw"](d) : l.next(d);
41	}, u = (l, g) => (t && (g = a++, r[g] = l), i(l), () => {
42		t && (r[g] = r[a], r[a--] = void 0);
43	});
44	return {
45		__SEROVAL_STREAM__: !0,
46		on: (l) => u(l),
47		next: (l) => {
48			t && (e.push(l), s(l, "next"));
49		},
50		throw: (l) => {
51			t && (e.push(l), s(l, "throw"), t = !1, n = !1, r.length = 0);
52		},
53		return: (l) => {
54			t && (e.push(l), s(l, "return"), t = !1, n = !0, r.length = 0);
55		}
56	};
57})()),dehydratedQueryClient:$R[22]={mutations:$R[23]=[],queries:$R[24]=[$R[25]={dehydratedAt:1791240746851,state:$R[26]={data:$R[27]=[$R[28]={slug:"zenn-cloud-tasks-emulator",title:"Cloud Tasks にもローカルエミュレータが欲しかったので作った",description:"",publishedAt:"2026-06-21",tags:$R[29]=[],externalUrl:"https://zenn.dev/ken109/articles/cloud-tasks-emulator",source:"Zenn"},$R[30]={slug:"zenn-fluxlay-claude-code-plugin",title:"Claude Code に「動く壁紙」を書かせるプラグインを作った",description:"",publishedAt:"2026-05-04",tags:$R[31]=[],externalUrl:"https://zenn.dev/fluxlay/articles/fluxlay-claude-code-plugin",source:"Zenn"},$R[32]={slug:"zenn-fluxlay-dev-dashboard-wallpaper",title:"壁紙に PR とカレンダーと Pomodoro を埋め込んだ",description:"",publishedAt:"2026-05-03",tags:$R[33]=[],externalUrl:"https://zenn.dev/fluxlay/articles/fluxlay-dev-dashboard-wallpaper",source:"Zenn"},$R[34]={slug:"zenn-fluxlay-clickable-live-wallpaper",title:"クリックスルーなライブ壁紙の中の React UI を操作可能にする",description:"",publishedAt:"2026-05-03",tags:$R[35]=[],externalUrl:"https://zenn.dev/fluxlay/articles/fluxlay-clickable-live-wallpaper",source:"Zenn"},$R[36]={slug:"zenn-fluxlay-philosophy-live-wallpaper",title:"なぜ壁紙のプラットフォームを作っているのか — Fluxlay の哲学",description:"",publishedAt:"2026-04-29",tags:$R[37]=[],externalUrl:"https://zenn.dev/fluxlay/articles/fluxlay-philosophy-live-wallpaper",source:"Zenn"},$R[38]={slug:"zenn-react-live-wallpaper-macos-fluxlay",title:"React で macOS のライブ壁紙を作れるツールを開発した",description:"",publishedAt:"2026-04-07",tags:$R[39]=[],externalUrl:"https://zenn.dev/fluxlay/articles/react-live-wallpaper-macos-fluxlay",source:"Zenn"},$R[40]={slug:"devto-i-built-a-live-wallpaper-engine-for-macos-heres-how-it-works-5ed",title:"I Built a Live Wallpaper Engine for macOS — Here's How It Works",description:"Your desktop wallpaper is a static image. It has been since the day you set it up. Mine was...",publishedAt:"2026-04-07",tags:$R[41]=["showdev","react","rust","webdev"],externalUrl:"https://dev.to/ken109/i-built-a-live-wallpaper-engine-for-macos-heres-how-it-works-5ed",source:"DEV"}],dataUpdateCount:1,dataUpdatedAt:1791240746851,error:null,errorUpdate
57Count:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:"success",fetchStatus:"idle"},queryKey:$R[42]=["external-articles"],queryHash:"[\"external-articles\"]"}]}}})($R["tsr"]);document.currentScript.remove()</script>
57<script type="module" async="">import("/assets/index-ClIfQcG2.js")</script>
57<script>($R=>$R[20].return(void 0))($R["tsr"]);$_TSR.e();document.currentScript.remove()</script>
57<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"344a403179da454db8c901a41ba25504","r":1,"spa":2}' crossorigin="anonymous"></script>
57
58</body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.