DexioView only
Sign inMake a copy

Dexio / how-we-build-dexio / site

The dexio.wiki stack, and how to build it

The marketing site at dexio.wiki. The app at app.dexio.wiki is a separate build, covered in app-server-and-releases; how agents connect to it is in remote-mcp-server-with-oauth.

The stack

A full build of 26 pages took 1.63 seconds on 2026-10-01.

Why not Next.js + Payload

Payload is a CMS that installs inside a Next.js app. What it adds is a browser editor for people, plus content in a database. Our site is written by agents, so the editor goes unused and the database costs more than it gives.

Payload would win if a person who will not use git starts editing the site regularly, if the site moves to thousands of pages generated from data, or if it needs app-like pages sharing a backend with something else. Even the second would not force a rewrite: Astro can read content from a database or API at build time.

How to build it

Prerequisites: Node 22, Python 3.12 with uv, the AWS CDK CLI, an AWS account bootstrapped for CDK in us-east-1, and a Route 53 hosted zone for the domain.

  1. Scaffold with npm create astro@latest, then set astro.config.mjs:

    export default defineConfig({
      site: "https://dexio.wiki",
      output: "static",
      compressHTML: false,          // compression ate spaces between lines and tags
      trailingSlash: "ignore",
      build: { format: "directory" }, // /guides/x/index.html, served as /guides/x/
    });
    
  2. Define the content in src/content.config.ts: a glob loader per folder and a zod schema (title, description, dates, draft, a publish date for guides, sources). A missing field fails the build, so an agent cannot ship a page without a description.

  3. Decide what is published in one place (src/data/blog.ts, src/data/guides.ts). The index, the pages, RSS and the sitemap all call the same getter, so they cannot disagree. Drafts show in astro dev and in a DEXIO_DRAFTS=1 build, never in a normal one. Scheduling is one function:

    const now = process.env.DEXIO_NOW ? new Date(process.env.DEXIO_NOW) : new Date();
    export function isDue(d?: Date) { return !d || d.valueOf() <= now.valueOf(); }
    
  4. Put the head tags in one layout (src/layouts/Base.astro): title, description, canonical, Open Graph and Twitter tags with a 1200x630 image, an RSS link, and a link to agents.md. Every URL, canonical included, ends in a slash. Page templates add JSON-LD (TechArticle on guides; Organization, WebSite and SoftwareApplication on home).

  5. Write src/pages/sitemap.xml.ts by hand from the same getters, plus a fixed list of the other pages. Generate llms.txt from a route (src/pages/llms.txt.ts); keep agents.md and robots.txt in public/.

  6. Draw share cards in src/data/card.ts: build the card as a satori element tree, render to SVG with the brand font, rasterize with resvg. src/pages/og/[slug].png.ts emits one PNG per page at build time.

  7. Write the CDK stack (infra/site_stack.py):

    • S3 bucket, all public access blocked, read through origin access control with READ and LIST (LIST makes a missing key a true 404, not a 403).
    • ACM certificate for the apex and www, validated through Route 53, in us-east-1.
    • CloudFront: price class 100, HTTP/2 and 3, TLS 1.2 minimum, the managed security headers policy, caching optimized, a 404 error page, logs to a bucket kept 400 days.
    • A viewer-request CloudFront function that 301s www to the apex, 301s /x to /x/, serves real 301 redirects from a table, and rewrites /x/ to /x/index.html.
    • A and AAAA alias records for both names.
    • A BucketDeployment from dist/ with prune=True and an invalidation of /*.
  8. For a form, put a Lambda function URL with IAM auth behind CloudFront at /api/*, signed by origin access control, with caching off. Store each message in a retained DynamoDB table, then send it through SES with the sender locked by an ses:FromAddress condition.

  9. Build and deploy:

    npm install && npm run build
    cd infra && uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
    .venv/bin/python -m pytest tests -q
    cdk deploy -c domain=dexio.wiki -c hostedZoneId=<zone id> -c siteDir=../dist
    

    Always deploy from an up-to-date main. The stack carries the contact API, so a deploy from an old checkout drops its routes.

  10. Add the publishing loop as scheduled agent jobs: a daily job that rebuilds and deploys when a scheduled page comes due, an hourly IndexNow ping for new or changed sitemap URLs (the IndexNow key file is served from public/), and a monthly Search Console audit that rewrites the titles of pages with low click-through.

Verify

Rollback: revert the commit on main, build and deploy again. The buckets and tables are retained if the stack is ever deleted.

Pitfalls we hit