Evan Panahi

Software, games, and whatever I’m reading.

Theme
Essay

Architecture of a Multi-Domain Astro Blog

How one Astro codebase powers three domains — eap.me, eap.dev, and eap.gg — using environment variables, GROQ queries, and a single source of truth in domain.ts.

1 February 2026

The Multi-Domain Challenge

Building a personal brand with multiple subdomains is a common pattern, but maintaining separate codebases for each is a maintenance nightmare. This post explains how eap.me solves it with one repo, one build pipeline, and a single config file.

The domain.ts Config

Everything starts with a single file that maps environment variables to domain-specific configuration:

export type Domain = 'me' | 'dev' | 'gg';
export type Category = 'dev' | 'gaming';

export const domainConfigs: Record<Domain, DomainConfig> = {
  me: { domain: 'me', category: null, title: 'Evan A Panahi' },
  dev: { domain: 'dev', category: 'dev', title: 'eap.dev' },
  gg: { domain: 'gg', category: 'gaming', title: 'eap.gg' },
};

Category-Based Filtering

Each subdomain filters content by category. The aggregator (eap.me) has a null category, meaning it shows everything. The GROQ queries use a helper to build the category filter:

export function buildCategoryFilter(category?: Category) {
  return {
    filter: category ? ' && category == $category' : '',
    params: category ? { category } : {},
  };
}

GROQ Query Design

Sanity queries are composed from reusable fragments. The field projections are shared across list and detail views:

Post list views use postFields while detail views add the body with postWithBodyFields.

Build Pipeline

Three Cloudflare Pages projects share the same Git repo. Each sets a different SITE_DOMAIN environment variable:

  • eap-me: SITE_DOMAIN=me
  • eap-dev: SITE_DOMAIN=dev
  • eap-gg: SITE_DOMAIN=gg