Infrastructure

Building The Trenches

A Deep Dive into Architecture, Design Tokens, and the Philosophy of Building in Public

Boyan Balev
Boyan Balev Software Engineer
18 min
Building The Trenches

The Confession

There’s something wonderfully absurd about writing an article about writing articles. A meta-confession about the infrastructure of confessions.

But here’s the thing: transparency is a muscle. If we’re going to write about open-source alternatives to proprietary platforms, about seeing through the cloud, about building in public, then we should probably start by showing you how this is built.

So consider this an anatomy lesson. A dissection of the trenches themselves.

What you’re reading right now is served from a static site generator, styled with hand-written CSS tokens, deployed on a free-tier CDN, and written in MDX files that you could open in any text editor. There’s no database. No CMS login. No monthly SaaS bill.

And that’s not an accident. That’s the point.


Part I: Why Astro

The Framework Wars Are Over (For Us)

Let’s be honest: the JavaScript framework ecosystem is exhausting.

React vs. Vue vs. Svelte. Next.js vs. Remix vs. SvelteKit. Server components vs. client components vs. island architecture. Every year brings a new paradigm, a new mental model, a new reason to rewrite everything.

We opted out.

Astro answered that question in a way that felt almost too obvious: ship zero JavaScript by default. Add it only where you need it. Islands of interactivity in a sea of static HTML.

The Static-First Philosophy

Here’s what happens when you load a page on The Trenches:

  1. Cloudflare’s edge serves a pre-built HTML file
  2. CSS loads (it’s small, we wrote it ourselves)
  3. Fonts load (four of them, but they’re optimized)
  4. That’s it. You’re reading.

No hydration. No client-side routing. No bundle that needs to be parsed before anything renders. Just HTML the way Tim Berners-Lee intended.

MetricTypical Next.js BlogThe Trenches (Astro)
JavaScript bundle 150-300KB ~0KB (most pages)
Time to Interactive 2-4 seconds Instant (it's HTML)
Build complexity Webpack/Turbopack config Zero config
Hosting requirements Node.js or Edge runtime Static files anywhere
Vendor lock-in Vercel-optimized None

Content Collections: The Hidden Gem

Astro’s killer feature isn’t the island architecture. It’s Content Collections.

// src/content/config.ts - Our actual configuration
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    subtitle: z.string().optional(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    category: z.enum([
      'Architecture',
      'Infrastructure',
      'Data Engineering',
      'Economics',
    ]),
    readTime: z.string(),
    tags: z.array(z.string()).default([]),
    heroImage: z.string().optional(),
    ogImage: z.string().optional(),
    accent: z.enum(['orange', 'blue', 'purple', 'pink']).default('orange'),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

That’s Zod schema validation for our frontmatter. If we try to publish an article without a description, or with an invalid category, the build fails. Type safety for content. No CMS needed.


Part II: The Stack Breakdown

The Full Inventory

The Trenches Tech Stack

Framework Astro 5.x Static-first, zero JS by default
Content MDX Markdown with component superpowers
Language TypeScript Strict mode, no exceptions
Styling Custom CSS Design tokens, no framework
Deployment Cloudflare Pages Free tier, global CDN
Images Cloudflare Images Optimized delivery

What We Didn’t Choose

The choices we didn’t make are just as important:

No Tailwind

We wrote our own CSS. It’s ~600 lines across three files. We understand every property. No purging, no JIT, no “why is this class not working in production?”

No CMS

No Contentful. No Sanity. No Strapi. Just MDX files in a git repo. Version control is our content management.

No Analytics SaaS

No Google Analytics. No Plausible. No Fathom. If we add analytics, it’ll be self-hosted and privacy-respecting.

No React

Astro components are enough. Zero client-side framework. The theme toggle is vanilla JavaScript. Everything else is static.

The Configuration

Our entire Astro configuration is 19 lines:

// astro.config.mjs - The whole thing
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://hundredsunited.com',
  integrations: [
    mdx(),
    sitemap(),
  ],
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
      wrap: true,
    },
  },
});

That’s it. Three integrations: MDX for rich content, sitemap for SEO, and Shiki for code highlighting. No plugins. No loaders. No configuration hell.


