# Kotobane — CMS-Driven Website Plan ## Context Kotobane is a Japanese pop-culture news site (VTubers, Anime, Manga, Games, Music, Japan, Culture, Industry) with a fully-defined design system. The goal is to build a website where content can be added and updated from a Directus CMS without touching code or redeploying. The user runs everything on a VPS and already has a Directus instance (fresh install, no collections yet). There is a single author (no authors collection needed). **Chosen architecture:** Next.js 14 App Router with ISR + on-demand revalidation. Pages are served as pre-built static HTML; when the editor publishes in Directus, a Directus Flow fires a webhook to `/api/revalidate` which surgically regenerates only the affected pages — no rebuild, no redeploy, content live in seconds. **Design reference:** `docs/design-reference/kotobane-reference.html` — single file with four tabs: Homepage mockup, Article page mockup, Color system, Directus workflow. Open in a browser for full visual reference. **Directus instance:** `https://cms.achmad.dev` --- ## Stack - **Frontend:** Next.js 14 (App Router), TypeScript, Tailwind CSS - **CMS:** Directus (existing VPS instance) - **Fonts:** Inter (English) + Noto Sans JP (Japanese) via `next/font` - **Icons:** Lucide React - **Rich text rendering:** `@directus/sdk` + custom renderer for Directus rich text blocks - **Image optimization:** Next.js `` pointing at Directus `/assets/` endpoint --- ## Final Color Palette ### Brand Accent | Token | Hex | Usage | |---|---|---| | accent | `#00B4D8` | Primary buttons, links, active nav, "Read →", focus rings | | accent-hover | `#00D4FF` | Hover state | ### Status / Badge Colors | Token | Hex | Usage | |---|---|---| | violet | `#7C3AED` | Featured / hero badges, secondary actions | | coral | `#D64545` | Trending, breaking news, error states | | amber | `#FFB300` | New indicator, alerts, warnings | | green | `#00D166` | Published status, success states | ### Backgrounds | Token | Hex | Usage | |---|---|---| | bg | `#0D0D14` | Main page background | | bg-elevated | `#171A21` | Navbar, sidebars | | bg-card | `#1D212B` | Cards, inputs | | border | `#2A3140` | All borders and dividers | ### Text | Token | Hex | Usage | |---|---|---| | text-primary | `#F3F5F7` | Headlines, body | | text-secondary | `#B8C0CC` | Excerpts, captions | | text-muted | `#7D8795` | Timestamps, category labels, metadata | | text-disabled | `#5C6470` | Disabled / placeholder | > Category names in nav and cards use `text-muted` — no per-category color coding. Colors are reserved for status badges only. --- ## Directus Data Model Create these collections in Directus admin: ### `articles` | Field | Type | Notes | |---|---|---| | title | string | required | | slug | string | unique, required — used in URL | | status | string | `published` / `draft` — Directus built-in | | content | rich text (WYSIWYG) | full article body | | excerpt | text | short blurb shown on cards | | featured_image | file (M2O → directus_files) | hero image | | published_at | datetime | set on first publish | | is_featured | boolean | controls hero slot on homepage | | seo_title | string | optional override for `` | | seo_description | string | optional override for meta description | | category | M2O → categories | required | | tags | M2M → tags (junction: articles_tags) | optional | ### `categories` | Field | Type | Notes | |---|---|---| | name | string | Anime, VTubers, Manga, Games, Music, Japan, Culture, Industry | | slug | string | unique — used in URL | | description | text | shown on category listing page | ### `tags` | Field | Type | Notes | |---|---|---| | name | string | e.g. "hololive", "Spring 2025" | | slug | string | unique — for URL filtering | ### `site_settings` (singleton) | Field | Type | Notes | |---|---|---| | site_name | string | default: "Kotobane" | | hero_article | M2O → articles | which article appears in the homepage hero | | nav_categories | JSON / M2M → categories | ordered list for navbar | --- ## Directus Flow (webhook trigger) Create one Flow in Directus: - **Trigger:** "Event Hook" on `articles.items.update` and `articles.items.create` where `status = published` - **Action:** Webhook POST to `https://yourdomain.com/api/revalidate` - **Payload:** `{ "secret": "REVALIDATE_SECRET", "article_id": "{{$trigger.key}}" }` The revalidate endpoint receives the article ID, then queries Directus itself to get the full `slug` and `category.slug` — this is more reliable than relying on the Flow payload containing all fields (Directus only sends changed fields, so `category` may be absent if only `status` changed). Also add a second Flow trigger on `site_settings` update to revalidate the homepage. --- ## Next.js Project Structure ``` kotobane/ ├── app/ │ ├── layout.tsx # Root layout: fonts, navbar, footer │ ├── page.tsx # Homepage (ISR) │ ├── [category]/ │ │ ├── page.tsx # Category listing (ISR) │ │ └── [slug]/ │ │ └── page.tsx # Article detail (ISR) │ └── api/ │ └── revalidate/ │ └── route.ts # POST endpoint for Directus webhook ├── components/ │ ├── layout/ │ │ ├── Navbar.tsx # Logo, nav categories, search icon │ │ └── Footer.tsx │ ├── home/ │ │ ├── HeroSection.tsx # Featured article hero │ │ └── ArticleGrid.tsx # Latest articles grid │ ├── article/ │ │ ├── ArticleCard.tsx # Card used in grids and listings │ │ ├── ArticleBody.tsx # Rich text renderer │ │ └── TagRow.tsx │ └── search/ │ └── SearchOverlay.tsx # Cmd+K command palette search ├── lib/ │ ├── directus.ts # Directus SDK client + typed fetch helpers │ └── types.ts # TypeScript types mirroring Directus collections ├── tailwind.config.ts # Design system tokens (colors, spacing, fonts) └── .env.local # DIRECTUS_URL, DIRECTUS_TOKEN, REVALIDATE_SECRET ``` --- ## Pages ### `/` — Homepage (`app/page.tsx`) - Fetch: `site_settings` (hero article) + latest 12 published articles - Sections: Hero → Latest grid → articles grouped by 2–3 featured categories - ISR: `revalidate = false` (on-demand only, via webhook) ### `/[category]` — Category listing (`app/[category]/page.tsx`) - Fetch: category by slug + articles filtered by that category, sorted by `published_at` desc - `generateStaticParams`: pre-build all category slugs at deploy time - Pagination: load more button (client-side, hits Directus with `offset`) ### `/[category]/[slug]` — Article (`app/[category]/[slug]/page.tsx`) - Fetch: single article by slug with `fields=*,tags.*,category.*` - Sections: Hero image → title → metadata → rich text body → tags → related articles (same category, limit 4) - `generateMetadata`: uses `seo_title` / `seo_description` if set, falls back to article title/excerpt - `generateStaticParams`: pre-build all published articles at deploy time; `dynamicParams = true` (Next.js default) ensures new articles published after deploy are generated on first request via ISR — no rebuild needed ### `/api/revalidate` — Webhook handler (`app/api/revalidate/route.ts`) - Accepts POST with `{ secret, slug, category_slug }` - Validates secret matches `REVALIDATE_SECRET` env var - Calls `revalidatePath('/')`, `revalidatePath('/[category]', 'page')`, `revalidatePath('/[category]/[slug]', 'page')` ### Search — `SearchOverlay` (global, client component) - Triggered by Cmd+K or clicking the search icon in Navbar - Debounced input (300ms) → live GET to Directus `/items/articles?search=query&limit=8&fields=title,slug,category.slug` - Results rendered as keyboard-navigable list (↑↓ Enter) - No ISR needed — always queries Directus live --- ## Design System Integration (`tailwind.config.ts`) Map the design system tokens directly into Tailwind: ```ts colors: { bg: { DEFAULT: '#0D0D14', elevated: '#171A21', card: '#1D212B' }, border: { DEFAULT: '#2A3140' }, accent: { DEFAULT: '#00B4D8', hover: '#00D4FF' }, violet: '#7C3AED', coral: '#D64545', amber: '#FFB300', green: '#00D166', text: { primary: '#F3F5F7', secondary: '#B8C0CC', muted: '#7D8795', disabled: '#5C6470' }, }, borderRadius: { sm: '8px', md: '12px', lg: '16px', xl: '20px', '2xl': '28px', }, ``` Standard Tailwind spacing (multiples of 4/8) maps cleanly to the 8px spacing system. --- ## Directus SDK Client (`lib/directus.ts`) Use `@directus/sdk` with a typed schema. Key helpers: ```ts // Singleton client const directus = createDirectus(DIRECTUS_URL).with(rest()) // Typed fetch helpers getArticles(options) // list with filters getArticleBySlug(slug) // single article getCategoryBySlug(slug) // single category getAllCategories() // for navbar + generateStaticParams getSiteSettings() // homepage hero + nav order searchArticles(query) // search overlay ``` All helpers use `readItems` / `readSingleton` from the SDK. Token auth via `staticToken(DIRECTUS_TOKEN)` — create a read-only token in Directus for the frontend. --- ## Environment Variables ``` DIRECTUS_URL=https://cms.achmad.dev DIRECTUS_TOKEN=<read-only static token from Directus> REVALIDATE_SECRET=<random string, shared with Directus Flow> ``` --- ## Deployment (VPS) - Run Next.js as a Node server: `next build && next start -p 3000` - Use PM2 or systemd to keep it running - Nginx reverse proxy in front (handles SSL, proxies to port 3000) - Directus already running on same VPS (separate port/domain) --- ## Implementation Order 1. **Bootstrap** — `create-next-app`, install deps (`@directus/sdk`, `lucide-react`), configure Tailwind with design tokens, set up fonts 2. **Directus collections** — create all 4 collections + junction table in Directus admin, add a few test articles 3. **SDK client** — `lib/directus.ts` with typed helpers + `lib/types.ts` 4. **Layout** — `Navbar` + `Footer`, root `layout.tsx` 5. **Homepage** — `HeroSection` + `ArticleGrid` + `page.tsx` 6. **Article page** — `ArticleCard`, `ArticleBody` (rich text renderer), full article page 7. **Category page** — listing with pagination 8. **Search overlay** — `SearchOverlay` component wired to Cmd+K 9. **Revalidate endpoint** — `/api/revalidate` + test with curl 10. **Directus Flow** — set up webhook trigger in Directus 11. **SEO** — `generateMetadata` on article + category pages, `sitemap.ts`, `robots.ts` 12. **Deploy** — build, PM2/systemd, Nginx config --- ## Verification - Add a test article in Directus → confirm it appears on homepage within seconds (no restart) - Update an article → confirm revalidate endpoint fires and page updates - Test search overlay with Cmd+K - Check mobile layout (single-column per design system) - Confirm Directus going offline does not break cached pages for visitors - Lighthouse score: aim for 90+ performance (static HTML + lazy images)