
Why rebuild?
I hosted my personal site on Squarespace for years. It worked, but I wanted something faster, cheaper to run, and fully mine to iterate on: a dark, modern portfolio that matches the way I ship demos, with content and layout in code instead of a hosted CMS.
So I rebuilt ronpicard.com as a React single-page app—TypeScript, Vite, React Router, sanitized article HTML, and static hosting on GitHub Pages (via the familiar gh-pages publish flow). This post is the engineering story: how I pulled the old site in, modernized the shell, added motion, and tightened security for a static deployment.
Rebuilding with Cursor and Claude
Most of the hands-on work happened in Cursor with several models enabled under Settings → Models, rather than locking a single model for every reply. I had Premium, Composer 2 Fast, GPT-5.3 Codex, GPT-5.4, Sonnet 4.6, and Opus 4.6 checked on so the editor could draw from both fast, coding-oriented options and heavier reasoning models. Cursor combines chat, multi-file Composer / agent-style edits, and optional terminal steps so the assistant can read the repo, propose patches across components and CSS, and run commands after you approve them.
In practice Cursor often auto-selects which of those enabled models serves a given request—especially in agent and Composer flows—so I was not switching models by hand on every message. The product tends to favor lower-latency models for small questions or tight feedback loops, and to reach for more capable models when the context is large, the change is multi-step, or the patch spans many files. The exact routing is implemented inside Cursor and evolves with releases; what mattered for this rebuild was curating the set of enabled models so automatic selection always had strong candidates.
I used short chat threads for targeted questions—Vite base for GitHub Pages vs a custom domain, React Router on a static host, react-helmet-async for per-route meta—and larger prompts when scaffolding UI (card grid, article layout, top bar) or iterating on design tokens in src/index.css. Whichever model Cursor routed in, the stack was strong at boilerplate and refactors; I still owned the architecture: mirrored JSON articles, slug derivation and legacy redirects in articles.ts, strict sanitization in prepareArticleBodyHtml, and the allowlisted demo and repo URLs.
What worked well: paste build errors verbatim, name the files in play, and state constraints up front (for example, that all article HTML must stay behind DOMPurify, or no new dependencies without a reason). What I avoided: landing large diffs without reading them, especially around security helpers and routing. Net result: faster iteration on look-and-feel and plumbing without outsourcing trust boundaries or deploy shape.
Migrating content: scraping instead of copy-paste
I did not want to manually re-enter dozens of posts, images, and PDFs. The repo includes a fetch pipeline aimed at the legacy Squarespace site: it reads sitemap.xml, downloads each blog URL once, and parses the rendered HTML with Cheerio to extract titles, dates, summaries, the article body, hero imagery, and “Demo / Code” style buttons (mapped to demoUrl and repoUrl in JSON).
Rough workflow (documented in the README):
npm run sync:articles— refreshsrc/data/siteArticles.jsonfrom the live Squarespace export.npm run mirror:resources— pull remote images and PDFs referenced in the JSON intopublic/resources/, rewrite paths to stable hashed filenames, and track them inscripts/resource-manifest.json.npm run merge:blog-post— merge a single new Squarespace post without redoing the whole sync.
On ingest, the fetch script strips scripts, iframes, forms, and other risky tags and runs DOMPurify so the archived HTML is already conservative before it ever hits the React app.
Modern UI: prompts, iteration, and taste
Squarespace gave me content; it did not give me the cyber-minimal grid I wanted. Most of the visual language—dark base, neon cyan/magenta accents, tight typography, card chrome—came from iterating in Cursor with the editor auto-routing across that same enabled model set: short natural-language prompts to explore layout options, refine CSS variables, and tune spacing until the home page felt like my demos rather than a generic template.
That is not “vibe coding” the whole architecture—the routing, data model, and build are deliberate—but AI was useful for faster feedback loops on look and feel, then I normalized patterns into shared classes in src/index.css so the site stays consistent.
Animations and motion
Once the layout was stable, I layered in atmosphere without drowning the content:
Ambient background — a lightweight
AmbientParticleslayer (subtle drifting dots and glyphs) driven by CSS@keyframesfor drift and twinkle, with cyan/magenta hues that match the accent system.Interaction polish — cards and controls use short
transitions on hover and focus so the UI feels responsive rather than static.Accessibility — animations respect
prefers-reduced-motionso the particles and carrier motion drop out when the OS asks for reduced motion.
Cyber hardening (for a site with no backend)
A static portfolio still has to assume untrusted HTML in long-form posts mirrored from the old CMS. The approach is defense in depth:
Sanitize at render —
prepareArticleBodyHtmlruns DOMPurify again with a strict allowlist of tags and attributes, strips inline event handlers, forbidsiframe/svg/script, and only allows<img src>that resolve to mirrored assets under/resources/.Safe outbound targets — demo and repo links are validated (e.g. GitHub Pages under my
github.iohost, GitHub repos ongithub.com) so tampered JSON cannot point the UI at arbitrary origins.External links — https links in articles get
target="_blank"withrel="noopener noreferrer"by default.Supply chain — keep dependencies current and run
npm auditbefore deploys; there is no app server or database in this repo to patch, but the JS bundle is still an attack surface for anyone who can influence content.
Other tweaks that paid off
Readable URLs — public
/blog/<slug>paths are derived from post titles inarticles.ts, while legacy Squarespace slugs still resolve for old bookmarks.Prerender — after
vite build,scripts/prerender.mjsemits static HTML for the home page and each post so crawlers and social previews see real<title>and Open Graph tags without executing the bundle.Custom domain —
public/CNAMEplus GitHub Pages settings point ronpicard.com at thegh-pagesbranch;vite.config.tsusesbase: '/'for that layout.
Card photo: Unsplash (Christopher Gower).