Skip to content
JC — circuit gear (solid) Jorge Cortés· Home

Behind the scenes

How I built this site

This site is part of my portfolio too: the same automated delivery and infrastructure as code I offer clients, applied here. All the code is public.

See the repository: jcortes/website ↗

Architecture

A static site served from Cloudflare's edge, with three small functions for what needs a server.

  • Astro builds static HTML in Spanish (root) and English (/en/), styled with Tailwind CSS. There is no database.
  • Content (services, projects, profile, testimonials) is YAML files in the repository, validated against schemas: invalid content fails the build. I edit it in an admin panel (Keystatic) with GitHub login: every save is a commit that deploys itself.
  • The CV PDF and the Open Graph images are generated on every build from the same content, so they never go stale.
  • Cloudflare Pages serves the site on *.pages.dev. Three Pages Functions: /api/contact validates the form, verifies Turnstile and sends the email through Resend; /api/event stores conversion events in Workers Analytics Engine; /api/keystatic handles the panel's GitHub login.
  • Cookie-free analytics with Cloudflare Web Analytics, so there's no consent banner.

Pipeline

GitHub Actions verifies every change before it goes live.

  • Lint (ESLint and Prettier), typechecking and unit tests (Vitest) for the HTTP handlers.
  • Lighthouse CI on every page, in parallel without holding up the deploy: fails below 95 on performance.
  • End-to-end tests with Playwright against the production build: routes in both languages, the contact form, conversion events, axe accessibility checks, SEO, and a privacy guard that scans the HTML and the PDF for phone numbers and client names.
  • On every pull request: tofu fmt, validate and plan (posted to the PR), and a preview deploy with its own URL.
  • On merge to main: tofu apply and a production deploy.

Infrastructure as code

OpenTofu defines everything the site needs on Cloudflare, in infra/.

  • The Cloudflare Pages project and its functions' configuration: secrets as environment variables (including the key that encrypts the panel's session, generated by OpenTofu) and the Analytics Engine dataset.
  • The Turnstile widget (spam protection without annoying CAPTCHAs), whose secret key goes straight to the contact function.
  • The Web Analytics site.
  • State lives in a private Cloudflare R2 bucket; the decision is recorded in an ADR.
  • No secret is in the repository: they come from GitHub Actions and Cloudflare secrets.