This is a Next.js project bootstrapped with create-next-app.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun devOpen http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.
This project uses next/font to automatically optimize and load Geist, a new font family for Vercel.
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
Posts are not files in this repo. They live as rows in the Sprintable backend, served by
a public read API (story e5731937) — /ko/blog and /ko/blog/{slug} fetch() that API on
every request. Publishing a post is a server-side write behind an approval gate; it never
touches this repo, never triggers a deploy, never needs a commit — a new post shows up here
within the API's cache TTL (60s), not on the next git push.
This replaced an earlier design (story 2b4067b5) that treated content/blog/{lang}/{slug}.md
committed by a GitHub connector as the source of truth — reverted per PO/선생님 decision
("글 1편=커밋 1건은 구조적으로 틀림", 2026-09-03): a commit-per-post model doesn't scale, and
ties every publish to this repo's deploy pipeline for no reason once posts are just data.
- Contract (
lib/site-posts.ts, canonical text lives in bothsprintable/e5731937andsprintable-landing/15a18511story bodies — identical, either is authoritative):GET {SPRINTABLE_PUBLIC_API_BASE}/api/v2/public/site-posts?public_key=&lang=→{"posts": [{slug, title, summary|null, tags[], lang, published_at}]}(published_at desc, no body) — unknownpublic_key→ 404, missinglang→ 400.GET {SPRINTABLE_PUBLIC_API_BASE}/api/v2/public/site-posts/{slug}?public_key=&lang=→{slug, title, summary, tags, lang, published_at, body_md, source_story_id}— not found → 404.body_mdis rendered to HTML withmarkedat request time (nonode:fsanywhere in this path — that's what makes these two routes safe as ordinary Edge Functions, see below).- Response carries
Cache-Control: public, s-maxage=60, stale-while-revalidate=300; the fetch calls pass Cloudflare's{ cf: { cacheTtl: 60 } }extension to respect it. getAllBlogPosts/getBlogPostBySlugnever throw on a missingpublic_key, a non-2xx response, or a network failure — they resolve to[]/null, so the list page falls back to its empty-state copy and the detail page 404s vianotFound(), exactly like a real empty/missing state would (PO instruction: "API 오류/빈 목록 → 빈 상태 문구, throw 0").
- Why both routes are plain Edge Functions, not statically prerendered — this was worked
out the hard way on the previous (file-based) design and the reasoning still applies now
that both routes fetch instead of reading files: every route here inherits
runtime = "edge"from the root layout, and Next 16 (Turbopack) flatly disables static generation for any page under edge runtime. An earlier attempt overroderuntime = "nodejs"to regain static generation —next buildlooked fine (○ /ko/blog), but@cloudflare/next-on-pagesrejects anodejs-runtime page with zero prerendered instances as "must be a live function", which only surfaces when the post count is actually 0 — exactly the state this repo shipped in before the first post existed. Real incident: CF Pages deploy run33700034117failed onmainthis way, verified only after running the realnpx @cloudflare/next-on-pagesbuild (not justnext build, whose route table looks identical for the working and the broken version — seefeedback-cf-pages-runtime-verify-wranglerdiscipline) against both the 0-post and 1-post case. Both routes now render as ordinary Edge Functions (like/already does) — trivially fast since there's no I/O beyond the one upstream fetch. - View-count beacon (
app/components/blog/view-beacon.tsx) —fetch(..., {keepalive: true})(notnavigator.sendBeacon, which sendstext/plainand the backend 422s on that) toPOST {SPRINTABLE_PUBLIC_API_BASE}/api/v2/public/pageview, body{public_key, path, referrer}(field name confirmed directly againstbackend/app/routers/public_pageview.py::PageviewBeaconRequestin story fbcc07b5/PR#3728, separate backend work). Config is a plain constants file (lib/sprintable-public-api.config.ts, PO decision — not env vars: neither value is a secret, and the beacon and site-posts API share the same base URL and public key) —SPRINTABLE_ORG_PUBLIC_KEYships as an empty string until the backend stories above deploy, and both consumers no-op (skip the fetch entirely, or resolve to empty/null) while it's empty, so this repo has no ordering dependency on either backend story landing first. - Locale: this repo has no existing
/ko-prefixed routing (next-intl here is a cookie/Accept-Language-based single-tree, no middleware, no[locale]segment) —/ko/blogis a standalone folder, independent of that cookie logic.getTranslations({locale: 'ko', ...})needed a real fix toi18n/request.tsto actually work:getRequestConfig's callback previously ignoredrequestLocaleentirely and always resolved from cookies, so an explicit override was silently dropped — the callback now readsrequestLocalefirst, falling back to the existing cookie/header logic exactly as before when no override is given (zero behavior change for the rest of the site).
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
