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)npm run test:coverage- Build a coverage report for tests through the site
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.tshandles MDX parsing with gray-matter for frontmatter - URL Structure: Posts follow
/:year/:month/:day/:slug/pattern with trailing slashes - Draft System: Posts with
draft: truefrontmatter 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:
SearchInputandSearchResultswith 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 (
.darkon<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: falseFor 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/esmfor 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.tscontains metadata, author info, analytics - Content processing:
src/lib/content.tshandles MDX parsing and post management - Route layouts: App Router layouts in
src/app/layout.tsxwith font loading - Static export:
next.config.tsconfigures 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
localStorageunder keytheme(light|dark). - No flash on load: An inline script in
src/app/layout.tsxapplies.darkto<html>before React hydrates, based onlocalStorageor system preference fallback. - Tailwind v4: Dark mode is class-based via CSS.
:rootdefines light tokens and.darkoverrides 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.tsxand delete the.darkoverrides 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.tsand 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].tscreate-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/tagsunless 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>.bakbefore 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