Bilingual (ES/EN) portfolio for a backend & AI engineer who builds his own projects: personal stationery in two inks, light first, built with Astro 7 and scoped vanilla CSS
Source code for my personal portfolio, for a backend & AI engineer: LLM systems in production on Google Cloud (agents, RAG, data) plus his own projects built with AI agents. The page reads like his printed stationery: the front of a business card, a letter sheet with the work, and the back of the card in a teal flood. Built with Astro 7 (SSG) and scoped vanilla CSS over design tokens, fully driven by public/cv.{es,en}.json. It ships no JavaScript of its own.
Live Site: sergiomarquez.dev
Light first, in the visual family of One dAIly Blog without copying it. Warm uncoated paper (#fafaf9), warm black ink, a teal identity ink (#0d4b4d) for the name, project and role names, and coral only for state: the monogram square, the email underline and hover/focus/active. Square corners, no shadows, no cards, no icons. A dark scheme follows prefers-color-scheme (no toggle); the teal footer flood is the same in both.
| Role | Family | Use |
|---|---|---|
| Sans | Space Grotesk (variable 300-700) | Name (700), headings and item names (600), all running text (400) |
| Serif | Instrument Serif (400, roman) | Only the headline statement, the About summary and the featured project headline |
Both fonts are self-hosted latin subsets in public/fonts/ (version in the file name, served immutable) with metric-matched local fallbacks. There is no monospace.
All tokens (inks, type steps, 8px spacing module, motion) are CSS custom properties in src/styles/global.css; it is the only file with color values or prefers-color-scheme (plus the two theme-color metas in BaseHead.astro).
- Bilingual (i18n): ES at
/, EN at/en/, language switcher as a plain link withlang/hreflang, and hreflang alternates. No automatic redirect by browser language. - Data-driven content: every section renders from
public/cv.{es,en}.json; interface strings live insrc/i18n/index.ts. Key parity between locales is enforced by tests. - Front of the card (first screen): name at display size, tagline, serif statement, and the contact block with the email as the main action plus the seven channels grouped in three rows: the blog (domain and description), videos (YouTube, TikTok, Instagram) and profiles (LinkedIn, X, GitHub). Links work from the first viewport.
- About, Projects, Experience: headings hang in a margin column and stay pinned (
position: sticky) while their section scrolls. Projects are grouped (publishing and generation, conversational agents, tools, games) with the featured one leading its group. The current role shows its four strongest highlights and folds the rest; earlier roles fold theirs into a native<details>. Certifications close the Experience section. - Back of the card (footer, outside
<main>): full-bleed teal flood with the email, the channel names and the language switch. - Links overprint: on hover and focus a link turns coral and its underline goes from 1px to 2px in a two-frame step (
steps(2), 90 ms). No other motion;prefers-reduced-motionremoves it. - Bilingual 404: one static
404.html(Cloudflare serves it for any path) with the message in Spanish and English,noindex, no canonical, hreflang or JSON-LD. - Accessibility: skip link, landmarks, no heading skips, 2px coral focus ring, links at least 24px tall, content fully visible without JavaScript.
- SEO: canonical, hreflang (es, en, x-default), JSON-LD
ProfilePagewithPersonasmainEntity(withhomeLocationfrombasics.location), OG/Twitter cards, sitemap (redirects are never listed), PWA manifest andapple-touch-icon. - Vanity redirects:
/linkedin,/github,/x,/twitter,/youtube,/yt,/blog,/tiktok,/instagram→ external profiles, declared inastro.config.mjs(redirects) frombasics.urlsincv.es.json. The build also writesdist/_redirectsfrom the same map, so Cloudflare Pages answers each one with a single HTTP 302; the meta refresh pages withnoindexstay as the fallback and serveastro dev. - Performance: inlined CSS, two preloaded font files, no client JavaScript, no build-time network calls, immutable caching for fonts (
public/_headers).
- Astro ^7.3.5 -- Static site generator with i18n routing
- @astrojs/sitemap ^3.7.4 -- i18n sitemap generation
- Scoped vanilla CSS -- design tokens in
global.css+ a preflight-style reset (reset.css); no CSS framework - TypeScript -- Type-safe JavaScript
- Vitest ^5.0.3 -- Unit testing framework
- Fonts -- Space Grotesk variable and Instrument Serif 400, latin subsets from Fontsource 5.3.0, self-hosted in
public/fonts/ - Biome -- Fast linting and formatting
- pnpm -- Package manager (local dev and Cloudflare Pages)
- Cloudflare Pages -- Hosting & CDN (builds with pnpm)
sergiomarquez-dev/
├── .github/workflows/ci.yml # GitHub Actions CI pipeline + CF Pages deploy hook + production check
├── .github/workflows/audit.yml # Weekly pnpm audit (no deploy)
├── .github/dependabot.yml # Monthly grouped GitHub Actions updates
├── .husky/ # Git hooks (pre-commit via lint-staged)
├── .vscode/ # Shared VS Code settings & extensions
├── docs/
│ ├── ARCHITECTURE.md # Architecture documentation
│ └── archive/ # Historical specs (shipped work), kept for reference
├── public/ # Static assets served as-is
│ ├── _headers # Cloudflare headers: cache-control, CSP, HSTS and other security headers
│ ├── ads.txt # Google AdSense verification
│ ├── cv.es.json # Portfolio data (Spanish)
│ ├── cv.en.json # Portfolio data (English)
│ ├── fonts/ # Space Grotesk + Instrument Serif (latin woff2, versioned names)
│ ├── favicon.ico / favicon.svg
│ ├── apple-touch-icon.png # 180x180 iOS icon
│ ├── icon-192.{png,svg} / icon-512.png # PWA icons
│ ├── manifest.webmanifest # PWA manifest
│ ├── og-image.png / og-image.svg
│ ├── Profile.pdf # Resume PDF (not linked from the page)
│ └── robots.txt
├── src/
│ ├── components/
│ │ ├── BaseHead.astro # Meta tags, preloads, manifest, hreflang, JSON-LD (noindex mode for 404)
│ │ ├── EmailLink.astro # mailto link with a break opportunity after "@"
│ │ ├── HomePage.astro # Section composition
│ │ ├── LanguageSwitcher.astro # Plain link to the other locale
│ │ ├── SergioMark.astro # Path-based "S■M" monogram
│ │ ├── SiteHeader.astro # Monogram + language switch
│ │ ├── SiteFooter.astro # Back of the card: teal flood with email and channels
│ │ └── sections/ # Intro (front of the card), About, Projects, Experience
│ ├── data/
│ │ ├── cv.ts # Typed loader + locale cache for cv JSON, pure view helpers
│ │ └── __tests__/ # Unit tests (cv data and helpers)
│ ├── i18n/
│ │ ├── index.ts # Locale config, translation dict, helpers
│ │ └── __tests__/ # Unit tests (locale helpers, date formatting)
│ ├── layouts/
│ │ └── Layout.astro # Skip link, header, <main>, footer
│ ├── pages/
│ │ ├── index.astro # Home page (ES, default locale)
│ │ ├── en/index.astro # Home page (EN)
│ │ └── 404.astro # Static bilingual 404 (noindex)
│ └── styles/
│ ├── global.css # Tokens, @font-face, base type, links, focus, section grid, print
│ └── reset.css # Preflight-style CSS reset (no framework)
├── astro.config.mjs
├── biome.json
├── pnpm-workspace.yaml # pnpm build-script approvals (esbuild)
├── skills-lock.json # Project agent skills (taste, web-design-guidelines); restore with `npx skills experimental_install`
├── CLAUDE.md # Agent instructions (AGENTS.md is a synced copy)
├── AGENTS.md
├── CHANGELOG.md
├── PRODUCT.md # Product context for design work (impeccable)
├── DESIGN.md # Design system derived from the build (sidecar: .impeccable/design.json)
├── SECURITY.md # Vulnerability reporting policy
└── package.json
- Node.js 26 (
.nvmrcpins 26.12.0) - pnpm 12 (
packageManagerpins 12.14.0; the project lockfile ispnpm-lock.yaml)
-
Clone the repository
git clone https://github.com/sergiomarquezdev/sergiomarquez-dev.git cd sergiomarquez-dev -
Install dependencies
pnpm install
-
Start the development server
pnpm run dev
- Spanish (default):
http://localhost:4321/ - English:
http://localhost:4321/en/ - Hot reload enabled for instant feedback
- Spanish (default):
# Development
pnpm run dev # Start development server with hot reload
pnpm run build # Create production build
pnpm run preview # Preview production build locally
# Quality checks
pnpm run type-check # TypeScript validation with Astro
pnpm run lint # Biome linting
pnpm run lint:fix # Auto-fix linting issues
pnpm run format # Auto-format with Biome
pnpm run format:check # Check formatting without modifying
# Testing
pnpm run test # Run unit tests (Vitest)
pnpm run test:watch # Run tests in watch mode
# Full validation (mandatory before committing)
pnpm run validate # type-check + lint + test + build- Update content in
public/cv.es.json/public/cv.en.jsonor tweak components. - Test locally with
pnpm run dev-- check both/(ES) and/en/(EN). - Run
pnpm run validateto ensure everything passes. - Commit changes (pre-commit hook runs lint-staged on
src/). - Push to
main-- CI runs lint/type-check/test/build, then triggers the Cloudflare Pages deploy hook and waits until production serves the pushed commit (/build.txt) before smoke-testing it.
Note: pushes touching only Markdown,
docs/or.vscode/skip CI and deploy (paths-ignoreinci.yml).
- Lighthouse / PageSpeed: last measured before the 2026-10 redesign (99 mobile / 100 desktop performance); re-measure after deploy
- Sitemap: Generated automatically via
@astrojs/sitemapwith i18n support; redirects are never listed - Hreflang:
<link rel="alternate">tags for ES, EN, and x-default - Structured data: JSON-LD
ProfilePage(withPersonasmainEntity) viaBaseHead.astro - Canonical URLs: Per-page canonical links
- PWA manifest:
manifest.webmanifestfor installability - Fast delivery: Static output served from Cloudflare Pages CDN with immutable caching for fonts (
public/_headers)
Sergio Marquez Backend & AI Engineer -- agents, RAG and LLMs in production
- Website: sergiomarquez.dev
- Email: [email protected]
- LinkedIn: sergiomarquezp
- GitHub: sergiomarquezdev
- X (Twitter): @sergiomarquezp_
- YouTube: @sergio_marquezp
- TikTok: @sergio_marquezp