Files
kotobane/docs/superpowers/specs/2026-05-28-kotobane-cms-website-design.md
T

11 KiB
Raw Blame History

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 <Image> 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 <title>
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 23 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:

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:

// 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. Bootstrapcreate-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 clientlib/directus.ts with typed helpers + lib/types.ts
  4. LayoutNavbar + Footer, root layout.tsx
  5. HomepageHeroSection + ArticleGrid + page.tsx
  6. Article pageArticleCard, ArticleBody (rich text renderer), full article page
  7. Category page — listing with pagination
  8. Search overlaySearchOverlay component wired to Cmd+K
  9. Revalidate endpoint/api/revalidate + test with curl
  10. Directus Flow — set up webhook trigger in Directus
  11. SEOgenerateMetadata 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)