Part III: Layout Architecture

This is where things get interesting. The layout system is the backbone of any content site, and we built ours with three core principles: composability, responsiveness, and progressive enhancement.

The Three-Layer Layout System

BaseLayout.astro
    └── ArticleLayout.astro
            └── [Your MDX Content]

BaseLayout handles the foundation: HTML document structure, meta tags, theme initialization, and global styles. It uses Astro’s named slots for header and footer:

<!-- BaseLayout.astro (simplified) -->
<html lang="en">
  <head>
    <!-- Meta tags, OG tags, theme script -->
    <style is:global>
      @import '../styles/tokens.css';
      @import '../styles/base.css';
      @import '../styles/components.css';
    </style>
  </head>
  <body>
    <a href="#main-content" class="skip-link">Skip to main content</a>
    <slot name="header" />
    <main id="main-content">
      <slot />
    </main>
    <slot name="footer" />
  </body>
</html>

ArticleLayout extends BaseLayout and adds article-specific structure: the header with metadata, the two-column grid with table of contents, and the share footer.

The Article Grid: A Deep Dive

The article layout uses CSS Grid with a deliberate responsive strategy:

.article__layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-12);
  max-width: var(--content-width);
  margin: 0 auto;
}

@media (min-width: 1024px) {
  .article__layout {
    /* Content area + gap + TOC width */
    max-width: calc(var(--content-width) + var(--space-12) + 220px);
    grid-template-columns: var(--content-width) 220px;
  }
}

On mobile, the TOC disappears entirely. We don’t collapse it into a hamburger menu or a sticky floating button. We just remove it. Mobile readers scroll; they don’t need a table of contents.

.article__toc {
  display: none;
}

@media (min-width: 1024px) {
  .article__toc {
    display: block;
    order: 2;
  }
}

The Sticky Table of Contents

The TOC uses position: sticky with careful calculations to stay visible while scrolling:

.toc {
  position: sticky;
  top: calc(var(--header-height) + var(--space-8));
  max-height: calc(100vh - var(--header-height) - var(--space-16));
  overflow-y: auto;
}

Why these values?

  • top: The header is 64px tall. We add 2rem (32px) of breathing room. So the TOC starts 96px from the viewport top.
  • max-height: We don’t want the TOC to extend past the viewport bottom. Subtract the header height and some padding, and the TOC scrolls internally if needed.

The active link highlighting uses the Intersection Observer API:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      activeId = entry.target.id;
      updateActiveLink();
    }
  });
}, {
  rootMargin: '-80px 0px -80% 0px',
  threshold: 0,
});

That rootMargin is doing heavy lifting. -80px from the top accounts for the sticky header. -80% from the bottom means a heading is considered “active” when it’s in the top 20% of the viewport. This creates a natural reading flow where links highlight just before you read the section.

Wide Elements: Breaking the Content Width

Sometimes content needs to breathe. Images, code blocks, and comparison tables can feel cramped at 680px. We built a .wide class that expands elements to 920px:

.article__content :global(.wide) {
  max-width: var(--wide-width);
  margin-left: 50%;
  transform: translateX(-50%);
  width: 100vw;
  padding: 0 var(--container-padding);
}

@media (min-width: 1024px) {
  .article__content :global(.wide) {
    width: auto;
    padding: 0;
    margin-left: calc(-1 * (var(--wide-width) - var(--content-width)) / 2);
    transform: none;
  }
}

The trick on mobile: use width: 100vw with the translate hack to make elements full-width. On desktop, we do the math to center the wider element: (920 - 680) / 2 = 120px offset to the left.


Part IV: The Design Token System

This is where we spent the most time, and it shows.

Why Tokens Over Utility Classes

Tailwind is great. It’s also a layer of indirection. When you write text-gray-500, you’re not writing CSS, you’re writing a reference to CSS that you hope does what you expect.

We wanted to write CSS. Just CSS. But with constraints.

Design tokens are those constraints. They’re CSS custom properties that define the vocabulary of your design system. When you only have 12 spacing values, your spacing is consistent. When you only have 4 border radiuses, your corners are consistent.

