feat: Nuxt 4 rewrite#116
Conversation
Replaces the legacy Nuxt 2 app (nuxt/) with the Nuxt 4 rewrite: - Nuxt 4 app structure (app/, content/, composables/, data/) - @nuxt/ui v3 + Tailwind v4 design system (@stuartclark/ui) - Playwright visual regression (4 breakpoints) + design baselines - Vitest unit/component tests - Storybook 9 (component stories + design reference) - Content collections via @nuxt/content v3 - Project tooling: pnpm, eslint, stylelint, knip, commitlint, mise Preserves drupal/, .github/, and other shared project files. Non-destructive: main and develop branches are untouched.
Adds a preview:live manual CI job that spins up the site and Storybook behind Cloudflare Quick Tunnels (via the existing nuxt-cloudflared-tunnel module) and posts both tunnel URLs as an MR comment that auto-cleans on job exit. The before_script clones + builds @stuartclark/ui as a workspace sibling (link:../../ui pattern) using the cross-project CI job token, then installs nuxt deps. Scripts (adapted from realityloop/website and the ui repo): - run-live-preview-tunnel.sh: starts Storybook (:6006) then nuxt dev, captures both tunnel URLs, posts/cleans MR comment, heartbeat - post-live-preview-comment.sh / upsert-mr-note.sh / delete-mr-note.sh: GitLab MR note management via $GITLAB_API_TOKEN
Without workflow:rules the pipeline runs as a branch (push) pipeline, so $CI_MERGE_REQUEST_IID is never set and the preview comment is silently skipped.
Use bulleted list so links render on separate lines in GitLab markdown. Use 'Site' label instead of 'Playground' for the nuxt tunnel.
Mirrors @stuartclark/ui. Stages: lint (eslint, stylelint, markdownlint, cspell, knip, commitlint), linkcheck (lychee), test (vitest), typecheck, build (static generate), separate visual + seo Playwright jobs, unlighthouse audit, manual live preview and baseline regeneration. Shared .setup template clones+builds @stuartclark/ui as a sibling so the link:../../ui dependency resolves. Adds post-visual-failure-comment.sh to upload actual/expected/diff PNGs to the MR on visual failures.
Establishes the mise-idiomatic git-hook standard: .githooks/ scripts delegate to mise tasks (commit-msg -> commitlint, pre-commit -> eslint) and a hooks:install task sets core.hooksPath. Adds unlighthouse.config.ts, .lychee.toml, and the packageManager field.
…EADME Adds AGENTS.md, CONTRIBUTING.md, CHANGELOG.md at the submodule root. Replaces .markdownlint.yaml with .markdownlint.jsonc (matching @stuartclark/ui) and extends .cspell.json. README drops the stale 'v4 prototype'/'vendored module' framing and documents CI + mise hooks.
Replaces the broken Node 14/yarn/cypress/netlify workflow with a pnpm 10/node 22 pipeline mirroring .gitlab-ci.yml, cloning+building @stuartclark/ui as a sibling. Bumps CodeQL to v3, scoped to main/develop, ignoring the legacy drupal/ subtree.
Declares unlighthouse.config.ts as an entry, ignores the ImageMagick `compare` binary used by the design-diff review script, and drops two stale ignores (app.config.ts, storybook-static/**) that knip now flags.
Unreachable from the isolated GitLab runner (same class as the existing linkedin/fonts excludes); not a real dead link.
app.vue set og/twitter titles but never a plain <title>, so the generated site had an empty document title and the Playwright SEO suite failed. Adds a global title plus a template so subpages render as "Page · stuar.tc" while the homepage stays "Stuart Clark · stuar.tc".
Generated by the manual `visual:update` CI job (arm64 runner, matching the `visual` job environment) so Chromium rendering is consistent. Covers all 8 pages across the phone/tablet/desktop/wide breakpoints (32 PNGs). 2% diff tolerance (maxDiffPixelRatio).
linkcheck: switch to --format detailed piped via tee (matching
@stuartclark/ui) and add retry/max-time to the lychee download so a flaky
api.github.com call can't hang a now-blocking job. Remove allow_failure.
lint:seo: puppeteer's bundled Chrome is x86_64 and fails via Rosetta on the
arm64 runner ("failed to open elf at /lib64/ld-linux-x86-64.so.2"). Install
the native system chromium and point unlighthouse at it via
UNLIGHTHOUSE_CHROME. Remove allow_failure.
Convert visual:update and preview:live from job-level when/allow_failure to the rules form (matching @stuartclark/ui). This is the unambiguous way to declare a manual job that never blocks the MR merge.
The full-page visual suite flaked (~5-7% height diffs over the 2% threshold) because @nuxt/fonts fetches Archivo/JetBrains Mono at runtime and whether every woff2 subset has settled before the screenshot is non-deterministic — a single wrap-point shift at phone width cascades into ~24-32px of full-page height variance. Apply the proven @stuartclark/ui playground-visual technique: inject a freeze + font-normalisation stylesheet via page.addStyleTag AFTER hydration (unhead strips init-injected styles), overriding --font-sans /--font-mono to deterministic DejaVu OS fonts and killing animations. Lock the colour scheme to light in beforeEach. Baselines regenerated from CI (arm64 runner, matching the visual job).
The font-normalisation comment in tests/visual/home.spec.ts introduces terms cspell didn't know (unhead, networkidle, normalise/stabilise family). Add them to .cspell.json so lint:docs passes.
Regenerated via the visual:update CI job now that the suite freezes animations and normalises fonts to deterministic DejaVu. 18 of 32 baselines shifted (phone/tablet wrap-points); the rest are byte-identical.
Patches nuxt-cloudflared-tunnel so secondary targets (e.g. Storybook) only open a tunnel once their port is actually listening, skipping cleanly otherwise. Wires up via the new module-level checkPorts option and the patchedDependencies entry in pnpm-workspace.yaml.
Moves the inline titleTemplate logic out of app.vue into app/utils/titleTemplate.ts (applyTitleTemplate) with full unit coverage, replacing the inline closure.
Mounts the existing AppSplash component on first paint and dismisses it once fonts have loaded plus a 300ms minimum dwell, with a splash-fade leave transition. App root tests cover show/hide behaviour using fake timers.
Comments out the /writing links in both header and footer nav (the section is parked) and adjusts the layout tests accordingly, while adding coverage for elsewhere links appearing in the mobile menu.
Adds composables that fetch live data (drupal.org REST, GitHub events, npm registry) with static-data fallbacks, and App* wrapper components that consume them. Refactors the home and open-source pages to compose these (stat band, activity feed, contribution heatmap, flagship Druxt card, module list with drupal/co-maint/npm filters, DrupalCon list). Also excludes *.stories.ts from the coverage gate, adds the project terms the new code introduces to the cspell dictionary, and resolves resulting typecheck/lint nits in the composables and their specs.
Replaces em-dashes with hyphens and tidies apostrophes across about/styleguide/talks and the article markdown for typographic consistency, softens the 'most-installed single-author' claim to 'most widely installed', swaps the about headshot for a Gravatar URL, and adds href links to the project cards data.
preview:live now needs only [build], so it is no longer skipped when an earlier non-allow_failure job (e.g. visual regression with stale baselines) fails. The manual preview stays available as long as the static generate succeeds.
External API calls (drupal.org, api.github.com, npm registries) and remote images made the generated site non-deterministic, so every composable rendered with live data and captures depended on the network. Intercept routes to fulfil images with a transparent pixel, abort all other external requests, hide <img> boxes, and wait for the boot splash to leave the DOM before capture. The site itself (localhost) is served normally and composables fall back to their static data, so snapshots are now stable.
The apt chromium crashes on launch ("Code: null") and puppeteer's
bundled Chrome is x86_64, failing under Rosetta on the arm64 runner.
Playwright's native arm64 chromium -- already proven by the visual and
seo jobs -- launches reliably with its bundled deps. Point
chrome-launcher at it via CHROME_PATH (unlighthouse auto-adds
--no-sandbox when running as root).
The drupal.org, GitHub, and npm profile links sat as a button row at the foot of the module list, where they read as module actions. Move them inline beneath the open-source intro as plain text links so the module list is purely modules and the profile handles are where the reader first lands.
The home and open-source pages fetched GitHub and Drupal APIs straight from the browser, which capped the data at the public REST rate limits, left contribution history at a thin 90-day window, and let burst pushes flood the activity feed. Move the external calls behind two Nitro server routes so a token (when configured) raises the limits and the GitHub GraphQL contribution calendar supplies a full year of history: /api/activity-gh fans out the public events across three pages, /api/contributions reads the contribution calendar. The composables now point at those proxies. Enrich the feed itself: collapse same-repo, same-day PushEvents into a single "pushed N times" row, pull Drupal GitLab merge requests in as a source, and raise the cap from 5 to 30. AppActivityFeed gains a limit prop so the home page keeps a short view while the open-source page scrolls the full list. Expand the contribution heatmap from 18 weeks to a full year (52) with horizontal scroll pinned to the latest week, and switch the static nitro preset to routeRules prerendering so the server routes can run.
…client data - Konami code unlock + leet-speak password gate (session-only) - Color scheme switcher (magenta/electric/violet/cyan/amber/orange) - Grid overlay (4px/8px dot grid), column guide, element outlines, opacity slider - Measure tool with dual-element pinning, gap calculation, alignment detection - Client data inspector: Nuxt payload JSON dump + static shell iframe overlay - Geometry utilities extracted to app/utils/dev-measure.ts (pure functions) - 101 tests (66 component + 35 geometry), 100% coverage - Documented in DEV-TOOLS.md
- Export fetchNpmPackages() as standalone async function - Coalesce search.objects ?? [] once to eliminate duplicate branch points - Use non-null assertion on data.value!.downloads (guaranteed truthy after guard) - Add 6 tests covering all fetch branches: search fail/success, all-scoped, downloads fail, nullish objects, nullish downloads - Achieves 100% coverage on useNpmPackages.ts
- Replace direct import.meta.dev with inject('devMode', import.meta.dev)
- import.meta.dev is false in vitest, so the true branch was uncovered
- Add test providing devMode:true to cover DevGrid rendering path
- Add explicit test verifying DevGrid absent by default in test env
- Achieves 100% branch coverage on default.vue
- Expand single-line CSS declaration blocks to multi-line (5 fixes) - Convert rgba() to modern rgb() notation, alpha to % (59 auto-fixes) - Add empty lines between CSS rules - Wrap markdown lines to 80 chars, add blanks around fences/lists - Add .markdownlint.jsonc symlink so config is found in submodule
Words: unleet, hacktheplanet, devconsole, devgrid, computeds, colour, lpignore, normalises, wrongpassword, prefs
- Add useOSSProfiles composable aggregating Drupal, GitHub, and npm stats - Add useDevPrefs composable for DevGrid module-filter toggle - Add format.ts utils with abbreviateNumber/shortHash helpers - Add Nitro server routes: github-stats.get.ts, repos-gh.get.ts - Proxy activity-gh and contributions through Nitro (remove direct client fetch) - Enrich useModules with totalDrupalStars, useNpmPackages with GitHub stars - Add stars field to modules data, expand co-maintained entries - Extend vitest coverage to include server/**/*.ts - Add nuxt/dist symlink to .gitignore - Full test coverage for all new composables, utils, and server routes
- Add AppOSSProfiles component rendering Drupal/GitHub/npm stat cards - Wire OSS profiles into open-source page ecosystem pane - Add DevGrid client-data inspector: Nuxt payload viewer + static SSG shell overlay - Add DevGrid module-list filter toggle (fades low-install modules) - Strip DrupalCon & community section from open-source (moved to /community) - Extend loaders.spec.ts with AppOSSProfiles and AppFlagshipDruxt coverage
…pacing - Add useContactModal composable (SSR-safe useState) for layout-level modal - Wire about page 'Get in touch' button to open contact modal (was LinkedIn link) - Expose ffpSites from useStats for live File (Field) Paths install count - Replace hardcoded '31,546' in about page bio with live data interpolation - Standardize hero spacing across index, about, open-source, community (pt-20 pb-12 mt-7) - Disable photos section for first launch (nav commented out, homepage teaser hidden) - Update layout and page tests for contact modal and photos disable
- Add composables table (all 10) to README and AGENTS.md - Update pages table: add /community, mark /writing and /photos as disabled - Add disabled-sections section to AGENTS.md - Document hero spacing convention (pt-20 pb-12, mt-7 gaps) - Update structure tree and status section - Fix CHANGELOG routes (10 routes, /community replaces /speaking) - Add CHANGELOG entries for contact modal, ffpSites, hero spacing, disabled sections
- Add h3 to knip ignoreDependencies (transitive Nitro dep used in server routes) - Add unstub, imageapi, textimage to cspell dictionary
Add freezeDynamicContent() to the Playwright visual suite. Called after hydration but before the screenshot, it replaces build-time baked live data (drupal.org install counts, GitHub stats, npm downloads, activity feed timestamps/content, contribution heatmap cells) with fixed placeholders so baselines are deterministic regardless of when `nuxt generate` ran.
Baselines regenerated from x86_64 CI (job 4905). All 32 snapshots (8 pages × 4 breakpoints) now show ###### placeholders for live API data instead of build-time baked values.
|
Important Review skippedToo many files! This PR contains 206 files, which is 56 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (72)
📒 Files selected for processing (206)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Deploy Preview for stuartc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…uild after ui clone
…G, API routes as Netlify Functions
Summary
Complete rewrite of the stuar.tc frontend from the Druxt/Nuxt 2 era to Nuxt 4, retaining the Drupal CMS backend.
routeRules: { '/**': { prerender: true } })GH_TOKEN)/communitypage with zigzag timeline for organising roles and sorted talks@stuartclark/uiintegration with Tailwind v4 scanning fixDecipher/sc-uibefore build (UI_REPO_TOKEN)Netlify setup (before merging)
feat/nuxt4nuxtbash scripts/netlify-install.shpnpm generateGH_TOKEN,UI_REPO_TOKEN(read access toDecipher/sc-ui)feat/oss-components→mainonDecipher/sc-uiso the install script picks up the latest ui