Work / P-04 · Jul – Sep 2026 · KAIST, 2026

A
lab
website
the
professor
can
edit
from
Notion

The site for KAIST’s Transport and Urban Planning with AI lab: Next.js on Vercel, content pulled hourly from Notion tables so a non-developer runs it, with a built-in fallback and full Korean–English parity.

Role
Designed and built as lab intern · primary developer
Recognition
Live at inhi.kim · replaced the lab’s WordPress site
Stack
Next.js 14 · TypeScript · Tailwind · framer-motion · Notion API · Vercel
Outcome
13 pages, two languages, one content surface, hourly revalidation
Visit the site
13
page routes
2
languages, rendered together and toggled
60min
revalidation, with instant refresh on demand
1
typed content surface every page reads from

Context

Jul – Sep 2026
01 — Problem

A lab site nobody could update

The lab’s old WordPress site had drifted out of date because updating it needed a developer. Publications, members, news and openings change every month; the person who knows them is the professor.

02 — Approach

Notion as the CMS, code as the fallback

Every content type — members, publications, news, gallery, projects, openings, FAQs — is a Notion table the professor edits directly. The site reads them through one typed module of getters; pages never call Notion themselves. Hourly revalidation keeps it fresh, and a token-protected refresh endpoint makes a change instant.

If Notion is empty or down, the site still renders from built-in data. Korean and English are rendered together and switched with a single attribute, so a missing translation is visible rather than silent.

00

Context

The TUPA lab’s website had stopped reflecting the lab. Members, publications and news change monthly, and the only person who knows them is the professor, who should not need a developer to update a list.

I started the rebuild two weeks before my first day in the lab and shipped it over the summer. The design decision that mattered most was not visual: content lives in Notion tables the professor already uses, and the site survives Notion being empty or down.

How it works

Fig. — Content flow, one edit
Step 01 / 6

The professor edits a row in Notion

A new paper goes into the Publications table. No code, no deploy, no developer.

11 Notion databases: members, publications, news, gallery, projects, research areas, journeys, openings, FAQs, settings, page copy

Step 02 / 6

Getters fetch on a schedule

Every page reads through one typed module of nineteen getters. Each fetch is cached for an hour and tagged, so the whole site revalidates together.

lib/content.ts · 4,017 lines · 22 types · revalidate 3600 · tag notion · raw Notion REST, no SDK

Step 03 / 6

Pages render in both languages

Korean and English are rendered into the same HTML and switched with one attribute, so a missing translation is visible rather than silent.

html[data-lang] · .ko-only / .en-only · 13 pages, 4 dynamic · no [locale] segment

Step 04 / 6

Instant refresh when it matters

For a time-sensitive change, a token-protected endpoint revalidates the tag and the layout immediately.

GET /api/refresh?token=… → revalidateTag + revalidatePath(/, layout)

Step 05 / 6

Notion is down. The site is not.

Every getter is Notion-first with a built-in fallback: the archive migrated from the old WordPress export. If Notion is empty or unreachable, the pages still render.

fallback data: ~52 members · ~153 publications · 47 news · 17 projects · 5 openings

Step 06 / 6

Live on Vercel

Static where possible, revalidated hourly, with analytics and a photo proxy for Notion’s expiring image links.

inhi.kim · 75 commits, 2 July – 1 September 2026 · /api/photo proxies Notion S3 URLs

Fig. — Content flow, one edit01 · The professor edits a row in Notion
contentcontrolfallbackeditNotion APIgettersbuildISR · every 60 min/api/refresh?tokenBuilt-in dataProfessorNotion tablesMembersPublicationsNewsOpeningslib/content.ts13 pagesVercelstage 1 / 6
prof
Prof. Inhi Kim’s lab, KAIST Cho Chun Shik Graduate School of Mobility · edits Notion directly
notion
11 databases · Notion-Version 2022-06-28 · workspace migrated to the professor’s account on 21 July 2026
getters
19 async getters, each Notion-first with a static fallback · pages never call Notion themselves
pages
research · publications · people · news · gallery · projects · prospective students · contact + 4 dynamic routes
vercel
Hourly ISR · @vercel/analytics · GA4 added later by the lab
isr
fetch revalidate 3600 with tag notion · the whole site refreshes together
refresh
Token-protected · revalidateTag + revalidatePath · used after urgent edits
fallback
Curated TS constants and JSON seeded from the WordPress WXR export

Deep dive

05
03 — Result

Shipped in September 2026

Thirteen pages, four of them dynamic, a hero video upscaled to 4K, migration scripts from the old WordPress archive, and a short user guide for the lab. The design system, Daylight Atlas, is the lab’s own.

04 — Architecture

One content surface

The rule that kept the codebase sane: pages only read from lib/content.ts. Twenty-two types, nineteen getters, each one Notion-first with a static fallback. Notion is called over raw REST rather than the SDK, with database ids overridable by environment, which made the move to the professor’s workspace in July a config change.

05 — Migration

From WordPress to tables

The old site was exported from WordPress and became curated TypeScript constants and JSON, then seed payloads for the Notion databases: fifty-one members, seventy-three papers plus later journal additions, over a hundred news items, forty-four gallery albums, seventeen projects. The same data doubles as the fallback, so the site never ships empty.

06 — Design

Daylight Atlas

The lab’s design system: paper white, cobalt and a safety orange accent, Overpass with IBM Plex Mono labels and Pretendard for Korean. The hero is a nine-second looping film of the TUPA letters as city blocks, generated, animated, upscaled to 4K and encoded to 1920p at under two megabytes.

07 — What I would do next

Parity as a test, not a convention

Bilingual parity in TUPA is structural, both languages render from the same records, but nothing fails when a Korean field is left blank. The next version gets the same kind of test this site has: a build that refuses an untranslated string.

Specification

08
Stack
Next.js 14 App Router · TypeScript · Tailwind · framer-motion · Notion REST API · Vercel
Content
11 Notion databases · 19 getters · 22 types · static fallback for every one
Pages
9 static + 4 dynamic routes · 2 API routes (refresh, photo proxy)
Freshness
ISR every 60 minutes · instant refresh endpoint
Languages
Korean and English rendered together, toggled client-side
Timeline
2 July – 1 September 2026 · 75 commits
Role
Designed and built as lab intern · primary developer
Live
inhi.kim