• Jun 7, 2026
  • 7 min read

Shipping Custom Sites: The Practices I Keep Across Every Project

After building the website audit tool I went back through every site I run: a solver’s marketing site, a technical blog, a consulting landing page, an ebooks library, the audit tool itself and this site. Each one had grown its own habits. Some were excellent, some were template leftovers nobody had looked at since the first commit.

This post keeps the excellent ones. It’s the checklist I now apply to every custom site I ship, for myself or for a client, with the real snippet behind each item.

1. Static first, and deploy only what you committed

Every site here is Astro with output: 'static', served by Cloudflare Pages. No server to patch, nothing to scale, and a page is a file.

Two habits make that safe:

  • Build from a clean checkout. A working folder collects drafts. For direct-upload projects I build from a git worktree of the commit, run npm ci there (which also proves the lockfile is complete), and deploy that dist/. Details in deploying direct-upload Pages from a git worktree.
  • Tag before a migration. git tag -a pre-<change> on the last commit before a big change makes “put the old site back” a single checkout.

2. One canonical address

Pick apex or www, redirect the other with a 301, and redirect the *.pages.dev hostname too, or analytics and search engines split one site into three. I keep a small script that curls every variant and fails on a wrong status. (apex vs www and pages.dev redirects)

The best version I had was on the consulting site: privacy and terms live in an Astro content collection, rendered by one LegalLayout.

---
import LegalLayout from '../layouts/LegalLayout.astro';
import { getEntry } from 'astro:content';
const privacy = await getEntry('legal', 'privacy');
const { Content } = await privacy.render();
---
<LegalLayout title={privacy.data.title} lastUpdated={privacy.data.lastUpdated}
             description={privacy.data.description}>
  <Content />
</LegalLayout>

Why Markdown wins for legal text:

  • It’s reviewable. A policy change is a diff, with a date in frontmatter.
  • It travels. The same file structure works on every site; the audit tool simply imports its privacy.md into a page.
  • It can be checked before deploy. The solver site starts from a template with {{BUSINESS_NAME}}-style placeholders, and make deploy runs this first:
// Fail the deploy if legal pages still contain {{PLACEHOLDER}} tokens.
for (const file of await readdir(dir)) {
  const text = await readFile(path.join(dir, file), "utf8");
  for (const token of new Set(text.match(/\{\{[A-Z_]+\}\}/g) ?? [])) {
    console.error(`check-legal: ${file} still contains ${token}`);
    missing++;
  }
}
if (missing) process.exit(1);

Two content rules matter more than the format: write the policy against the code, not the plan (every retention period and processor should exist in code), and use one contact address that actually reaches you.

4. Forms without third parties

Every form posts to a same-origin endpoint (/api/forms/submit) handled by one Worker, and every form works without JavaScript: a real <form action> that redirects back with ?form=sent, enhanced with fetch when scripts load. A hidden honeypot field catches dumb bots; optional consent is its own unticked box; subscriptions go through double opt-in. (one forms Worker for every site)

The payoff: no survey widget, no email relay, no third-party script on any page with a form, which is every article page here.

5. Analytics without cookies

Cloudflare Web Analytics, cookieless, at the zone or Pages level. When I switched, the lesson was to remove the old analytics code, not just stop configuring it: env-driven snippets for a dropped tool were still in four layouts.

6. Headers in one file

Cloudflare Pages reads public/_headers verbatim. The strongest one I have, from the ebooks site:

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Content-Security-Policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'; …

And from the solver site, the caching half:

/_astro/*
  Cache-Control: public, max-age=31536000, immutable
/fonts/*
  Cache-Control: public, max-age=31536000, immutable

Hashed build assets can be cached forever; HTML can’t. For an installable web app, two more rules saved me hours: serve the manifest as application/manifest+json (some CDNs send octet-stream and the install prompt vanishes) and send the service worker with Cache-Control: no-cache (or an old app version sticks around). One warning: _headers must be committed, or a clean build ships without it.

7. Content features that pay for themselves

FeatureWhereWhy
Sitemap (@astrojs/sitemap)solver, ebooksSearch engines find every page
RSS (@astrojs/rss)solverReaders and aggregators
Open Graph images rendered with headless ChromesolverEvery page looks deliberate when shared
Pagefind searchebooksFull-text search with no server
KaTeX math (remark-math + rehype-katex)ebooksEquations render at build time
Mermaid diagramsthis site, ebooksDiagrams live next to the text, in the repo
Code samples executed in CI (verify-snippets.py)solverEvery snippet on the site actually runs
Scheduled rebuildsblogFuture-dated posts appear on their day

For diagrams, validate before you push: render each block with mermaid-cli and look at the PNG. (validating Mermaid diagrams)

8. Quality gates

  • Type-check with astro check.
  • Lighthouse CI with real thresholds. The consulting site fails the build below 0.9 on performance:
"assert": { "assertions": {
  "categories:performance": ["error", { "minScore": 0.9 }]
}}
  • Test the UI states you can’t easily trigger, like “check your inbox”, with Playwright route mocking against the built site. (route mocking for form states)
  • Audit the result like a stranger would. Run your own site through the free audit tool: it’s humbling.

9. Copy as data

The consulting landing keeps hero text, FAQ, process steps and social proof in content collections; the audit tool keeps all of its copy (including the consent wording) in one site.md frontmatter file. Changing a headline or a call-to-action is a content edit, not a component change, and the copy stays reviewable in one place.

10. One mark, from one clean source

Generate every icon size from one high-resolution file: favicon.ico (16/32/48), the Apple touch icon (180) and the app icons (192/512). The trap I found: a template’s default SVG favicon left in place, which browsers that prefer SVG show instead of your logo.

11. Optional features behind a setting

The consulting chatbot and the audit tool’s database are both off by default, behind PUBLIC_CHATBOT_ENABLED and PB_ENABLED. The code stays, tested and documented, and turning a feature back on is one line in .env instead of an archaeology project.

The checklist

#PracticeDone when
1Static build, deployed from a clean checkoutdist/ comes from a commit, never a working folder
2One canonical addressapex/www and pages.dev all 301 to one host
3Legal pages as MarkdownA content collection + a pre-deploy check, policy matches code
4Forms without third partiesSame-origin endpoint, works without JS, honeypot, separate consent
5Cookieless analyticsOne tool, old snippets deleted
6Headers in _headersHSTS, CSP, nosniff, Referrer/Permissions-Policy, immutable assets, committed
7Content featuresSitemap, OG images, search/math/diagrams where they help
8Quality gatesastro check, Lighthouse CI thresholds, UI states tested
9Copy as dataHeadlines and CTAs editable without touching components
10Icons from one sourceNo template leftovers
11Optional features behind settingsOff by default, one line to enable

Takeaways

  • Your best practices are probably already in one of your projects; the work is making them the default everywhere.
  • Legal text, headers and copy are code: put them in the repo, review them as diffs, check them before deploy.
  • Template leftovers (an analytics snippet, a default favicon, an uncommitted _headers) are where good sites quietly regress.