Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
RemixでVersion skewに立ち向かう
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
chimame
September 25, 2024
Technology
1.3k
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
RemixでVersion skewに立ち向かう
Remix Tokyo Meetup #2
chimame
September 25, 2024
More Decks by chimame
See All by chimame
Cloudflare is Agents
chimame
0
210
知って得する@cloudflare_vite-pluginのあれこれ
chimame
2
690
Boost Your Web Performance with Hyperdrive
chimame
1
580
私がエッジを使う理由
chimame
10
4.2k
GraphQL Server on Edge after that
chimame
1
1.9k
Accelerating App Dev with Cloudflare Workers
chimame
1
520
GraphQL Server on Edge
chimame
12
6.8k
エッジで輝くフロントエンド
chimame
11
7k
Cloudflare Workersと状態管理
chimame
4
2.1k
Other Decks in Technology
See All in Technology
顧客の成果創出とプロダクトの成長を 両立するためのFDE
sansantech
PRO
0
610
猫でもわかるKiro Web
kentapapa
1
160
人にやさしく、AIにやさしく、書き手を選ばないIaCのガードレール再考 / Rethinking IaC Guardrails for Humans and AI Alike
kohbis
5
2k
組み立てて楽しむ AWS Blocks 入門
kmiya84377
0
110
[2026-09-30]ロックンロールは鳴り止まないっ - 信頼性かまってちゃん - 「データ駆動を投げ捨ててまで。」追いかける信頼性改善に向けた取り組みの話
tosite
0
180
【Findyテック文化祭ワークショップ】新卒エンジニア&採用担当と作る、 なりたい姿と今やるべき一歩
dip_tech
PRO
0
150
AI臭い文章とは何なのか
nasuvitz
38
77k
3人で1000GPU超を統合運用する?マルチクラウド&オンプレを跨ぐ、構築と運用のリアル!
kazukun0716
2
810
OSC2026on_the-world-is-waiting-for-your-voice.pdf
naruoga
0
220
プラットフォームを「作る」、 チームに「入り込む」
sansantech
PRO
0
470
Incremental HTTP
kazuho
5
1.9k
セルフサービスのオブザーバビリティ基盤をOpenTelemetryで作る / Building a Self-Service Observability Platform with OpenTelemetry
ymotongpoo
3
510
Featured
See All Featured
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.5k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Mind Mapping
helmedeiros
1
390
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
700
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
WCS-LA-2024
lcolladotor
0
840
Heart Work Chapter 1 - Part 1
lfama
PRO
10
36k
Why You Should Never Use an ORM
jnunemaker
PRO
61
10k
The Language of Interfaces
destraynor
162
27k
Ruling the World: When Life Gets Gamed
codingconduct
0
380
Darren the Foodie - Storyboard
khoart
PRO
4
4k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
410
Transcript
RemixͰVersion skewʹཱ͔ͪ͏ Remix Tokyo Meetup #2 2024.09.25
contents - Version skewͱ - ΫϥΠΞϯτͰରԠ͢Δ - αʔόͰରԠ͢Δ - ·ͱΊ
2
Version skewͱ 1 3
“ Version skewͱͦͷ··ͷ௨Γ όʔδϣϯͷࠩҟʹΑͬͯ Ҿ͖ى͜͞ΕΔͷ͜ͱ 4
ྫ͑RemixͳΒ… 5 <Form method="post"> <input type="email" name="emal" required /> <textarea
name="message" required /> </Form> export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); formData.get("emal"); … } Typo͍ͯ͠Δ͕ ಈ͘ͷಈ͘
ྫ͑RemixͳΒ… 6 <Form method="post"> <input type="email" name="email" required /> <textarea
name="message" required /> </Form> export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); formData.get("email"); … } मਖ਼Λͯ͠σϓϩΠ͢Δ
ΫϥΠΞϯτͱαʔόͷόʔδϣϯ ࣌ܥྻ ΫϥΠΞϯτ αʔό 1.࠷ॳͷόʔδϣϯΛσϓϩΠ͢Δ - v1 2.Ϣʔβ͕ΞΫηε͢Δ v1 v1
3.मਖ਼൛ΛσϓϩΠ͢Δ v1 v2 4.Ϣʔβ͕αϒϛοτ͢Δ v1 v2 7 ؆୯ʹࠩҟ͕ग़ͯɺઌఔͷྫͩͱΤϥʔʹͳΔ 7
Ͳ͏ରࡦ͢Δ͔ 1. ޙํޓΛߟ͑ͭͭɺίʔυΛमਖ਼͢Δ େମ͜ͷํ๏͕औΒΕΔͱࢥ͏ 2. ΫϥΠΞϯτΛαʔόͷόʔδϣϯʹ͢Δ ϑϩϯτ͕ͦͦݹ͍͜ͱΛͱͯ͠ରॲ͢Δํ๏ 8 3. αʔόΛΫϥΠΞϯτͷόʔδϣϯʹ߹ΘͤΔ
ݹ͍··ͳΒݹ͍αʔόڥͰಈ࡞͢Δ (ύεʹ /api/v1 తͳߟ͑) 8
Ͳ͏ରࡦ͢Δ͔ 1. ޙํޓΛߟ͑ͭͭɺίʔυΛमਖ਼͢Δ େମ͜ͷํ๏͕औΒΕΔͱࢥ͏ 2. ΫϥΠΞϯτΛαʔόͷόʔδϣϯʹ͢Δ ϑϩϯτ͕ͦͦݹ͍͜ͱΛͱͯ͠ରॲ͢Δํ๏ 9 3. αʔόΛΫϥΠΞϯτͷόʔδϣϯʹ߹ΘͤΔ
ݹ͍··ͳΒݹ͍αʔόڥͰಈ࡞͢Δ (ύεʹ /api/v1 తͳߟ͑) ͜ΕΛ͠·͢ 9
ΫϥΠΞϯτͰରԠ͢Δ 2 10
“ ΫϥΠΞϯτ͕ݹ͍ͷͰαʔόͷ όʔδϣϯʢ࠷৽ʣʹߋ৽͢Δ 11
“ ͍ͭʁ🤔 12
“ Ϣʔβʹҧײ༩͑ͳ͍λΠϛϯά ϧʔςΟϯάͷมߋ࣌ 13
ΫϥΠΞϯτΛߋ৽͢ΔLinkίϯϙʔωϯτ 14 import { type LinkProps, Link as RemixLink, useHref
} from "@remix-run/react"; import { forwardRef, useCallback } from "react"; import clientVersion from "~/version.json"; export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { const { onClick, ...rest } = props; const href = useHref(props.to, { relative: props.relative }); const handleClick = useCallback( async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { onClick?.(e); if (e.defaultPrevented) { return; } const currentUrl = new URL(window.location.href); const res = await fetch(new URL("/version.json", currentUrl).href); if (res.ok) { const serverVersion = await res.json<{ version: string }>(); if (serverVersion.version !== clientVersion.version) { location.href = href; } } }, [onClick, href], ); return <RemixLink ref={ref} {...rest} onClick={handleClick} />; });
େࣄͳͱ͜Ζ͚ͩ 15 import clientVersion from "~/version.json"; export const Link =
forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { … const handleClick = useCallback( async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { … const currentUrl = new URL(window.location.href); const res = await fetch(new URL("/version.json", currentUrl).href); if (res.ok) { const serverVersion = await res.json<{ version: string }>(); if (serverVersion.version !== clientVersion.version) { location.href = href; } } }, [onClick, href], ); return <RemixLink ref={ref} {...rest} onClick={handleClick} />; });
େࣄͳͱ͜Ζ͚ͩ 16 import clientVersion from "~/version.json"; export const Link =
forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { … const handleClick = useCallback( async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { … const currentUrl = new URL(window.location.href); const res = await fetch(new URL("/version.json", currentUrl).href); if (res.ok) { const serverVersion = await res.json<{ version: string }>(); if (serverVersion.version !== clientVersion.version) { location.href = href; } } }, [onClick, href], ); return <RemixLink ref={ref} {...rest} onClick={handleClick} />; }); αʔόͷόʔδϣϯΛऔಘͯ͘͠Δ
େࣄͳͱ͜Ζ͚ͩ 17 import clientVersion from "~/version.json"; export const Link =
forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { … const handleClick = useCallback( async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { … const currentUrl = new URL(window.location.href); const res = await fetch(new URL("/version.json", currentUrl).href); if (res.ok) { const serverVersion = await res.json<{ version: string }>(); if (serverVersion.version !== clientVersion.version) { location.href = href; } } }, [onClick, href], ); return <RemixLink ref={ref} {...rest} onClick={handleClick} />; }); όʔδϣϯࠩҟ͕͋Εϖʔδ͝ͱ࠶औಘ͢Δ
“ ҰԠ͜ΕͰʢͦΕͬΆ͘ʣಈ͘ɻ ͨͩɺ͕͋Δɻ 18
େࣄͳͱ͜Ζ͚ͩ 19 import clientVersion from "~/version.json"; export const Link =
forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => { … const handleClick = useCallback( async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { … const currentUrl = new URL(window.location.href); const res = await fetch(new URL("/version.json", currentUrl).href); if (res.ok) { const serverVersion = await res.json<{ version: string }>(); if (serverVersion.version !== clientVersion.version) { location.href = href; } } }, [onClick, href], ); return <RemixLink ref={ref} {...rest} onClick={handleClick} />; }); ඇಉظॲཧͳͷͰϖʔδ࠶औಘΛ͢Δલʹ ϧʔςΟϯάطʹ࣮ߦ͞Ε͍ͯΔʂʂ
ରࡦͱͯ͠ඍົ 1. ϧʔςΟϯά͕ߦΘΕ͍ͯΔͷͰloader࣮ߦ͞ΕΔ loader͔ΒͷJSONσʔλͷऔಘ͍ͬͯΔ 2. ͞ΒʹϖʔδΛߋ৽͢ΔͷͰ࠶loader͕࣮ߦ͞ΕΔ ϖʔδߋ৽Λ͢Δʹloader͕ࣄ্࣮2ճ࣮ߦ͞ΕΔ 20 3. LinkίϯϙʔωϯτҎ֎ʹFromίϯϙʔωϯτ
useFetcherͳͲରԠ͕ඞཁʹͳΔ ରԠͤ͞Δʹ৭ʑͳॲཧΛϥοϓ͢Δඞཁ͕͋Δɻ ಛʹuseSubmitհͰ͋Δɻ 20
αʔόͰରԠ͢Δ 3 21
“ αʔόଆͰΫϥΠΞϯτͱͷ όʔδϣϯൺֱΛߦ͏ 22
“ 23 ΫϥΠΞϯτͷόʔδϣϯΛ Ͳ͏ͬͯαʔόʹૹΔʁ🤔
“ 🍪 24
“ 25 ॳճʹϖʔδΛऔಘͨ͠ࡍʹ ͦͷ࣌ͷόʔδϣϯΛCookieΛ ৯͓͚ͤͯ͞ϦΫΤετ࣌ʹ औಘ࣌ͷόʔδϣϯΛૹͬͯ͘Δ
entry.server.tsxͰόʔδϣϯΛૹ͓ͬͯ͘ 26 import { renderToReadableStream } from "react-dom/server"; import versionJson
from "./version.json"; export default async function handleRequest( responseStatusCode: number, responseHeaders: Headers, ) { let isError = false; const body = await renderToReadableStream(...); … responseHeaders.append( "Set-Cookie", `version=${versionJson.version}; Max-Age=${60 * 60 * 24 * 365}; Path=/; SameSite=Lax; Secure; HttpOnly`, ); return new Response(body, { headers: responseHeaders, status: isError ? 500 : responseStatusCode, }); } CookieʹόʔδϣϯΛ ઃఆ͓ͯ͘͠
load-context.tsʹόʔδϣϯൺֱΛߦ͏ॲཧΛ ఆٛ͢Δ 27 import versionJson from "./app/version.json"; const getClientVersion =
(request: Request) => { const cookieHeader = request.headers.get("Cookie"); const clientVersion = cookieHeader ?.split(";") ?.find((c) => c.trim().startsWith("version=")) ?.split("=")[1]; return clientVersion; }; const isSameVersion = (request: Request) => { const clientVersion = getClientVersion(request); return clientVersion === versionJson.version; }; export const validSameVersion = async (request: Request) => { const url = new URL(request.url); if (!url.searchParams.has(" data")) { return; } const isSame = isSameVersion(request); if (!isSame) { url.searchParams.delete(" data"); throw redirectDocument(url.toString()); } }; export const getLoadContext: GetLoadContext = ({ context, request }) => { return { ...context, checkSameVersion() { validSameVersion(request); }, }; };
load-context.tsʹόʔδϣϯൺֱΛߦ͏ॲཧΛ ఆٛ͢Δ 28 const isSameVersion = async (request: Request) =>
{ const clientVersion = getClientVersion(request); return clientVersion === versionJson.version; }; export const validSameVersion = (request: Request) => { const url = new URL(request.url); if (!url.searchParams.has(" data")) { return; } const isSame = isSameVersion(request); if (!isSame) { url.searchParams.delete("_data"); throw redirectDocument(url.toString()); } }; ϖʔδऔಘͷϦΫΤετ Ͱൺֱ͠ͳ͍ ʢ࠷৽͕औಘ͞ΕΔͨΊʣ
load-context.tsʹόʔδϣϯൺֱΛߦ͏ॲཧΛ ఆٛ͢Δ 29 const isSameVersion = async (request: Request) =>
{ const clientVersion = getClientVersion(request); return clientVersion === versionJson.version; }; export const validSameVersion = (request: Request) => { const url = new URL(request.url); if (!url.searchParams.has("_data")) { return; } const isSame = isSameVersion(request); if (!isSame) { url.searchParams.delete("_data"); throw redirectDocument(url.toString()); } }; JSONऔಘϦΫΤετ࣌ʹૹ͖ͬͯͨ࣌ʹൺֱ͢Δɻ Cookieʹ͋ΔόʔδϣϯͱαʔόͷόʔδϣϯΛ ൺֱͯ͠ҟͳΕ ʮredirectDocumentʯ Ͱ ࢦఆURLΛϦϩʔυͤ͞Δ
࡞ͨ͠contextͷؔΛloaderʹࠐΉ 30 export const loader = async ({ context }:
LoaderFunctionArgs) => { await context.checkSameVersion(); … return { }; }; όʔδϣϯ͕ҟͳΕ ʮredirectDocumentʯ Λthrowͯ͠ΔͷͰ loaderͷޙଓॲཧߦΘΕͳ͍
“ 31 ΊͰͨ͠ΊͰͨ͠👏
“ 32 🙅
“ loaderredirectͰ ରॲ͚ͨ͠Ͳactionʁ🤔 33
“ 🚨 Caution 🚨 ͔͜͜Β͔ͳΓྗٕͰ͢ 34
“ શόʔδϣϯ͝ͱͷڥΛ ͯ͢४උ͓͍ͯͯ͠ɺ ΫϥΠΞϯτͷόʔδϣϯʹ ରԠͨ͠αʔόͰॲཧ͢Δ💪 35
ߏஙΠϝʔδ 36 v1 དྷͨϦΫΤετͷΫϥΠΞϯτͷόʔδϣϯʹΑͬͯ ϦΫΤετઌͷRemixΛৼΓ͚Δϩʔυόϥϯαʔͷ Α͏ͳͷΛΠϝʔδ v2 v3 v4 v5
v2 v3 v4 v5 v1
“ 37
Cloudflare PagesͷPreview deploymentsΛ͏ 38 • PagesͷϓϩδΣΫτʹຊ൪ͱಉ͡ઃఆʢҧ͏ઃఆ ग़དྷΔʣͰผURLͷڥΛ࡞ͬͯ͘ΕΔ • ແ੍ݶʹڥΛ࡞ΕΔ •
ࢲ͕͏جຊతͳ༻్ͱͯ͠ຊ൪ʹग़͢લͷ࠷ऴ֬ೝ Ͱ͏͜ͱ͕ଟ͍ υΩϡϝϯτͪ͜ΒʢˠQRͰಡΈࠐΉͷָ͕ʣ https://developers.cloudflare.com/pages/platform/limits/#preview-deployments 38
σϓϩΠ࣌ʹόʔδϣϯຖͷڥ༻ҙ͓ͯ͘͠ ʢྫ: GitHub Actionsʣ 39 - name: Get short SHA
id: get sha run: echo "short sha=$(git rev-parse --short HEAD)" >> $GITHUB OUTPUT - name: Deploy to Cloudflare Pages run: | echo '{"version":"${{ github.sha }}"}' > public/version.json echo '{"version":"${{ github.sha }}"}' > app/version.json npm run build wrangler pages deploy --branch ${{ steps.get sha.outputs.short sha }} wrangler pages deploy --branch main shell: bash ※Θ͔Γ͍͢Α͏ʹΘ͟ͱwrangler cliͰॻ͍͍ͯ·͢
σϓϩΠ࣌ʹόʔδϣϯຖͷڥ༻ҙ͓ͯ͘͠ ʢྫ: GitHub Actionsʣ 40 - name: Get short SHA
id: get_sha run: echo "short sha=$(git rev-parse --short HEAD)" >> $GITHUB OUTPUT - name: Deploy to Cloudflare Pages run: | echo '{"version":"${{ github.sha }}"}' > public/version.json echo '{"version":"${{ github.sha }}"}' > app/version.json npm run build wrangler pages deploy --branch ${{ steps.get sha.outputs.short sha }} wrangler pages deploy --branch main shell: bash σϓϩΠ࣌ͷόʔδϣϯΛܾΊΔ
σϓϩΠ࣌ʹόʔδϣϯຖͷڥ༻ҙ͓ͯ͘͠ ʢྫ: GitHub Actionsʣ 41 - name: Get short SHA
id: get_sha run: echo "short sha=$(git rev-parse --short HEAD)" >> $GITHUB OUTPUT - name: Deploy to Cloudflare Pages run: | echo '{"version":"${{ github.sha }}"}' > public/version.json echo '{"version":"${{ github.sha }}"}' > app/version.json npm run build wrangler pages deploy --branch ${{ steps.get sha.outputs.short sha }} wrangler pages deploy --branch main shell: bash ಛఆόʔδϣϯͷڥʹσϓϩΠ͢Δ branchΦϓγϣϯͰΛ༻͢Δͱ https://<branch>.<project name>.pages.dev ͱ͍͏ಠཱͨ͠ڥ͕࡞͞ΕΔ
σϓϩΠ࣌ʹόʔδϣϯຖͷڥ༻ҙ͓ͯ͘͠ ʢྫ: GitHub Actionsʣ 42 - name: Get short SHA
id: get sha run: echo "short sha=$(git rev-parse --short HEAD)" >> $GITHUB OUTPUT - name: Deploy to Cloudflare Pages run: | echo '{"version":"${{ github.sha }}"}' > public/version.json echo '{"version":"${{ github.sha }}"}' > app/version.json npm run build wrangler pages deploy --branch ${{ steps.get sha.outputs.short sha }} wrangler pages deploy --branch main shell: bash ຊ൪ͷڥʹσϓϩΠ͢Δ
“ MiddlewareͰΫϥΠΞϯτͷ όʔδϣϯʹରԠͨ͠αʔόʹ ׂΓৼͬͯResponseΛฦ͢ 43
const isOldVersionRequest = (request: Request) => { const method =
request.method.toUpperCase(); const clientVersion = getClientVersion(request); return ( clientVersion && clientVersion !== serverVersion.version && method !== "GET" ); }; functions/_middleware.tsʹͯϦΫΤετΛׂΓৼΔ 44 const originRequest: PagesFunction<Env> = async (context) => { const url = new URL(context.request.url); const shortVersion = getClientVersion(context.request)?.slice(0, 8); if (shortVersion && isOldVersionRequest(context.request)) { url.hostname = `${shortVersion}.${context.env.PROJECT_NAME}.pages.dev`; return await fetch(url.toString(), context.request); } return await context.next(); }; export const onRequest = [originRequest];
const isOldVersionRequest = (request: Request) => { const method =
request.method.toUpperCase(); const clientVersion = getClientVersion(request); return ( clientVersion && clientVersion !== serverVersion.version && method !== "GET" ); }; functions/_middleware.tsʹͯϦΫΤετΛׂΓৼΔ 45 const originRequest: PagesFunction<Env> = async (context) => { const url = new URL(context.request.url); const shortVersion = getClientVersion(context.request)?.slice(0, 8); if (shortVersion && isOldVersionRequest(context.request)) { url.hostname = `${shortVersion}.${context.env.PROJECT_NAME}.pages.dev`; return await fetch(url.toString(), context.request); } return await context.next(); }; export const onRequest = [originRequest]; چόʔδϣϯ͔νΣοΫ
const isOldVersionRequest = (request: Request) => { const method =
request.method.toUpperCase(); const clientVersion = getClientVersion(request); return ( clientVersion && clientVersion !== serverVersion.version && method !== "GET" ); }; functions/_middleware.tsʹͯϦΫΤετΛׂΓৼΔ 46 const originRequest: PagesFunction<Env> = async (context) => { const url = new URL(context.request.url); const shortVersion = getClientVersion(context.request)?.slice(0, 8); if (shortVersion && isOldVersionRequest(context.request)) { url.hostname = `${shortVersion}.${context.env.PROJECT_NAME}.pages.dev`; return await fetch(url.toString(), context.request); } return await context.next(); }; export const onRequest = [originRequest]; چόʔδϣϯͷ߹ରԠͨ͠αʔό͔ΒResponseΛ࡞͢Δ
const isOldVersionRequest = (request: Request) => { const method =
request.method.toUpperCase(); const clientVersion = getClientVersion(request); return ( clientVersion && clientVersion !== serverVersion.version && method !== "GET" ); }; functions/_middleware.tsʹͯϦΫΤετΛׂΓৼΔ 47 const originRequest: PagesFunction<Env> = async (context) => { const url = new URL(context.request.url); const shortVersion = getClientVersion(context.request)?.slice(0, 8); if (shortVersion && isOldVersionRequest(context.request)) { url.hostname = `${shortVersion}.${context.env.PROJECT_NAME}.pages.dev`; return await fetch(url.toString(), context.request); } return await context.next(); }; export const onRequest = [originRequest]; ࠷৽ͷ߹ࣗͰ ResponseΛ࡞͢Δ
·ͱΊ 4 48
·ͱΊ • Version skewͷશͳରԠΛߦ͏ʹͦΕͳΓͷ ڥߏங͕ඞཁͰ͢ɻ • loaderͷॲཧ͚ͩͰೖΕ͓ͯ͘ͷ͕Φεεϝ͠ Ͱ͢ɻ͕ൃੜ͠ʹ͘͘ͳΔͱಉ࣌ʹDOMߋ৽ ʹڧ͘ͳΔͷͰΦεεϝͰ͢ɻʢͨͩ͠loader͕ ແ͍ϖʔδಈ࡞͠ͳ͍ʣ
• Cloudflare Pages͍ͬͯΔͳΒPreview deploymentsΞϓϦέʔγϣϯӡ༻ʹ৺ڧ͍ຯ ํͳͷͰ༻ਪͰ͢ɻ 49 49
Thank you! name: chimame / rito job: WebΤϯδχΞ field: Cloudflare,
GCP, AWS, Ruby, Node.js, TypeScript, React, Next.js, Remix, Docker etc company: Goensגࣜձࣾ( https://about.goen-s.com ) twitter: @chimame_rt GitHub: chimame 50 Any questions?