The Complete Token Architecture

Our token file is 233 lines. Here’s the philosophy behind each section:

Typography Tokens

:root {
  /* Font Stacks - Fallbacks matter */
  --font-display: 'Playfair Display', Georgia, serif;
  --font-headline: 'Fraunces', Georgia, serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;

  /* Weights - Only what we use */
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  /* Type Scale - Modular, not arbitrary */
  --text-xs: 0.75rem;     /* 12px */
  --text-sm: 0.875rem;    /* 14px */
  --text-base: 1rem;      /* 16px */
  --text-lg: 1.125rem;    /* 18px */
  --text-xl: 1.25rem;     /* 20px */
  --text-2xl: 1.5rem;     /* 24px */
  --text-3xl: 1.875rem;   /* 30px */
  --text-4xl: 2.25rem;    /* 36px */
  --text-5xl: 3rem;       /* 48px */
  --text-6xl: 3.75rem;    /* 60px */
  --text-7xl: 4.5rem;     /* 72px */
}

The Spacing Scale

:root {
  /* 4px base scale */
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-5: 1.25rem;   /* 20px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
  --space-10: 2.5rem;   /* 40px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
  --space-20: 5rem;     /* 80px */
  --space-24: 6rem;     /* 96px */
}

Notice the gaps: there’s no --space-7, --space-9, or --space-11. This is intentional. We skip values to create meaningful jumps. When you need “a bit more” spacing, you jump to the next level, not just “4 more pixels.”

Layout Tokens

:root {
  --content-width: 680px;
  --wide-width: 920px;
  --max-width: 1200px;
  --header-height: 64px;
  --container-padding: var(--space-6);
}

@media (max-width: 768px) {
  :root {
    --container-padding: var(--space-4);
  }
}

The 680px content width isn’t arbitrary. It’s approximately 70-75 characters per line at our body font size, which is the optimal range for reading comprehension. Wider = your eyes lose track of lines. Narrower = too many hyphenations.

Transitions

:root {
  --transition-fast: 0.15s ease;
  --transition-base: 0.25s ease;
  --transition-smooth: 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

That custom cubic-bezier is an “ease-out-expo” curve. It starts fast and decelerates slowly, creating a feeling of weight and intentionality. We use it for elements that need to feel polished: card hovers, menu reveals, the progress bar.

The Dual Theme System

We support dark and light themes through CSS custom properties that swap on a data attribute:

/* Dark theme (default) */
:root {
  --bg-primary: #09090b;
  --text-primary: #f4f4f5;
  --accent-primary: #fb923c;
}

/* Light theme */
[data-theme="light"] {
  --bg-primary: #ffffff;
  --text-primary: #0f172a;
  --accent-primary: #ea580c;
}

The theme switch is vanilla JavaScript that runs immediately to prevent FOUC (Flash of Unstyled Content):

// This runs in <head>, before body renders
const theme = (() => {
  if (localStorage.getItem('theme')) {
    return localStorage.getItem('theme');
  }
  if (window.matchMedia('(prefers-color-scheme: light)').matches) {
    return 'light';
  }
  return 'dark';
})();

if (theme === 'light') {
  document.documentElement.setAttribute('data-theme', 'light');
}

Text Hierarchy: The WCAG-Compliant Way

:root {
  --text-primary: #f4f4f5;                    /* For headings, emphasis */
  --text-secondary: rgba(244, 244, 245, 0.72); /* For body text */
  --text-tertiary: rgba(244, 244, 245, 0.60);  /* For meta, captions */
  --text-muted: rgba(244, 244, 245, 0.36);     /* For disabled, placeholders */
}

Those alpha values weren’t guessed. We tested each against our background colors to ensure WCAG AA compliance (4.5:1 contrast ratio for text). The tertiary was originally 0.52, but we bumped it to 0.60 because small text (like timestamps) needs more contrast, not less.


Part V: Responsive Design Strategies

Responsiveness isn’t just media queries. It’s a philosophy about how your design degrades (or enhances) across viewports.

Mobile-First, Desktop-Enhanced

We write mobile styles first, then add complexity at larger breakpoints:

/* Base: Mobile */
.card-grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
}

