chore: add brainstorming docs and design reference
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
# 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 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)
|
||||
@@ -0,0 +1,263 @@
|
||||
# Kotobane — Brand & Design System
|
||||
|
||||
> "Words carried on wings." — 言羽
|
||||
|
||||
Kotobane is a Japanese pop-culture news site covering VTubers, Anime, Manga, Games, Music, Japan, Culture, and Industry.
|
||||
|
||||
---
|
||||
|
||||
## Brand Identity
|
||||
|
||||
**What it should feel like:**
|
||||
- A modern Japanese digital newspaper
|
||||
- A refined anime culture publication
|
||||
- A calm but fast-moving stream of information
|
||||
|
||||
**What it must never feel like:**
|
||||
- A loud clickbait anime blog
|
||||
- A generic SaaS dashboard
|
||||
- An overly playful otaku forum
|
||||
|
||||
**Emotional keywords:** intelligent · fast · atmospheric · minimal · editorial · futuristic · trustworthy
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Brand Accent
|
||||
| Token | Hex | Usage |
|
||||
|---|---|---|
|
||||
| accent | `#00B4D8` | Buttons, links, active nav, "Read →", focus rings |
|
||||
| accent-hover | `#00D4FF` | Hover state |
|
||||
|
||||
### Status Colors
|
||||
| Token | Hex | Usage |
|
||||
|---|---|---|
|
||||
| violet | `#7C3AED` | Featured / hero badges, secondary actions |
|
||||
| coral | `#D64545` | Trending, breaking news, errors |
|
||||
| amber | `#FFB300` | New indicator, alerts, warnings |
|
||||
| green | `#00D166` | Published status, success |
|
||||
|
||||
### Backgrounds
|
||||
| Token | Hex | Usage |
|
||||
|---|---|---|
|
||||
| bg | `#0D0D14` | Main page background |
|
||||
| bg-elevated | `#171A21` | Navbar, elevated surfaces |
|
||||
| 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 |
|
||||
|
||||
**Rules:**
|
||||
- Category names are always `text-muted` — never color-coded
|
||||
- No gradients on UI elements — flat colors only
|
||||
- Secondary accent colors are used sparingly, only for semantic meaning (trending, featured, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
**Font stack:**
|
||||
- English: **Inter**
|
||||
- Japanese: **Noto Sans JP**
|
||||
|
||||
Load both via `next/font`. Use Noto Sans JP as a fallback for any Japanese text in article titles or tags.
|
||||
|
||||
### Scale
|
||||
| Role | Size | Weight |
|
||||
|---|---|---|
|
||||
| Hero headline | 48–56px | 700 |
|
||||
| Article title | 34–40px | 700 |
|
||||
| Section title | 22–24px | 700 |
|
||||
| Card title | 13–15px | 600 |
|
||||
| Primary body | 16px | 400 |
|
||||
| Compact body (excerpts) | 13px | 400 |
|
||||
| Metadata / labels | 10–12px | 500–600 |
|
||||
|
||||
---
|
||||
|
||||
## Spacing
|
||||
|
||||
Base unit: **8px**. Only use these values — no arbitrary spacing.
|
||||
|
||||
`4 · 8 · 12 · 16 · 24 · 32 · 40 · 48 · 64 · 80 · 96`
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
| Breakpoint | Grid | Max content width |
|
||||
|---|---|---|
|
||||
| Desktop | 12-column | 1200px |
|
||||
| Tablet | 8-column | — |
|
||||
| Mobile | Single column, content-first | — |
|
||||
|
||||
Article body width: **760–780px** (centered).
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
| Token | Value | Usage |
|
||||
|---|---|---|
|
||||
| radius-sm | 8px | Inputs, small chips |
|
||||
| radius-md | 12px | Cards |
|
||||
| radius-lg | 16px | Large cards, hero panels |
|
||||
| radius-xl | 20px | Modals |
|
||||
| radius-2xl | 28px | Full-page panels |
|
||||
|
||||
---
|
||||
|
||||
## Shadows
|
||||
|
||||
Shadows must be soft and diffused. No hard black shadows.
|
||||
|
||||
| Token | Usage |
|
||||
|---|---|
|
||||
| shadow-sm | Subtle card lift |
|
||||
| shadow-md | Hover elevation |
|
||||
| shadow-lg | Modals, overlays |
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Duration | 150ms – 250ms |
|
||||
| Easing | `cubic-bezier(0.22, 1, 0.36, 1)` |
|
||||
|
||||
**Rules:**
|
||||
- Motion communicates hierarchy and guides focus — never decorates
|
||||
- No bouncing, no exaggerated scaling, no neon pulse effects
|
||||
- Always respect `prefers-reduced-motion`
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
- **Primary:** solid `#00B4D8` background, black text
|
||||
- **Secondary:** transparent background, `#00B4D8` border and text
|
||||
- Never: pill buttons, excessive gradients, loud CTA colors
|
||||
|
||||
### Cards
|
||||
- Background: `bg-card`
|
||||
- Border: 1px solid `border`
|
||||
- Radius: `radius-md` (12px)
|
||||
- Hover: border lightens slightly (`#3a4560`), no scale transform
|
||||
|
||||
### Badges
|
||||
- Use flat solid backgrounds, bold uppercase text, tight padding
|
||||
- Text on dark badges: white. Text on light badges (amber, green): black
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
Style: **outline, geometric, lightweight**
|
||||
|
||||
Recommended library: **Lucide React**
|
||||
|
||||
---
|
||||
|
||||
## Article Pages
|
||||
|
||||
Article pages are the most important pages. They must feel:
|
||||
- Quiet and focused
|
||||
- Distraction-free
|
||||
- Premium and readable
|
||||
|
||||
Avoid: intrusive widgets, autoplay media, excessive sidebar clutter.
|
||||
|
||||
---
|
||||
|
||||
## Editorial Tone
|
||||
|
||||
Articles should be:
|
||||
- Concise and informative
|
||||
- Culturally aware and contextual
|
||||
- Written in a calm, analytical voice — modern magazine journalism
|
||||
|
||||
**Good headlines:** direct, informative, contextual
|
||||
**Bad headlines:** "YOU WON'T BELIEVE", "THIS CHANGES EVERYTHING", fake urgency
|
||||
|
||||
Never: clickbait, ragebait, excessive hype.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
Mandatory requirements:
|
||||
- WCAG AA contrast minimum on all text
|
||||
- Keyboard navigation throughout
|
||||
- Visible focus states (use `ring-accent` / `#00B4D8`)
|
||||
- Semantic HTML with proper heading hierarchy (one `<h1>` per page)
|
||||
- `alt` text on all images
|
||||
- `prefers-reduced-motion` support
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
- SSR / ISR — no client-heavy rendering for core content
|
||||
- Lazy-load all images below the fold
|
||||
- `next/image` for all images (automatic WebP + sizing)
|
||||
- Minimal layout shift (CLS < 0.1)
|
||||
- Fonts loaded via `next/font` (no FOUT)
|
||||
|
||||
---
|
||||
|
||||
## SEO
|
||||
|
||||
Priority order: **readers → archives → search engines**
|
||||
|
||||
SEO must never dictate editorial tone. Every article and category page gets:
|
||||
- `<title>` — `seo_title` if set, otherwise article title + " — Kotobane"
|
||||
- `<meta description>` — `seo_description` if set, otherwise excerpt
|
||||
- `<link rel="canonical">`
|
||||
- Open Graph tags for social sharing
|
||||
|
||||
---
|
||||
|
||||
## AI Agent Rules
|
||||
|
||||
When modifying any UI in this project:
|
||||
|
||||
**Must:**
|
||||
- Maintain spacing system (8px grid, no arbitrary values)
|
||||
- Preserve typography hierarchy
|
||||
- Use flat colors only — no gradients on UI elements
|
||||
- Use accent colors sparingly and only for their defined semantic purpose
|
||||
- Keep category labels in `text-muted` — never color-code them
|
||||
|
||||
**Must not:**
|
||||
- Inject gradients onto UI elements
|
||||
- Overcrowd layouts
|
||||
- Add clickbait-style visual treatments
|
||||
- Break the spacing system
|
||||
- Add unnecessary UI chrome or decorations
|
||||
- Add per-category color coding
|
||||
|
||||
---
|
||||
|
||||
## Logo
|
||||
|
||||
The 言羽 kanji mark serves as the primary logo in all contexts.
|
||||
|
||||
Characteristics:
|
||||
- Rendered in `accent` color (`#00B4D8`) on dark backgrounds
|
||||
- High letter-spacing in the wordmark (tracking: 3px+)
|
||||
- Works at small sizes — suitable as favicon treatment
|
||||
- No gradients on the logo
|
||||
|
||||
---
|
||||
|
||||
*Design reference with visual mockups: `docs/design-reference/kotobane-reference.html`*
|
||||
*Implementation spec: `docs/superpowers/specs/2026-05-28-kotobane-cms-website-design.md`*
|
||||
Reference in New Issue
Block a user