Static site for Tech Tavern, LLC built with Next.js (App Router) and MDX, deployed to GitHub Pages as a static export.
- Node.js 20.x (recommended) or >= 18.17
- npm 9+
Choose the command set that matches your environment.
- macOS/Linux/Windows:
npm install npm run dev
- Windows with WSL: use the
win-npmwrapper so the dev server runs on the Windows host for reliable file watching. See Windows + WSL section below, then:win-npm install win-npm run dev
Build a static export (outputs to out/):
npm run build # or: win-npm run build (WSL)src/app/: App Router pages, layouts, metadata, global CSSsrc/components/: Reusable UI and section componentssrc/lib/: Utilities (e.g.,env.ts,posts.ts,site(.server).ts)content/articles/: MDX posts (YYYY-MM-DD-slug.mdx)public/: Static assets, fonts, images.github/workflows/: CI for lint/typecheck/build/deploy
dev: Next.js dev server (uses Turbopack)dev:webpack: Next.js dev server with webpack (fallback option)build: Next.js static export build →out/(uses Turbopack)start: Next.js start (rarely needed; static export is default)lint: ESLint (Next + TS rules)typecheck: TypeScript--noEmitnew-article: Scaffold a new MDX articledev:watch: Dev server + MDX change notifications (optional)
Recommended:
npm run dev # or: win-npm run dev (WSL)Fallback (if issues occur):
npm run dev:webpack # or: win-npm run dev:webpack (WSL)npm run build # or: win-npm run build (WSL)Status: ✅ Resolved in Next.js 16 with string-based plugin configuration
Solution:
The project now uses string-based plugin names in next.config.mjs, which are serializable and work with Turbopack:
remarkPlugins: ['remark-gfm'],
rehypePlugins: ['rehype-slug', ['rehype-autolink-headings', { behavior: 'wrap' }]]This is the official recommended method in Next.js 16 documentation. Plugin options (like { behavior: "wrap" }) are still supported via array syntax.
Historical Context:
Previously, Turbopack had serialization issues when MDX plugins were imported as JavaScript functions. The string-based approach resolves this while maintaining full plugin functionality. If any issues occur, the dev:webpack script remains available as a fallback.
- Articles live in
content/articles/as MDX. Filenames:YYYY-MM-DD-slug.mdxwith frontmatter. - Frontmatter (required):
title,date(yyyy-mm-dd),slug - Frontmatter (optional):
lastModified(yyyy-mm-dd),excerpt,tags,featuredImage,ogTitle,ogDescription,ogImage,canonicalUrl,draft - Add a new post:
npm run new-article(orwin-npm run new-articleon WSL) - MDX is compiled during dev/build automatically; no import map to maintain.
Reading time is computed automatically (~200 wpm) and shown on index and article pages.
- Links: internal links use
next/link; external links open in a new tab withrel="nofollow noopener noreferrer external". - Images:
- Preferred:
<Image src="/path.jpg" width={1200} height={630} alt="..." /> - Markdown images
also work (falls back to<img>when needed)
- Preferred:
- Articles index:
/articles/ - Article URLs:
/articles/YYYY/MM/DD/slug/
The site generates absolute links (sitemap, RSS, OpenGraph) using SITE_URL. Variables are validated with Zod in src/lib/env.ts.
- SITE_URL: The public origin of the site (no trailing slash). Examples:
- GitHub Pages default (staging in this repo):
https://<owner>.github.io/techtavern-nextjs.github.io - Custom domain:
https://example.com
- GitHub Pages default (staging in this repo):
- NEXT_PUBLIC_BASE_PATH: Set by CI for staging vs production; rarely needed locally.
- NEXT_PUBLIC_GA_ID: Optional Google Analytics measurement ID.
- NEXT_PUBLIC_HUBSPOT_PORTAL_ID: Optional HubSpot tracking portal ID (numeric only; do not include the trailing
.js). Set this in repo Variables or Secrets so CI can bake it into the static export. - CSP is centralized in
src/lib/csp.ts; update the allowlists there if you introduce additional third-party scripts or APIs.
Validation and defaults:
SITE_URLmust be a valid URL if provided; missing locally defaults tohttp://localhost:3000for previewing sitemap/RSS.NEXT_PUBLIC_BASE_PATHdefaults to empty and helpers normalize trailing slashes.
Quick example: computing absolute URLs
// src/anywhere.ts
import { getBaseUrl } from '@/lib/site.server';
// Preferred: let URL handle slashes
const absolute = new URL('/articles/2025/08/24/hello-world/', getBaseUrl()).toString();
// Or simple string concat (getBaseUrl() has no trailing slash)
const absolute2 = `${getBaseUrl()}/articles/2025/08/24/hello-world/`;Where to set SITE_URL:
- Local development: optional
.env.local(e.g.,SITE_URL=http://localhost:3000). - GitHub Actions: set automatically by the workflow; override via an Actions Variable or Secret named
SITE_URL.
Sitemap: /sitemap.xml • RSS: /rss.xml.
- CI builds on pushes/PRs; deployment runs on
main. - The workflow sets
NEXT_PUBLIC_BASE_PATHfor staging (subdirectory) vs production (root) and derivesSITE_URLautomatically. You can overrideSITE_URLvia an Actions Variable/Secret. - Static export artifacts are in
out/and are uploaded to GitHub Pages by the workflow.
- CSP meta tag lives in
src/app/layout.tsx. - Dev builds add
unsafe-evalto simplify tooling; production builds do not. - Zod is hard‑blocked from client bundles (
next.config.tssetsresolve.alias.zod = false). - If you embed remote images, extend
img-srcto includehttps:or add specific domains.
Server-only helpers that read validated env live in src/lib/site.server.ts and should be imported from server components/routes.
- Unit tests: Jest + React Testing Library (
npm run test) - Type safety:
npm run typecheck - Linting:
npm run lint