/* Tablet and up */
@media (min-width: 768px) {
  .card-grid--2 {
    grid-template-columns: repeat(2, 1fr);
  }
}

The mental model: mobile is the constraint, desktop is the enhancement. If something doesn’t work on mobile, it shouldn’t exist.

The Responsive Type Scale

Large headings look great on desktop. On a 375px phone, they’re overwhelming:

@media (max-width: 640px) {
  :root {
    --text-4xl: 2rem;    /* was 2.25rem */
    --text-5xl: 2.5rem;  /* was 3rem */
    --text-6xl: 3rem;    /* was 3.75rem */
    --text-7xl: 3.5rem;  /* was 4.5rem */
  }
}

We don’t just scale down linearly. We compress the high end of the scale because the visual difference between 60px and 72px on desktop becomes negligible on mobile, but it saves precious vertical space.

Container Padding Strategy

:root {
  --container-padding: var(--space-6); /* 24px */
}

@media (max-width: 768px) {
  :root {
    --container-padding: var(--space-4); /* 16px */
  }
}

Why change padding at 768px instead of, say, 640px? Because 768px is where we typically switch from phone-portrait to tablet-landscape. Below that, every pixel counts. The 8px we save per side (16px total) gives content more breathing room.

The min() Trick for Grids

.article-grid {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}

That min(100%, 320px) is crucial. Without min(), minmax(320px, 1fr) would overflow on viewports smaller than 320px. The min() function ensures the minimum is never larger than the container itself.


Part VI: Component Architecture

The Callout Component: A Case Study

Every component follows the same pattern: typed props, semantic HTML, scoped styles. Here’s our Callout:

---
interface Props {
  type?: 'info' | 'warning' | 'success' | 'error' | 'insight' | 'violet' | 'rose';
  title?: string;
}

const { type = 'info', title } = Astro.props;

const icons = {
  info: `<circle cx="12" cy="12" r="10"></circle>...`,
  warning: `<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2...`,
  // ... more icons
};
---

<aside class:list={['callout', `callout--${type}`]} role="note">
  <svg class="callout__icon" set:html={icons[type]} />
  <div class="callout__body">
    {title && <strong class="callout__title">{title}</strong>}
    <div class="callout__content"><slot /></div>
  </div>
</aside>

Key decisions:

  1. Semantic HTML: It’s an <aside> with role="note". Screen readers announce it correctly.
  2. TypeScript props: You can’t pass type="banana". The build fails.
  3. BEM naming: .callout, .callout__icon, .callout--warning. Predictable, flat specificity.
  4. Inline SVGs: No icon library. Each icon is a few lines of path data. Total icon weight: ~2KB.

The Progress Bar: Performance Matters

The reading progress bar at the top of articles is one of only three JavaScript features on the site:

const progressBar = document.querySelector('[data-progress-bar]');
let ticking = false;

function updateProgress() {
  const scrollTop = window.scrollY;
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;

  progressBar.style.width = `${Math.min(100, Math.max(0, progress))}%`;
  ticking = false;
}

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(updateProgress);
    ticking = true;
  }
}, { passive: true });

Performance considerations:

  1. requestAnimationFrame throttling: Scroll events can fire 100+ times per second. We only update once per frame (~60fps).
  2. passive: true: Tells the browser we won’t call preventDefault(), allowing optimized scroll handling.
  3. Ticking flag: Prevents queuing multiple rAF callbacks.

The bar itself is CSS-only decoration:

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 0%;
  height: 3px;
  background: linear-gradient(90deg, var(--accent-primary), var(--accent-rose));
  z-index: calc(var(--z-fixed) + 1);
  pointer-events: none;
}

The gradient from orange to rose creates visual interest without being distracting. And pointer-events: none ensures it never interferes with header clicks.

Reveal Animations (That Respect Preferences)

Elements with the .reveal class fade in as you scroll:

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
  }
}

The prefers-reduced-motion check is mandatory. Users who enable reduced motion have done so for a reason, often medical. Ignoring this preference is an accessibility failure.


Part VII: Content Architecture

MDX: Markdown with Superpowers

