This is website to publishe a static site for Optional Rule Games using Next.js.
The implementation builds a statis site that is served from GitHub Pages.
This is a Next.js 15 static blog site for Optional Rule Games that deploys to GitHub Pages. The project uses App Router with TypeScript and exports as a static site. Content is managed through MDX files with gray-matter frontmatter processing.
npm run dev
- Start development server with hot reloading (includes draft posts)npm run build
- Build static site for production (excludes draft posts)npm run validate-config
- Validate configuration (URL/CNAME/robots/sitemap/assets/GA)npm run start
- Start production servernpm run lint
- Run ESLint for code quality checksnpm run generate-search-index
- Generate search index from all postsnpm run find-empty-links
- Scan content for broken markdown linksnpm run create-post
- Create new blog post with frontmatter template
npm run test
- Run all unit testsnpm run test:watch
- Run tests in watch mode during developmentnpm run test:ui
- Run tests with browser-based UInpm run test:build
- Build verification tests (includes build + static tests)
See Testing Strategy Documentation for comprehensive testing approach and upgrade protection details.
The site uses Next.js static export (output: 'export'
) configured in next.config.ts
. All routes are pre-rendered at build time for GitHub Pages deployment. The build process runs through GitHub Actions using .github/workflows/deploy.yml
.
- Content Location: MDX files stored in
content/posts/
andcontent/pages/
- Processing Pipeline:
src/lib/content.ts
handles MDX parsing with gray-matter for frontmatter - URL Structure: Posts follow
/:year/:month/:day/:slug/
pattern with trailing slashes - Draft System: Posts with
draft: true
frontmatter are excluded from production builds
The App Router uses route groups for organization:
(content)
group: Blog posts, pagination, tags, search(pages)
group: Static pages like About- Dynamic routes:
[year]/[month]/[day]/[slug]
for posts,[tag]
for tag pages
- Client-side search using Fuse.js for fuzzy matching
- Search index generation creates JSON index from all posts during build
- Components:
SearchInput
andSearchResults
with debounced typing - URL integration: Search queries reflected in URL parameters
- TailwindCSS v4 with inline configuration in
globals.css
- Geist fonts (sans and mono) loaded via
next/font/google
- Theme toggle using class-based dark mode (
.dark
on<html>
) with persistence
slug: post-slug
title: Post Title
date: 'YYYY-MM-DD'
excerpt: Brief description
tags: [tag1, tag2]
featured_image: /images/image.jpg
draft: false
showToc: false
For static images rendered in MDX, Next.js's <Image>
component requires explicit dimensions. Add imageWidth
and imageHeight
fields to your frontmatter and use them when referencing the image:
imageWidth: 800
imageHeight: 600
<img src="/images/example.png" alt="Example" width={frontmatter.imageWidth} height={frontmatter.imageHeight} />
- Reading time calculation automatically added to all posts
- Heading extraction for table of contents generation
- Excerpt generation from content if not provided in frontmatter
- Tag normalization and slug generation for tag pages
- Path aliases:
@/*
maps to./src/*
for clean imports - ESM scripts: Scripts use
node --import tsx/esm
for TypeScript execution - Type definitions: Comprehensive types in
src/lib/types.ts
The search system uses strictly typed interfaces for SearchIndexItem
, SearchResult
, and SearchOptions
with runtime validation during index loading.
All scripts use ESM loader pattern and are located in scripts/
directory:
- Search index generation: Processes all MDX files into searchable JSON
- Empty link detection: Scans for broken markdown link patterns
- Post creation: Interactive post scaffold with proper frontmatter
- Generate search index from all posts
- Next.js static build with route pre-rendering
- Export to
out/
directory for GitHub Pages - Automatic deployment via GitHub Actions
- Site config:
src/config/site.ts
contains metadata, author info, analytics - Content processing:
src/lib/content.ts
handles MDX parsing and post management - Route layouts: App Router layouts in
src/app/layout.tsx
with font loading - Static export:
next.config.ts
configures GitHub Pages deployment
- Content outside src/: MDX files in
content/
directory separate from Next.js source - Trailing slashes required: All routes end with
/
for GitHub Pages compatibility - Static-only: No server-side rendering or API routes in production
- Image optimization enabled: Uses Next.js
<Image>
with static export; images require width and height in frontmatter - Search runs client-side: No server dependency for search functionality
The application emits a strict Content Security Policy (CSP) using Next.js middleware. A unique nonce is generated for every request and applied to inline <script>
tags, allowing them to execute while blocking injected scripts. The middleware sets the Content-Security-Policy
header with:
script-src 'self' 'nonce-<random>' https://www.googletagmanager.com https://platform.twitter.com https://s.imgur.com
This restricts script execution to the site itself and the trusted domains above. Server components can read the nonce via headers().get('x-nonce')
and pass it to <script>
or next/script
elements.
- Behavior: A toggle in the header switches between Light/Dark and persists in
localStorage
under keytheme
(light
|dark
). - No flash on load: An inline script in
src/app/layout.tsx
applies.dark
to<html>
before React hydrates, based onlocalStorage
or system preference fallback. - Tailwind v4: Dark mode is class-based via CSS.
:root
defines light tokens and.dark
overrides tokens and setscolor-scheme: dark
. - Reset preference: Clear local storage key
theme
(DevTools > Application > Local Storage) or runlocalStorage.removeItem('theme')
in console, then reload. - Revert to system preference: Remove the inline script in
src/app/layout.tsx
and delete the.dark
overrides insrc/app/globals.css
, then rely on the existing@media (prefers-color-scheme: dark)
approach. - Default theme at loadtime can be set via
/src/config/site.ts
and theme variables changed as needed.
Direct script execution (not via npm commands) for maintenance and content management tasks.
Run TypeScript scripts using the ESM loader:
node --import tsx/esm scripts/[script-name].ts
create-post.ts
- Interactive blog post creation
Creates new MDX post files with proper frontmatter and filename structure.
node --import tsx/esm scripts/create-post.ts
- Interactive prompts for title, tags, and featured image
- Auto-generates slug and filename from title
- Creates file in
content/posts/
with today's date
find-empty-links.ts
- Scan for broken markdown links
Scans all MDX files for empty or malformed markdown links.
node --import tsx/esm scripts/find-empty-links.ts
- Detects patterns like
[text]()
,[text]("")
,[]()
- Reports file, line number, and problematic content
- No command-line flags
generate-search-index.ts
- Build search index
Processes all blog posts into a searchable JSON index for client-side search.
node --import tsx/esm scripts/generate-search-index.ts
- Reads all MDX files from
content/posts/
- Extracts title, excerpt, tags, and content
- Outputs to
public/search-index.json
generate-rss.ts
- Generate RSS feed
Creates RSS/Atom feed from blog posts.
node --import tsx/esm scripts/generate-rss.ts
- Processes recent posts for RSS feed
- Outputs feed files to
public/
- Uses site config for feed metadata
download-external-images.ts
- Cache external images locally
Downloads external images referenced in posts and updates MDX files.
node --import tsx/esm scripts/download-external-images.ts
- Scans MDX files for external image URLs
- Downloads to
public/images/cache/
- Updates MDX files with local paths
replace-default-images.ts
- Update default featured images
Replaces old/default featured images with random selections from current image set.
node --import tsx/esm scripts/replace-default-images.ts
- Targets specific old image paths
- Randomly assigns from curated replacement set
- Updates frontmatter in MDX files
enhance-post.mjs
— AI-powered post enhancement
Generates a concise excerpt and curated tags for MDX posts using OpenAI. Requires OPENAI_API_KEY
and OPENAI_MODEL
in .env
. Uses the OpenAI Responses API with an automatic fallback to Chat Completions.
Usage (via npm alias):
npm run enhance-post -- <path> [options]
Direct execution:
node scripts/enhance-post.mjs <path> [options]
Behavior:
- Requires a path (file or directory); errors if omitted.
- Skips posts with
draft: true
. - Does not overwrite existing
excerpt
/tags
unless overwrite flags are provided. - Tags are restricted to an internal whitelist (1–4 max, exact match).
- Excerpt is post-processed to one sentence (100–160 chars), plain text, no quotes/backticks/ellipsis, and avoids repeating the title.
Options:
--dry-run
: Preview changes without writing files--overwrite-excerpt
: Overwrite existing excerpt--overwrite-tags
: Overwrite existing tags--overwrite-all
: Overwrite both excerpt and tags--backup
: Create a<file>.bak
before writing--model <name>
: Override model from.env
Examples:
# Single file dry run
npm run enhance-post -- content/posts/2025-09-12-kcd2-alchemy-tool-and-nostalgia-for-ttrpg-crafting.mdx --dry-run
# Enhance all posts in a directory, overwrite only excerpt
npm run enhance-post -- content/posts --overwrite-excerpt
# Overwrite both excerpt and tags, create backups
npm run enhance-post -- content/posts --overwrite-all --backup