Personal website for Stuart Clark — a Nuxt 4 app built on Nuxt UI v3 and the
@stuartclark/ui design system. Static-generated. The /writing
section's content is synced from a real Drupal 11 backend into a typed
@nuxt/content data collection — see Content sync below.
- Nuxt 4 + Nuxt UI v3 (Tailwind v4, semantic utilities)
- @nuxt/content v3 (typed article collection, Drupal-synced — see below)
- @nuxt/fonts (self-hosted Archivo + JetBrains Mono)
- @stuartclark/ui design system (34 Vue SFCs, consumed as
link:../../ui) - Drupal 11 backend — source of truth for
/writingcontent, decoupled via JSON:API - Light + dark mode via Nuxt UI color mode
nuxt-cloudflared-tunnelmodule (dev-only live preview: site + Storybook tunnels)
mise install # Node 24 + pnpm 10
mise run install # pnpm install (in nuxt/)
mise run dev # http://localhost:3000
mise run hooks:install # optional: enable commit-msg + pre-commit hooksNo Docker/DDEV required — just PHP, Composer, and SQLite, via .devtools/
(see drupal/.devtools/README.md for the full breakdown):
cd drupal
make build # assemble (composer install) + provision (SQLite + content import) + start
make login # one-time login linkThe built-in PHP server auto-discovers a free port (from 8888) and prints
its URL on start/login/info. A Cloudflare quick tunnel opens by default
for sharing a live local preview — opt out with CLOUDFLARE_TUNNEL=0.
/writing articles live in Drupal, not as hand-authored Markdown. A sync
script (nuxt/scripts/sync-content.mjs) pulls them from Drupal's JSON:API —
via druxt's DruxtClient/DruxtSchema, not the legacy Nuxt 2 druxt
package — and writes typed JSON into nuxt/content/articles-data/, which
@nuxt/content then serves as an ordinary collection. The frontend never
calls Drupal directly at build or runtime; it's headless once synced. RSS
feeds (/blog.xml, /planet-drupal.xml) and comments (via Giscus) run off
the same synced data.
pnpm typecheck # vue-tsc
pnpm lint # ESLint
pnpm lint:style # Stylelint
pnpm lint:knip # Dead code detection
pnpm lint:spell # cspell
pnpm test # Vitest
pnpm test:coverage # Same, with coverage report (thresholds enforced — effectively 100%, see nuxt/vitest.config.ts)
pnpm size # Client bundle size budget (size-limit)
pnpm generate # Static build → .output/public/
pnpm test:visual # Playwright visual + SEO (needs generate first)Or via mise:
mise run ci # typecheck + lint + style + knip + spell + tests
mise run ci:full # + visual regression + SEO audit| Port | Service |
|---|---|
3000 |
Nuxt.js |
6006 |
Storybook |
8888+ |
Drupal (auto-discovered by .devtools/start) |
Colors are registered via @theme static in app/assets/css/main.css:
| Palette | Alias | Usage |
|---|---|---|
| magenta | primary |
links, tags, CTAs, active states |
| sand | neutral |
backgrounds, text, borders |
| electric | — | accent highlights |
| coral | — | secondary accents |
Nuxt UI semantic utilities (text-highlighted, text-muted, text-dimmed,
bg-default, bg-muted, bg-elevated, border-default) automatically adapt
to light/dark mode. app.config.ts sets primary: 'magenta', neutral: 'sand'.
Fonts: Archivo (display/body) + JetBrains Mono (labels/numerals), self-hosted by @nuxt/fonts.
The design system lives in the sibling @stuartclark/ui repo (34 Vue
SFCs built on Nuxt UI primitives). It is consumed here as link:../../ui:
| Category | Components |
|---|---|
| Layout | AppHeader, AppFooter, AppLogo, ThemeToggle, BackToTop |
| Atoms | Eyebrow, StatusPill, StatBlock, StatBand, Quote, SectionHeader, Steps |
| Data | ModuleRow, ActivityRow, ProfileRow, ContributionHeatmap |
| Cards | ProjectCard, FeaturedCard, DrupalConCard, UsesGroup, GiveBackCard, ArticleCard, SponsorCard |
| Content | ArticleRow, CodeBlock, CommentItem, CommentComposer, PageHero, EcosystemPane |
| Media | ImageSlot, ImageLightbox, Gallery |
| Overlays | ContactModal |
| Loaders | AppSkeleton |
| Route | Description |
|---|---|
/ |
Hero, stats band, heartbeat, latest writing, selected work |
/about |
Bio, headshot, expertise, elsewhere links; "Get in touch" opens contact modal |
/open-source |
Profiles, module installs, contribution heatmap, activity, flagship DruxtJS |
/community |
Talks, Splash Award, DrupalCons attended, organising & training |
/writing |
Article list — Drupal-sourced (see Content sync) |
/writing/[...slug] |
Article detail, with Giscus comments |
/blog.xml, /planet-drupal.xml |
RSS feeds over the same article data |
/uses |
Tools, hardware, services — disabled for first launch |
/drupalgive |
Maintained projects, DrupalCons — disabled for first launch |
/styleguide |
Component showcase — disabled for first launch |
/photos |
Photography gallery — disabled for first launch |
Disabled pages live in app/disabled-pages/ (out of the router entirely, not
just hidden from nav) and can be moved back into app/pages/ to re-enable.
Auto-imported from app/composables/. Most fetch live data from the Drupal.org
or GitHub APIs at build time (SSG) with static fallbacks.
| Composable | Purpose |
|---|---|
useSite |
Site config singleton (name, tagline, socials) |
useStats |
Stats band data; ffpSites exposes the live File (Field) Paths install count |
useModules |
Drupal.org project_module installs, ranked by usage |
useCoMaintainedModules |
Curated co-maintained modules from Drupal.org API |
useNpmPackages |
npm download counts + GitHub stars |
useActivity |
Merged GitHub + Drupal GitLab activity feed |
useContributions |
Contribution heatmap cells (GitHub + Drupal) |
useDrupalCons |
DrupalCon attendance from Drupal.org profile API |
useOSSProfiles |
Open-source profile aggregates (Drupal, GitHub, npm) |
useContactModal |
Shared useState for the layout-level contact modal (any page can open it) |
useHomeReadiness |
Signals when the homepage's above-the-fold data has settled, so the splash screen can hide without a fixed timer |
useShareUrl |
Request-aware canonical URL for share links/QR codes (tunnel in dev, stuar.tc in production) |
useQrCode |
Generates the QR code used on OG share images |
useDevPrefs |
Persisted preferences for the dev-only DevGrid overlay |
nuxt/
app/
app.vue Root: UApp + head/SEO meta + splash screen
app.config.ts Nuxt UI: primary=magenta, neutral=sand
assets/css/main.css @theme static: magenta/sand palettes + bg-stripes
components/ App wrappers (StatBand, ActivityFeed, etc.) + DevGrid;
AppDruxtParagraph*.vue render the synced paragraph tree
composables/ 14 auto-imported composables (see above)
data/ Typed TS data (site, stats, projects, modules, etc.)
layouts/default.vue AppHeader + slot + AppFooter + ContactModal
pages/ Active routes (see Pages above)
disabled-pages/ Routes excluded from the router — move back to re-enable
content/articles-data/ @nuxt/content collection, synced from Drupal (JSON, not hand-authored)
content.schema.ts Zod schema for the article collection (dependency-free, so
tests/content/*.spec.ts can validate content files directly)
scripts/sync-content.mjs Pulls Drupal JSON:API → content/articles-data/*.json
server/routes/ blog.xml, planet-drupal.xml (RSS, prerendered)
server/api/ contact form, GitHub/Drupal activity + stats proxies
tests/ Vitest unit/component, Playwright visual + SEO
drupal/ Drupal 11 backend — source of truth for /writing content.
JSON:API tested via kernel/functional tests in CI; consumed
by the Nuxt frontend only at sync time, never at runtime
drupal/.devtools/ Composer + PHP built-in server + SQLite local dev workflow
.githooks/ Mise-driven commit-msg + pre-commit hooks
.gitlab/ CI pipeline + helper scripts
wiki/ Deeper architecture/testing/troubleshooting docs (submodule)
GitLab CI (.gitlab-ci.yml) runs on MRs with stages lint → test → build → visual → audit → sync → preview. Every install job clones + builds
@stuartclark/ui as a sibling so the link:../../ui dependency resolves.
GitHub Actions (.github/workflows/) mirror the pipeline for when GitHub CI
is in use, and add a drupal job (PHPCS, PHPStan, PHPUnit via .devtools/)
for the backend. Coverage reports upload to Codecov.
Netlify deploys automatically from the connected branch — build command
pnpm generate, base directory nuxt, via netlify.toml and the
clone-ui build plugin (see nuxt/netlify/plugins/clone-ui).
Follows GitFlow:
feature/fix/docs branches off develop, releases cut from main, main
merged back into develop after each release. See CHANGELOG.md
for release history.