Every article on The Trenches is an MDX file. MDX is Markdown that can import and render components:

import Callout from '../../components/blog/Callout.astro';
import KeyPoint from '../../components/blog/KeyPoint.astro';

## My Section

Regular markdown here.

<KeyPoint title="Important" accent="violet">
This gets rendered as a styled component with a slot for children.
</KeyPoint>

The result is rich, interactive content that’s still just a text file. No WYSIWYG editor. No block-based CMS. Just Markdown with components.

The Component Library

We built a focused set of components for articles:

Content Components

Callout Contextual Alerts info, warning, success, error, insight, violet, rose
KeyPoint Highlighted Ideas Accent-colored boxes for key takeaways
TechStack Technology Lists Structured tech inventory with categories
StatGrid Metrics Display Numbers that pop with accent colors
ComparisonTable Side-by-Side Compare options with colored headers
Card Boxed Content Variants: default, violet, rose, highlight

Each component is an Astro component: server-rendered HTML, scoped CSS, zero client-side JavaScript.

File-Based Routing

The URL structure mirrors the file system:

src/content/blog/
├── streaming-vs-batch.mdx          → /blog/streaming-vs-batch
├── snowflake-databricks-review.mdx → /blog/snowflake-databricks-review
├── anakincloud-open-source-paas.mdx → /blog/anakincloud-open-source-paas
└── building-the-trenches.mdx       → /blog/building-the-trenches

No route configuration. No page component boilerplate. Drop a file, get a URL.


Part VIII: Brand Identity

Why “The Trenches”?

The name isn’t accidental. “The trenches” is engineering slang for where the real work happens. Not the architecture diagrams. Not the conference talks. The actual codebase, at 2 AM, when the deploy is failing and you’re reading stack traces.

Voice Principles

Every article follows four principles:

Candid

We say what we actually think. No hedging to avoid controversy. No “it depends” without explanation. Strong opinions, clearly stated.

Data-Driven

Claims come with evidence. Cost comparisons show real numbers. Performance claims show benchmarks. Opinions are labeled as opinions.

Pragmatic

We optimize for “does this work in production?” not “is this theoretically elegant?” Real-world constraints matter more than ideal architectures.

Rigorous

We do the research. We read the documentation. We test the claims. If we’re wrong, we update.

Visual Identity

The design reflects the content philosophy:

  • Dark by default: Engineers live in dark mode. We meet them where they are.
  • High contrast: Text should be readable. Accessibility isn’t optional.
  • Warm accent colors: Orange primary, violet and rose secondaries. Technical content doesn’t have to feel cold.
  • Generous whitespace: Dense information needs room to breathe.

Part IX: Deployment & Infrastructure

Why Cloudflare Pages (Not Vercel)

We love what Vercel has done for developer experience. But for a static site, they’re solving problems we don’t have.

FactorVercelCloudflare Pages
Free tier bandwidth 100GB/month Unlimited
Build minutes (free) 6,000/month 500/month
Edge locations ~20 regions 300+ cities
Static site focus Optimized for Next.js Framework agnostic
Pricing model Per-seat + usage Generous free tier

For a static blog, Cloudflare’s free tier is essentially unlimited. And their edge network is genuinely global, not just “we have servers in some places.”

The Build Process

# That's the build command
npm run build

# Output
dist/
├── index.html
├── blog/
   ├── index.html
   ├── streaming-vs-batch/index.html
   └── ...
├── _astro/
   └── [hashed CSS and assets]
└── sitemap-index.xml

Build time: ~30 seconds. Output: static HTML files. Deployment: Cloudflare pulls from git, builds, deploys globally.

Image Handling

Images are served through Cloudflare Images:

  • Automatic format conversion: WebP, AVIF where supported
  • Responsive sizing: srcset generation
  • Global CDN delivery: Same edge network as the HTML
  • Lazy loading: Native loading="lazy" attribute

We upload once, Cloudflare handles the optimization.


Part X: The Numbers

Cost Breakdown

$0 Monthly Hosting
$0 Build Minutes
$5 Domain (yearly)
~$2 Images (monthly)

The entire infrastructure cost for The Trenches is approximately $7/year for the domain plus a few dollars monthly for Cloudflare Images.

