Files
Techno Tim 794bfdb73d feat(nextjs): migrate application to Next.js (#839)
* test(migration): capture Razzle compatibility baseline

- Add parity test suite with 49 tests covering theme, shadows, button target, email links, Mastodon rel, share visibility, analytics inclusion, custom buttons, button order, README example, injection regression, and Dark theme exact-match semantics
- Add production HTTP smoke test script for container-level validation
- All 8 existing suites and 14 tests remain unchanged and green

* chore(nextjs): add Next.js 16 App Router foundation

- Install Next.js 16.2.10, React 19.2.7, React DOM 19.2.7, server-only
- Add dev dependencies: eslint@8.57.0, eslint-config-next@14.2.30, @testing-library/react, @testing-library/dom, @testing-library/jest-dom
- Upgrade react-test-renderer to 19.2.7 for React 19 compatibility
- Add next.config.js with poweredByHeader: false and reactStrictMode: true
- Add jsconfig.json with @/* path alias
- Create minimal App Router layout and healthcheck route
- Update parity and Sort tests to use @testing-library/react instead of deprecated react-test-renderer
- Add .next to gitignore
- 9 suites, 49 tests, 5 snapshots all green

* refactor(config): add request-time environment reader

- Add src/config/envNames.js with canonical allowlist of 165 supported keys
- Add src/config/runtimeConfig.js with getRuntimeConfig(), getTheme(), and shouldSkipHealthLog() helpers
- Import server-only to prevent client bundling
- Return a new plain object on every call (no module-level caching)
- Keep raw values as strings without type coercion
- Add comprehensive unit tests: key reading, missing keys, special characters, mutation isolation, theme logic, health log suppression, and allowlist invariants
- 10 suites, 70 tests, 5 snapshots all green

* refactor(assets): serve brand icons through Next.js public assets

- Copy all SVG icons from src/icons to public/icons
- Replace all 112 SVG imports in Home.js with stable /icons/ URLs
- Update Sort test to use URL paths instead of SVG imports
- All 10 suites, 70 tests, 5 snapshots green
- CSS order and desktop/mobile layout unchanged

* feat(nextjs): port runtime document metadata

- Replace static metadata object with async generateMetadata()
- Add force-dynamic to prevent build-time capture of config values
- Read LANG, THEME, FAVICON_URL and stylesheet config at request time
- Render stylesheets in original order (fonts, normalize, theme, os, littlelink, brands)
- Render og:type, author, keywords, and X-UA-Compatible meta tags in <head>
- Set html class to dark/light based on THEME=Dark exact match
- Add layout unit tests covering config-driven rendering and injection safety
- Add generateMetadata unit tests for all metadata fields with edge cases

* feat(nextjs): port link page to Server Components

- Convert Home, Button, Avatar, Sort to Server Components
- Convert Share to Client Component with use client directive
- Create AnalyticsTracker Client Component with event delegation
- Replace onClick handlers with data-analytics-event attribute
- Accept config values as props instead of importing runtimeConfig
- Pass dropShadow and buttonTarget from page to all buttons/avatar
- Remove memo, prop-types, react-router-dom dependencies from components
- Add try/catch for navigator.share rejection
- Update app/page.js to render Home with runtime config and AnalyticsTracker

* feat(analytics): port runtime scripts and click tracking

- Create AnalyticsScripts Server Component using next/script
- Implement safeJsEncode() for inline script value encoding
- Support Google Analytics, Umami, Matomo, and Plausible providers
- Preserve required value checks for each provider
- Preserve Umami default script filename and custom override
- Preserve Matomo image fallback with referrer policy
- Add comprehensive unit tests for safeJsEncode encoding
- Add unit tests for provider conditional rendering logic

* feat(server): run Next.js with compatible container middleware

- Create root server.js with Next.js custom server
- Use Express for production Morgan combined logging and compression
- Honor SKIP_HEALTH_CHECK_LOGS via shouldSkipHealthLog helper
- Listen on 0.0.0.0 with configurable PORT (default 3000)
- Disable x-powered-by header via Express and Next.js config
- Set compress: false in next.config.js to avoid double compression
- Forward all requests to Next.js request handler after middleware

* test(nextjs): migrate component tests to Testing Library

- Replace react-test-renderer with @testing-library/react in all tests
- Remove 5 obsolete snapshot files no longer used
- Remove setupTests.js (React Router warning suppression)
- Add comprehensive Button tests: rendering, styles, target, rel, analytics, dropShadow
- Add Avatar tests: src, srcSet, size, dropShadow
- Add Share tests: navigator.share availability and rejection handling
- Simplify Home and App tests to verify rendering without snapshot

* feat(container): build Next.js runtime image

- Replace Razzle build stage with Next.js build stage
- Remove patches directory from build context
- Copy app, next.config.js, jsconfig.json, and server.js into build stage
- Copy .next, public, server.js, next.config.js, package.json, and node_modules into runtime stage
- Add HOSTNAME=0.0.0.0 and PORT=3000 environment variables
- Change CMD from build/server.js to server.js (custom Next.js server)

* ci(nextjs): validate production container compatibility

- Run yarn build in main workflow CI after lint and test
- Run yarn build in PR workflow CI after lint and test
- Add Docker smoke test to PR workflow with runtime environment check
- Add GitHub Actions ecosystem to Dependabot updates
- Change npm Dependabot schedule from daily to weekly

* chore(razzle): remove Razzle build system and react-router

- Delete all Razzle-related source files (src/index.js, src/server.js, src/client.js, src/config.js)
- Delete App component with react-router Routes/Route pattern
- Create FontAwesomeInit client component for icon library initialization
- Update layout.js to wrap children with FontAwesomeInit
- Remove Razzle dependencies from package.json (razzle, webpack, react-router-dom, babel-preset-razzle, etc.)
- Update test scripts to use next/jest instead of razzle test
- Fix Google and Umami analytics tests for Jest 30 compatibility
- Fix parity test import for new runtimeConfig module
- Update .eslintrc to use next/core-web-vitals config
- Update .env.example to remove RAZZLE_ prefix
- Update yarn.lock after dependency changes

* docs(nextjs): update documentation for Next.js migration

- Update README with migration note, developer setup commands, and link to docs/
- Update CONTRIBUTION.md with Node 24 requirement and corrected file paths
- Update docs/healthcheck.md to remove express-specific language
- Remove duplicate analytics and healthcheck sections from README
- Keep README Docker Compose and docker run examples unchanged

* test(e2e): add Playwright e2e tests with full-env and minimal-env scenarios

- Add playwright.config.cjs with chromium and minimal-env projects
- Add e2e/full-env.spec.js: 14 tests for full docker-compose example env
- Add e2e/minimal-env.spec.js: 6 tests for zero-env-var scenario
- Add e2e/minimal-env.sh: script to run minimal-env tests via Docker
- Add test-results to .gitignore
- Add test:e2e and test:e2e:minimal npm scripts
- Fix #undefined rendering bug in Home.js (template literals with undefined)
- Fix duplicate meta keywords/author in layout.js
- Fix Dockerfile missing src/ copy (runtime MODULE_NOT_FOUND)
- Remove babel packages from package.json

* fix(parity-test): use empty env object to avoid next/jest .env loading

next/jest automatically loads .env files, which now contain unprefixed
env vars picked up by getRuntimeConfig. The old getCleanRuntimeConfig
only removed LANG/THEME, not the full set of .env vars.
Passing {} ensures a true clean baseline.

* refactor(tests): co-locate all tests under src/__tests__/

- Move analytics tests (google, umami, AnalyticsScripts) to src/__tests__/analytics/
- Move component tests (Avatar, Button, Home, Share, Sort) to src/__tests__/components/
- Move runtimeConfig.test.js to src/__tests__/config/
- Update all relative import paths in moved test files
- Add .prettierrc config matching eslint prettier rule (singleQuote: true)
- Fix public/css/brands.css: change button display from inline-block to block
  to prevent side-by-side layout on wide screens

* chore(deps): remove unused helmet and server-only packages

- helmet was a leftover from Razzle era, never imported in the codebase
- server-only cannot be used with CommonJS require() from the custom server
- Both removed from dependencies and yarn.lock updated

* chore(typescript): add strict TypeScript toolchain and ESLint flat config

- Add typescript, @types/node, @types/react, @types/react-dom as dev deps
- Upgrade eslint-config-next from 14.2.30 to 16.2.10
- Upgrade eslint from 8.57.0 to ^9.39.5
- Remove deprecated eslint plugins (eslint-config-prettier, eslint-plugin-import, eslint-plugin-jsx, eslint-plugin-node, eslint-plugin-promise, eslint-plugin-react)
- Create tsconfig.json based on Next.js 16.2.10 template with allowJs: true
- Create eslint.config.mjs with flat config, core-web-vitals and typescript presets
- Delete jsconfig.json, .eslintrc, .eslintignore
- Add typegen and typecheck scripts
- Add *.tsbuildinfo to .gitignore
- Fix prettier formatting in playwright.config.cjs

* refactor(config): type runtime environment configuration

- Rename envNames.js to envNames.ts with as const readonly tuple
- Rename runtimeConfig.js to runtimeConfig.ts with typed RuntimeConfig
- Add RuntimeConfig, Theme, DropShadow, and EnvName types
- Convert CommonJS exports to named ES module exports
- Update runtimeConfig test to .ts with proper type assertions
- Add @types/jest and jest to tsconfig types
- Delete unused src/utils.js
- Add moduleFileExtensions with ts/tsx to jest config

* refactor(components): type shared UI components

- Convert Avatar.js to Avatar.tsx with AvatarProps interface
- Convert Button.js to Button.tsx with ButtonProps interface
- Convert Share.js to Share.tsx with ShareProps interface
- Convert FontAwesomeInit.js to FontAwesomeInit.tsx with typed children
- Convert corresponding tests to .tsx with proper type handling
- Add moduleNameMapper rule for .js->.ts resolution in jest config

* refactor(analytics): type scripts and browser tracking

- Add src/types/global.d.ts for window.gtag, umami, and _paq
- Convert google.js, umami.js, matomo.js to .ts with typed functions
- Convert AnalyticsScripts.js to .tsx with typed RuntimeConfig
- Convert AnalyticsTracker.js to .tsx with AnalyticsTrackerProps
- Convert analytics tests to .ts/.tsx
- Use optional chaining for window analytics globals

* refactor(home): type button rendering and ordering

- Convert Sort.js to Sort.tsx with typed SortableElement interface
- Convert Home.js to Home.tsx with HomeProps using RuntimeConfig
- Convert Sort and Home tests to .tsx
- Cast dropShadow from config as DropShadow type
- Use data-order instead of non-standard order on div elements

* feat(app-router): type App Router entry points with TypeScript

- Rename app/layout.js -> app/layout.tsx, app/page.js -> app/page.tsx, app/healthcheck/route.js -> app/healthcheck/route.ts
- Add generateMetadata return type as Promise<Metadata> in layout.tsx with typed RuntimeConfig
- Rename test files to .tsx extension: layout.test.tsx, parity.test.tsx
- Fix prettier formatting and lint issues in layout.tsx and layout.test.tsx
- Add non-null assertions for openGraph, twitter, and other metadata properties in tests
- Fix Twitter card type assertion in test to handle discriminated union narrowing
- Suppress @typescript-eslint/no-require-imports for dynamic export test

* test(typescript): type unit and end-to-end tests

- Convert Font Awesome mock to .tsx with typed return type React.ReactElement
- Convert full-env.spec.js to .spec.ts with typed ENV fixture via Record<string, string>
- Convert minimal-env.spec.js to .spec.ts
- Convert playwright.config.cjs to playwright.config.ts with ES module syntax
- Update minimal-env.sh to reference playwright.config.ts and use yarn playwright
- Update testMatch patterns in playwright config to .spec.ts
- Remove old .js and .cjs test/configuration files

* refactor(server): run typed custom server in Docker

- Convert server.js to server.ts with typed Express Request parameter for Morgan skip
- Add @types/express, @types/morgan, @types/compression dev dependencies
- Add tsx dev dependency for development server execution
- Create tsconfig.server.json for server-side compilation to dist-server/
- Add dist-server/ to .gitignore and eslint.config.mjs ignores
- Update package.json scripts: dev=tsx server.ts, build=next build && tsc -p tsconfig.server.json, start=NODE_ENV=production node dist-server/server.js
- Update Dockerfile: copy tsconfig.json, tsconfig.server.json, next-env.d.ts, server.ts to build stage; copy dist-server/ to runtime; CMD uses dist-server/server.js
- Update playwright.config.ts webServer command to use dist-server/server.js
- Add tsconfig.server.json for CommonJS compilation of server.ts and its runtime dependencies
- Remove server.js, jsconfig.json reference from Dockerfile

* ci(typescript): enforce types and end-to-end tests

- Rename js-install-and-test job to install-test-build in both PR and main workflows
- Split monolithic install+test+lint+typecheck+build step into separate named steps
- Add explicit yarn typecheck step after lint and before build
- Update PR template checklist to include yarn typecheck

* docs(typescript): document typed development workflow

- Add yarn typecheck and file extension conventions to README.md
- Add VS Code workspace TypeScript version instructions to README.md and CONTRIBUTION.md
- Document runtime configuration behavior (per-request env reading, no NEXT_PUBLIC_ prefix)
- Update CONTRIBUTION.md with typecheck, lint, and full validation commands
- Update file references in CONTRIBUTION.md from .js to .ts/.tsx extensions

* chore(typescript): tighten config with allowJs false

- Set allowJs to false now that all application code uses .ts/.tsx
- Remove .js, .jsx, .mjs, .cjs from include globs
- jest.config.js and next.config.js retained as documented tool config exceptions

* chore(config): convert next.config.js to next.config.ts

- Replace CommonJS next.config.js with ES module next.config.ts using NextConfig type
- Update Dockerfile to copy next.config.ts instead of next.config.js
- jest.config.js remains as the only .js file (documented tool config exception)

* ci(e2e): add Playwright e2e jobs to CI workflows

- Add full-env-e2e job to PR workflow (needs install-test-build, runs yarn test:e2e with Playwright chromium)
- Add full-env-e2e and minimal-env-e2e jobs to release workflow
- Make release build-and-push-docker-image depend on both e2e jobs
- Split full-env (webServer with dev build) and minimal-env (Docker container with no env vars)
- Use trap for Docker cleanup in minimal-env job
- Install Playwright browsers with --with-deps chromium in each e2e job
- Update PR template checklist to include yarn test:e2e

* ci(e2e): pin Playwright container to v1.61.1 with digest

- Switch full-env-e2e jobs to use mcr.microsoft.com/playwright:v1.61.1 pinned to sha256:5b8f294aff9041b7191c34a4bab3ac270157a28774d4b0660e9743297b697e48
- Remove playwright install step from full-env-e2e jobs (browsers preinstalled in container)
- Add playwright install step to minimal-env-e2e (needs host-based browsers)
- Pin matches local @playwright/test version 1.61.1 from yarn.lock

* chore(types): update next-env.d.ts to production route types

- Import path changed from .next/dev/types/routes.d.ts to .next/types/routes.d.ts
- Auto-generated by next typegen after production build

* refactor(ci): DRY up workflows into reusable components

- Extract install-test-build.yml as reusable workflow with build-docker input
- Extract full-env-e2e.yml as reusable Playwright container workflow
- Extract minimal-env-e2e.yml as reusable Docker+minimal workflow
- Extract build-and-push.yml as reusable multi-arch publish workflow
- Remove inline job definitions from pull-request.yml and main.yml
- Both top-level workflows now call shared reusable workflows via uses:

* chore(docker): optimize Docker build and add .dockerignore

- Add .dockerignore to reduce build context from 822MB to 92KB
- Prune .next/ of build-only artifacts (cache, dev, turbopack, diagnostics, traces, source maps)
- Remove unnecessary runtime copies of src/, server.ts, next.config.ts, package.json
- Remove unused apk add curl from both stages (wget is built into Alpine)

* chore(ci): fix FUNDING.yml missing trailing newline

- Add missing newline at end of FUNDING.yml
- next-env.d.ts reverted to dev route types after running next dev

* chore(docker): use Next standalone runtime image

- Enable Next.js standalone output for traced production runtime files
- Build dependencies in a dedicated cached deps stage
- Copy only standalone server, static assets, and public files into the final image
- Pin the Node Alpine base image by digest
- Run the final container as the non-root node user

* chore(ci): include typecheck in ci script

- Add yarn typecheck to the package ci script
- Commit the next-env.d.ts route type path generated by next typegen

* chore(docker): tighten production runtime image

- Disable Next.js telemetry during Docker build and runtime
- Cache .next build artifacts with BuildKit to speed up rebuilds
- Prune unused glibc Sharp binaries from the Alpine standalone bundle
- Keep the final image on the pinned Node 24.18.0 Alpine base

* chore(lint): suppress intentional Next warnings

- Suppress public stylesheet link warnings in the runtime-configured layout
- Suppress plain img warnings for analytics pixels and runtime-configured image URLs
- Keep ESLint clean without changing rendered behavior

* ci(actions): tighten reusable workflows

- Restore pull request and release workflows to local reusable workflow calls
- Pin reusable jobs to ubuntu-24.04 and add job timeouts
- Scope full-env Playwright runs to the chromium project after building
- Reuse the minimal-env Docker e2e helper with configurable image names
- Add GitHub Actions cache backend to Docker publish builds

* docs(readme): refresh Next.js project overview

- Remove migration-specific README notes

- Describe the current Next.js and TypeScript application setup

- Clean up developer commands and configuration wording

* chore(cleanup): remove old build references

- Update comments to describe the current test behavior

- Remove the obsolete prefix-specific allowlist test

- Keep runtime configuration docs focused on documented environment names

* fix(icons): resolve custom button icon warnings

- Use Font Awesome 6 icon names in README and e2e examples

- Register Font Awesome packs during server rendering

- Remove missing-icon noise from full-env Playwright runs

* ci(actions): clarify reusable workflow checks

- Give caller jobs and reusable jobs distinct names

- Add js-install-and-test compatibility gate for required checks

- Keep the required check tied to the Node quality gate result

* ci(actions): remove legacy required-check shim

- Remove the compatibility js-install-and-test job

- Keep PR checks named for the actual quality and e2e suites

- Expect branch protection to require the new workflow check name
2026-07-19 13:26:37 -05:00

290 B

🩺 Health Check

A health check endpoint exists on /healthcheck. If healthy, it will return with a 200 and the following response:

{
  "status": "ok"
}

To skip logging these health check calls, add the environment variable:

SKIP_HEALTH_CHECK_LOGS=true