How this site is built: Astro 7 bilingual blog + Cloudflare Pages

AstroCloudflare Pagesi18nsite-notesDeepSeek Harness

This post documents how HarnessDo.com went from zero to live — built and deployed by DeepSeek Harness, with me deciding and reviewing. A memo for myself, and hopefully a useful reference for anyone building a similar bilingual blog.

Who built it: DeepSeek Harness and me

The code, deployment, and troubleshooting were almost entirely done by DeepSeek Harness (DSH) — an AI coding agent. As the human, I made the decisions, supplied accounts and credentials, and reviewed every result. Splitting the labor makes it clear what an AI agent actually takes off your hands.

What DeepSeek Harness did

  • Built the Astro 7 bilingual project from scratch: config, i18n routing, content-collection schema, layouts/components/styles
  • Fixed environment issues: npm/pnpm cache EPERM, Astro telemetry sandbox block
  • Designed the bilingual content collections and fixed the id-collision bug for same-slug zh/en posts
  • Wrote the first bilingual posts (drafts)
  • Built, previewed, and verified every route locally
  • Created the Cloudflare Pages project and deployed it
  • Verified domain ownership via RDAP/DNS (caught the harnessgo.com vs harnessdo.com mismatch)
  • Renamed everything harnessgo → harnessdo (repo, Pages project, working directory)
  • Attached the custom domain, created the CNAME, and followed SSL issuance until https worked
  • Installed Cloudflare skills and MCP servers for DSH itself, and restarted the service
  • Renamed the visible brand to HarnessDo.com and redeployed
  • Git init and first commit

What I (the human) did

  • Registered the domain (harnessdo.com, at Cloudflare)
  • Made the stack decisions: Astro, bilingual zh+en, deploy to Cloudflare Pages
  • Completed the Cloudflare OAuth login in the browser
  • Created and configured Cloudflare API tokens (including DNS edit)
  • Corrected course each round: confirmed harnessdo, asked for the full rename, chose the HarnessDo brand
  • Provided the launch command and confirmed the restart
  • Ongoing: write posts, review content, maintain the site

In one line: DSH did nearly all the hands-on execution; I did the deciding, authorizing, and accepting. That is exactly the DeepSeek Harness usage this blog tracks.

Stack

Need Choice Why
Static site framework Astro Built for content sites; Markdown is a first-class citizen; outputs pure static pages with zero JS by default
Hosting Cloudflare Pages Free, global CDN, same account as the domain, no ICP filing
Writing format Markdown Content collections with schema validation — the cheapest thing to maintain long-term

Astro over Next.js or Hugo, mainly because of the content model: content collections validate frontmatter against a schema at build time, so a malformed post fails the build instead of silently breaking the site. That matters for a long-lived bilingual blog.

Project structure

HarnessDo.com/
├── astro.config.mjs          # site config + sitemap integration
├── src/
│   ├── content.config.ts     # content collection schema (Astro 5+ style)
│   ├── content/blog/
│   │   ├── zh/*.md           # Chinese posts
│   │   └── en/*.md           # English posts
│   ├── i18n/ui.ts            # bilingual UI strings
│   ├── layouts/              # Base / PostLayout
│   ├── components/           # Header / Footer
│   └── pages/
│       ├── index.astro       # root: client-side language redirect
│       ├── [lang]/           # /zh/ /en/ homes
│       │   ├── blog/         # post list + [...slug] post pages
│       │   ├── about.astro
│       │   └── rss.xml.ts    # one RSS feed per language
│       └── 404.astro
└── public/                   # favicon, robots.txt

How the bilingual part works

  • Routes are /zh/... and /en/...; the root / redirects client-side based on navigator.language (defaults to Chinese).
  • Each post is written twice under the same slug: src/content/blog/zh/<slug>.md and src/content/blog/en/<slug>.md, with lang declared in frontmatter.
  • Each file’s id keeps the language prefix (e.g. zh/2025-08-15-welcome) to avoid collisions between same-slug posts; the URL slug strips the prefix at the route layer (slugOf in src/lib/posts.ts).
  • UI strings (nav, buttons, about page) live in src/i18n/ui.ts, looked up by lang.

Deployment

The build output is dist/, pushed to Cloudflare Pages with wrangler:

pnpm build
wrangler pages deploy dist --project-name=harnessdo

Domain binding: Cloudflare dashboard → Pages → project → Custom domains, add HarnessDo.com, then add a CNAME record (@harnessdo.pages.dev, proxied). The domain is registered at Cloudflare, so no ICP filing.

How to publish a new post

  1. Write <date>-<slug>.md in src/content/blog/zh/, and the English version in en/, keeping the same slug.
  2. Required frontmatter: lang, title, description, pubDate. Optional: tags, draft, dshVersion, series.
  3. Preview locally with pnpm dev, then pnpm build && wrangler pages deploy dist --project-name=harnessdo to ship.

The schema lives in src/content.config.ts; new fields are added there.

Pitfalls hit along the way

  • Broken npm cache. ~/.npm on this machine contains root-owned files, so npm fails with EPERM. Fix: .npmrc points cache at the project directory, and commands override the inherited npm_config_cache env var (env beats project config).
  • pnpm’s dlx cache lives in the home dir too. pnpm create astro’s temp cache was blocked as well, so I hand-wrote every file — which ended up being clearer anyway, since each config is now understood.

That’s it. On to the actual writing.

← Back to blog