No database hosting. No serverless function costs. No per-seat SaaS subscriptions. No bandwidth overages.

Performance Metrics

100 Lighthouse Performance
100 Lighthouse Accessibility
<1s Time to First Byte
~50KB Page Weight (typical)

These aren’t cherry-picked numbers. They’re what you get when you ship static HTML with minimal CSS and zero JavaScript.

The File Size Breakdown

CSS (all three files):     ~18KB uncompressed, ~4KB gzipped
HTML (typical article):    ~25KB uncompressed, ~6KB gzipped
JavaScript (progress bar): ~1KB
Fonts (subset):            ~80KB total
Images:                    Varies, but optimized

Total page weight for a text-heavy article: ~50KB. That’s smaller than a single hero image on most marketing sites.

Comparison With Alternatives

What would this cost on other platforms?

PlatformEstimated Monthly CostNotes
The Trenches (Astro + Cloudflare) ~$0.50 Just image hosting
Ghost Pro $9-25 Managed CMS
WordPress + Hosting $10-30 Shared hosting minimum
Webflow $14-39 CMS plans
Medium $5 (membership) Platform owns distribution

Tricks and Tips We Learned

Building this site taught us some things. Here are the most useful:

1. The CSS Reset You Actually Need

Don’t use a full normalize.css. Most of it is for legacy browser bugs. This is enough:

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

2. Anti-Aliasing for Fonts

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

This makes fonts render more consistently across browsers. The difference is subtle but professional.

When you click a TOC link, the browser scrolls to that heading. But if you have a sticky header, the heading gets hidden behind it. Fix:

h2, h3 {
  scroll-margin-top: calc(var(--header-height) + var(--space-8));
}

4. The Focus-Visible Trick

:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

:focus-visible only shows focus rings on keyboard navigation, not on mouse clicks. This keeps the visual design clean while maintaining accessibility.

5. System Color Scheme Meta Tag

<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />

This colors the browser chrome (address bar, etc.) to match your site. Small touch, big polish.

6. Passive Event Listeners

window.addEventListener('scroll', handler, { passive: true });

Adding passive: true to scroll/touch listeners tells the browser you won’t call preventDefault(). This allows the browser to start scrolling immediately without waiting for your JavaScript, improving perceived performance.


The Meta Conclusion

You’re reading an article about the blog you’re reading it on. There’s something circular about that, and it’s intentional.

This is what building in public looks like. Not just open-sourcing code, but explaining why. Not just sharing tools, but sharing the philosophy behind choosing them.

What We Learned

Building The Trenches reinforced some beliefs we already held:

  1. Static sites are underrated. For content-focused sites, the complexity of SSR, hydration, and client-side routing is usually unnecessary overhead.

  2. CSS frameworks are optional. A well-structured token system gives you consistency without the abstraction tax.

  3. Git is a better CMS than most CMSs. Version control, branching, pull requests, history: these are CMS features, and git does them better than any SaaS.

  4. Free tiers are generous now. Between Cloudflare, Vercel, Netlify, and others, hosting static sites costs essentially nothing. The barriers to publishing are lower than ever.

  5. Understanding beats convenience. We could have set this up faster with Tailwind and a CMS. But we’d understand it less. And when something breaks, understanding is what matters.

The Stack, Summarized

For those who skipped to the end:

  • Framework: Astro (static-first, zero JS by default)
  • Content: MDX with Content Collections
  • Styling: Custom CSS with design tokens (no framework)
  • Deployment: Cloudflare Pages (free tier)
  • Images: Cloudflare Images
  • Analytics: None (yet)
  • CMS: Git

Monthly cost: ~$0.50 (images only) Build time: ~30 seconds JavaScript shipped: ~1KB (progress bar only) Lighthouse score: 100/100/100/100


This article was written in MDX, rendered by Astro, styled with custom CSS tokens, and deployed to Cloudflare’s global edge network. It’s roughly 5,000 words, which at average reading speed is about 18 minutes. The irony of writing this much about writing is not lost on us. Total cost to publish: $0. Time from commit to live: ~2 minutes. That’s the trenches.