This Website: From PDF to Terminal-Styled SPA in 72 Hours
I sent my CV as a PDF to Claude. It came back as a fully structured, terminal-themed, markdown-driven single-page application — with Mermaid diagrams, scroll spy, resizable sidebar, and four color themes. Then I spent three days making it mine.
The Spark
It started at 3 AM on March 18, 2026. I had a polished PDF resume and a simple question for Claude: "Can you turn this into a website?"
I didn't mean a static HTML page with Bootstrap. I didn't mean a WordPress theme. I meant something that felt like me — a developer who lives in terminals, thinks in modules, and believes that anything worth doing is worth automating.
What came back was the initial commit: 28 files, 5,620 lines — a complete SPA with markdown rendering, a sidebar TOC, syntax highlighting, and a dark terminal aesthetic. Not a template. Not a wireframe. A working site.
That was the starting point. The next 72 hours were a conversation — not between a user and a tool, but between a developer and an opinionated collaborator.
Architecture: Zero Frameworks, Maximum Control
The site runs on vanilla JavaScript, CSS3, and HTML5. No React, no Vue, no bundler, no build step beyond TOC generation. Three JS modules, one CSS file, a handful of markdown documents.
index.html ← Single HTML shell
├── css/style.css ← 1,270 lines of themed CSS
├── js/
│ ├── app.js ← SPA routing, TOC, scroll spy (~700 lines)
│ ├── markdown-renderer.js ← marked.js + Mermaid + code blocks (~310 lines)
│ └── theme-switcher.js ← Color/OS mode persistence (~170 lines)
├── content/ ← All content as markdown
│ ├── about.md
│ ├── skills.md
│ ├── experience/*.md ← One file per role
│ ├── projects/*.md
│ ├── blog/*.md ← Technical articles
│ └── blog/cmf/*.md ← 13-part series with parent-child nav
├── scripts/
│ ├── build-toc.js ← Scans content/, generates toc.json
│ ├── workflow.js ← Interactive dev CLI
│ └── download-assets.js ← CDN asset downloader
└── toc.json ← Generated sidebar structureindex.html ← Single HTML shell
├── css/style.css ← 1,270 lines of themed CSS
├── js/
│ ├── app.js ← SPA routing, TOC, scroll spy (~700 lines)
│ ├── markdown-renderer.js ← marked.js + Mermaid + code blocks (~310 lines)
│ └── theme-switcher.js ← Color/OS mode persistence (~170 lines)
├── content/ ← All content as markdown
│ ├── about.md
│ ├── skills.md
│ ├── experience/*.md ← One file per role
│ ├── projects/*.md
│ ├── blog/*.md ← Technical articles
│ └── blog/cmf/*.md ← 13-part series with parent-child nav
├── scripts/
│ ├── build-toc.js ← Scans content/, generates toc.json
│ ├── workflow.js ← Interactive dev CLI
│ └── download-assets.js ← CDN asset downloader
└── toc.json ← Generated sidebar structureWhy no framework? Because the problem doesn't need one. The site loads markdown files via fetch, renders them with marked.js, highlights code with highlight.js, and draws diagrams with Mermaid. The browser already has everything else: DOM manipulation, history API, CSS custom properties, scroll events.
This isn't anti-framework ideology — it's the "anything-as-code" principle applied to itself. The simplest tool that solves the problem is the right tool.
The Terminal Aesthetic
The site mimics a terminal window because that's where I spend most of my time. The design isn't decoration — it communicates something about how I think about software.
┌─ ● ○ ○ ────── serard@dev00:~/cv ──── ☾ 🎨 ◑ 🍎 ⊞ 🐧 ─┐
├─ ~/about ──────┼──────────────────────────────────────────┤
│ > Who Am I? │ # Stéphane ERARD │
│ > Quick Facts │ > Pragmatic Full-Stack Web Architect │
│ ~/projects │ │
│ ~/experience │ As a full-stack web developer with │
│ ~/skills │ 15+ years of experience... │
│ ~/blog │ │
└────────────────┴──────────────────────────────────────────┘┌─ ● ○ ○ ────── serard@dev00:~/cv ──── ☾ 🎨 ◑ 🍎 ⊞ 🐧 ─┐
├─ ~/about ──────┼──────────────────────────────────────────┤
│ > Who Am I? │ # Stéphane ERARD │
│ > Quick Facts │ > Pragmatic Full-Stack Web Architect │
│ ~/projects │ │
│ ~/experience │ As a full-stack web developer with │
│ ~/skills │ 15+ years of experience... │
│ ~/blog │ │
└────────────────┴──────────────────────────────────────────┘Three OS Flavors
The title bar adapts to three operating system styles — because why not?
| OS | Title Bar | Window Controls |
|---|---|---|
| macOS | serard — ~/cv |
Traffic light dots (●●●) |
| Windows | C:\Users\serard\cv |
Minimize/maximize/close (— □ ×) |
| Linux | serard@dev00:~/cv |
Square buttons with symbols |
Each mode adjusts the title text, the window control styling, and the border radius. All persisted to localStorage.
Four Color Themes
The theming system uses CSS custom properties on the <html> element:
[data-color-mode="dark"] {
--bg-primary: #0d1117;
--text-primary: #e6edf3;
--text-muted: #848d97;
--accent-green: #3fb950;
--accent-blue: #58a6ff;
/* ... 16 variables total */
}[data-color-mode="dark"] {
--bg-primary: #0d1117;
--text-primary: #e6edf3;
--text-muted: #848d97;
--accent-green: #3fb950;
--accent-blue: #58a6ff;
/* ... 16 variables total */
}Switching themes is a single attribute change — data-color-mode and data-color-theme — and every element instantly reflows through the CSS cascade. No class toggling, no JavaScript DOM walks, no FOUC.
The four modes:
- Dark (default) — GitHub-dark inspired
- Light — GitHub-light inspired
- Dark High Contrast — WCAG AAA accessible
- Light High Contrast — WCAG AAA accessible
Deep Linking & Navigation
The URL hash drives everything. The format is #path/to/file.md::heading-slug, with :: as the separator between page and anchor. This avoids collisions with heading slugs that contain / or #.
// Parse: "content/blog/ddd.md::bounded-contexts--strategic-design"
// → page: "content/blog/ddd.md"
// → anchor: "bounded-contexts--strategic-design"
const idx = hash.indexOf('::');// Parse: "content/blog/ddd.md::bounded-contexts--strategic-design"
// → page: "content/blog/ddd.md"
// → anchor: "bounded-contexts--strategic-design"
const idx = hash.indexOf('::');Heading slugs are hierarchical: a h3 under a h2 under a h1 produces h1-slug--h2-slug--h3-slug. This means every heading in every document has a globally unique, human-readable anchor — and the TOC links resolve natively.
The sidebar TOC supports:
- Collapsible sections (
~/about,~/projects,~/blog, etc.) - Nested content trees (the 13-part CMF series has its own parent-child hierarchy)
- In-page heading navigation (h2/h3 headings appear below the active item)
- Scroll spy — the sidebar highlights the heading currently visible in the viewport
Mermaid Diagrams: Click to Explore
Mermaid code blocks render inline as SVGs with full theming support. But the real feature is the full-screen overlay: click any diagram and it opens in a zoomable, pannable modal.
// Zoom with mouse wheel
container.addEventListener('wheel', (e) => {
zoomLevel *= e.deltaY < 0 ? 1.1 : 0.9;
zoomLevel = Math.max(0.1, Math.min(5, zoomLevel));
applyTransform();
});
// Pan with click-drag
container.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panX += e.clientX - lastMouseX;
panY += e.clientY - lastMouseY;
applyTransform();
});// Zoom with mouse wheel
container.addEventListener('wheel', (e) => {
zoomLevel *= e.deltaY < 0 ? 1.1 : 0.9;
zoomLevel = Math.max(0.1, Math.min(5, zoomLevel));
applyTransform();
});
// Pan with click-drag
container.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panX += e.clientX - lastMouseX;
panY += e.clientY - lastMouseY;
applyTransform();
});This matters because architecture diagrams — like the BinaryWrapper pipeline or the Career Timeline — are dense. Being able to zoom into a specific node makes them actually useful, not just decorative.
The Sidebar: More Than a Menu
The sidebar is a resizable panel with a drag handle. Widths persist across sessions. It supports:
- Folder-based grouping: files in
content/blog/cmf/withparent: cmffrontmatter nest under their parent index - Icons per section: 📝 for standalone files, 📁/📂 for folders
- Tooltips: hover 650ms over a truncated title to see the full text (positioned outside the sidebar to avoid clipping)
- Active state: the current page gets a blue left border and a blue-tinted background
The TOC itself is generated at build time by scripts/build-toc.js, which scans the content/ directory, parses YAML frontmatter, extracts h2/h3 headings, and produces a structured toc.json. The heading slugs in toc.json use the exact same algorithm as the client-side renderer — so sidebar links always resolve correctly.
Content as Markdown
Every page on this site is a markdown file with YAML frontmatter:
---
title: "BinaryWrapper: Type-Safe .NET Wrappers for CLI Binaries"
section: blog
order: 1
date: "2025-01-15"
tags: ["C#", ".NET", "Source Generator"]
------
title: "BinaryWrapper: Type-Safe .NET Wrappers for CLI Binaries"
section: blog
order: 1
date: "2025-01-15"
tags: ["C#", ".NET", "Source Generator"]
---The renderer adds:
- Heading anchors with copy-to-clipboard buttons (📋)
- Code blocks with syntax highlighting and copy buttons
- Tables with striped rows and uppercase headers
- Internal links that resolve relative paths (
[DDD](#content/blog/ddd.md)) - Mermaid diagrams with full-screen overlay support
This means adding a new page is: create a .md file, add frontmatter, run npm run build:toc. Done.
Accessibility: Lighthouse 100/100
After the initial build, I ran automated accessibility scans using three tools — axe-core, Pa11y (with both axe and htmlcs runners), and Lighthouse — and discovered 50+ WCAG violations. Fixing them became a systematic effort across multiple layers.
Automated Fixes
Color contrast was the biggest category. The CSS variable --text-muted was #6e7681 on --bg-secondary #161b22 — a 3.77:1 ratio, below the WCAG AA minimum of 4.5:1. Fixed across all four themes:
| Theme | --text-muted before |
After | Ratio |
|---|---|---|---|
| Dark | #6e7681 |
#848d97 |
4.5:1+ |
| Light | #818b98 |
#656d76 |
4.5:1+ |
| Dark HC / Light HC | Already compliant | — | 7:1+ |
Other automated fixes:
- Skip links — TOC heading links used
#path::slugformat but targets only hadid="slug". Added focusable<span>anchors with the composite ID before each heading. - Mermaid duplicate IDs — Mermaid generates
id="node-undefined"on SVG<path>elements. Post-processing deduplicates them. The fullscreen overlay SVG is lazily injected on click (not in the DOM at scan time) to avoid duplicate IDs entirely. - Table header contrast — light theme used
--accent-blueon--bg-tertiary(3.8:1). Switched to--text-primary. - Active TOC item — light theme used
--accent-blueon a semi-transparent blue background (4.3:1). Darkened to#0550aewith reduced opacity. - Theme buttons — changed icon color from
--text-secondaryto--text-primaryfor sufficient contrast.
ARIA & Keyboard Accessibility
Beyond automated scanning, I addressed Lighthouse's 10 manual audit items:
| Audit Item | Implementation |
|---|---|
| Interactive controls are keyboard focusable | Added tabindex="0" to resize handle, contact link, Mermaid SVGs, and images |
| Elements indicate purpose and state | Added role="switch" + aria-checked to dark/light and contrast toggles, synced on every toggle |
| Custom controls have labels | Added aria-label to all OS buttons, theme toggles, contact link, copy buttons |
| Custom controls have ARIA roles | role="switch" for toggles, role="separator" for resize handle, role="button" for contact and diagrams |
| Focus directed to new content | SPA navigation focuses #markdown-output after page load so screen readers announce new content |
| Focus not trapped | Mermaid/image overlays close with Escape, focus returns to the triggering element |
| HTML5 landmarks | <header>, <nav>, <main>, <article> used throughout |
| Offscreen content hidden | aria-hidden="true" on icon spans (only the visible one per theme renders) |
Keyboard-Accessible Diagrams and Images
Mermaid diagrams and images support full keyboard interaction:
- Tab to a diagram or image (focusable,
role="button") - Enter or Space opens the fullscreen overlay with zoom/pan controls
- Focus moves to the close button in the overlay
- Escape closes the overlay
- Focus returns to the original element
Mermaid Theme Switching
Mermaid diagrams re-render when switching between dark and light mode. Each diagram stores its raw source in a data-source attribute, and mermaid.initialize() is called with the new theme variables before re-rendering. Timeline diagrams use explicit cScale0–cScale11 color palettes for both themes — pastel backgrounds with dark text for light mode, saturated backgrounds with white text for dark mode.
Cross-Theme Test Suite
The accessibility test suite runs across all four theme combinations using Pa11y's action system to click theme toggle buttons before scanning:
const themes = [
{ name: 'Dark (default)', actions: [] },
{ name: 'Dark + High Contrast', actions: ['click element #btn-color-theme'] },
{ name: 'Light', actions: ['click element #btn-color-mode'] },
{ name: 'Light + High Contrast', actions: ['click element #btn-color-mode',
'click element #btn-color-theme'] },
];const themes = [
{ name: 'Dark (default)', actions: [] },
{ name: 'Dark + High Contrast', actions: ['click element #btn-color-theme'] },
{ name: 'Light', actions: ['click element #btn-color-mode'] },
{ name: 'Light + High Contrast', actions: ['click element #btn-color-mode',
'click element #btn-color-theme'] },
];Results:
- axe-core: 0 violations (all themes)
- Lighthouse: 100/100
- Pa11y: 18 remaining errors across all themes — all emoji contrast false positives (
☾,☼,◑,📋) that axe-core correctly ignores
Run it yourself: npm run a11y (quick axe-core scan) or npm run a11y:full (all themes with Pa11y).
The Claude Collaboration
This website is the product of a larger story. The 72 hours weren't just about building a CV site — they were about building an entire ecosystem of .NET projects with Claude, and then documenting them here.
The Bigger Picture: FrenchExDev
In parallel with this website, I was working with Claude on the FrenchExDev ecosystem — a .NET & PowerShell monorepo with 20+ projects: type-safe CLI wrappers for 7 binaries (Docker, Podman, Vagrant, Packer, GitLab CLI...), a composable Result<T> pattern, a source-generated Builder, a meta-metamodeling DSL framework, a full DDD code generation pipeline, a Content Management Framework with 4 sub-DSLs, an AI-powered document search engine, and a Roslyn-based static analysis quality gate.
Each blog post on this site describes a real project that exists and was built or significantly advanced during those same 72 hours:
| Project | What It Does |
|---|---|
| BinaryWrapper | Wraps CLI binaries into typed C# APIs — powers 7 wrappers across 200+ binary versions |
| Result Pattern | Composable error handling replacing exceptions with Result<T, TError> types |
| Builder Pattern | Source-generated async object construction with validation |
| DDD & Code Generation | Attribute-based domain modeling that generates repositories, commands, events |
| QualityGate | Roslyn-powered static analysis — complexity, coupling, cohesion, mutation testing |
| Docker Compose Bundle | Typed models from 32 Docker Compose schema versions |
| Modeling & Metamodeling | The M0-M3 meta-metamodel theory behind the DSL framework |
| Feature Tracking | Requirements as code, with compile-time traceability from feature to test |
| Infrastructure as Code | The PowerShell module ecosystem tying it all together |
| CMF Series | 13-part deep dive into building a Content Management Framework from scratch |
The website was born from the need to present these projects coherently. A PDF CV can list job titles. A terminal-styled SPA with Mermaid diagrams, cross-referenced articles, and live code samples can show how you actually think and build.
The Collaboration Pattern
The pattern was always the same:
- I describe what I want (sometimes vaguely, sometimes precisely)
- Claude proposes an implementation
- I review, adjust, redirect
- We iterate until it feels right
This applied equally to writing C# source generators, designing DSL attribute hierarchies, and building this website. The projects and the site grew together — I'd implement a feature in the .NET monorepo, then write about it here, then Claude would help refine both the code and the article.
Some examples specific to this site:
- "Turn my CV PDF into a website" → Claude generated the initial 28-file SPA structure, choosing the terminal aesthetic based on my profile (DevOps background, IaC philosophy, terminal-centric workflow)
- "Add a blog section" → not just a list of posts, but a full markdown rendering pipeline with Mermaid support, code highlighting, and internal cross-references
- "The CMF series needs 13 parts with navigation" → Claude designed the parent-child TOC system with collapsible trees
- "Test accessibility" → research on tools (pa11y, axe-core, Lighthouse), installation, scanning, fixing violations, building a multi-theme test script
- "Write about this website" → this article
The interesting part: Claude made architectural decisions that I wouldn't have made myself — the :: deep-link separator, the hierarchical slug system, the CSS-custom-property-only theming. Some I kept because they were better than what I had in mind. Some I redirected. That's the nature of a good collaboration.
What Claude Code Brought to the Table
The first generation happened in Claude's web interface — I uploaded the PDF and got back a website structure. But the refinement happened in Claude Code (the CLI), which changed the dynamic entirely:
- File-level edits: Claude reads and modifies files directly, so iterations are fast
- Tool access: running accessibility scans, git operations, npm installs — all within the conversation
- Context persistence: Claude remembers decisions across the session, so I don't repeat myself
- Parallel exploration: while I review one change, Claude researches the next problem
- Cross-project context: Claude understands both the .NET codebase and this website, so blog articles accurately reflect the actual implementations
The 72-hour timeline:
| Day | What Happened |
|---|---|
| Day 1 (Mar 18) | Initial generation from PDF. Theme system. Experience entries. First blog posts on BinaryWrapper, Result, Builder. |
| Day 2 (Mar 19) | Homelab project writeup. DDD, CMF (13-part series), feature tracking, quality gates articles — all describing real FrenchExDev projects built in parallel. Menu fixes. |
| Day 3 (Mar 20) | Cross-references between posts. Workflow automation script. TOC refinements. Internal link resolution. |
| Day 4 (Mar 21) | Docker Compose article. Accessibility audit & fixes (Lighthouse 100/100). This blog post. |
The Developer Workflow
Local development is intentionally simple:
npm run dev # Rebuild TOC + start local server
npm run build:toc # Regenerate toc.json from content/
npm run a11y # Quick accessibility scan
npm run a11y:full # Test all 4 theme combinations
npm run work # Interactive CLI (commit, push, serve, rebuild)npm run dev # Rebuild TOC + start local server
npm run build:toc # Regenerate toc.json from content/
npm run a11y # Quick accessibility scan
npm run a11y:full # Test all 4 theme combinations
npm run work # Interactive CLI (commit, push, serve, rebuild)The workflow.js script provides an interactive menu for common operations — commit with message, restart server, rebuild TOC — because even the development process should follow the "anything-as-code" principle.
What I'd Do Differently
- Service Worker: the site could work offline with minimal effort — all content is static markdown
- Search: a client-side full-text search over the markdown files would be useful as content grows
- RSS feed: generated from frontmatter dates and blog section items
- Print stylesheet: exists but could be refined for PDF export (closing the loop from PDF → website → PDF)
The Takeaway
This site isn't just a CV. It's a demonstration of how I work: iteratively, with clear separation of concerns, using the simplest tools that solve the problem, and collaborating effectively with AI.
The entire codebase is vanilla — no framework, no bundler, no build complexity. The content is markdown — portable, version-controlled, readable without rendering. The theming is CSS custom properties — no JavaScript required for style changes. The architecture is modular — each concern (rendering, navigation, theming) lives in its own file.
And the whole thing started with a PDF and a prompt.
Total time: ~72 hours of iterative development with Claude. Total JavaScript: ~1,180 lines across 3 modules (no dependencies beyond marked/mermaid/hljs). Total CSS: ~1,270 lines, 4 color themes, 3 OS styles. Lighthouse score: 100/100 accessibility. Framework dependencies: zero.