iToverDose/Software· 22 MAY 2026 · 08:07

Why tech teams switch from Substack to self-hosted newsletters

Moving a newsletter from a closed platform to an in-house Astro stack gave one team better SEO control, cleaner structured data, and a custom reader experience they couldn’t get elsewhere.

DEV Community4 min read0 Comments

A few months ago, a tech team swapped Substack for their own Astro-powered stack to publish a newsletter—and found the switch surprisingly rewarding. The move wasn’t about cost savings or audience reach; it was about ownership. In an era where technical SEO and reader experience define online success, a closed platform like Substack left too many variables out of their hands.

Ditching Substack for SEO clarity

Newsletters on Substack were functional, but the platform’s lack of transparency became a growing frustration. Sitemaps, canonical tags, and lastmod timestamps were inaccessible. URLs lived on a domain they didn’t control. When their main site, getbeton.ai, faced indexing issues, every unknown variable added friction to debugging.

Switching platforms removed a black box. By consolidating the newsletter and blog on their own domain, they gained full control over every header, tag, and metadata field—turning a technical bottleneck into a strategic advantage.

Building on the same Astro foundation

The new setup didn’t reinvent the wheel. It reused the existing Astro stack that powered getbeton.ai, complete with Tailwind design tokens and Vercel deployments. Each newsletter post is a markdown file in a content collection, rendered to static HTML at build time—no CMS, no database, just clean files.

Frontmatter in each post includes the title, description, a TL;DR, and an FAQ. These aren’t just reader-friendly elements; they’re also compiled into JSON-LD structured data automatically, eliminating the need for manual markup.

Two subtle but powerful details emerged from owning the renderer:

  • Every post has a raw-markdown twin at /blog/.md, making it easy for scripts or LLMs to read the original source instead of scraping HTML.
  • The sitemap’s lastmod value is pulled from Git history, not build time—ensuring Google sees accurate publication dates, not false “recent changes” signals.

Crafting a reading experience on your own terms

Once the team controlled the renderer, small improvements added up to a distinct reading experience. Sidenotes became a favorite feature: on desktop, footnotes float in the left margin next to the referenced line, mimicking the clarity of a well-designed print book. On mobile, they collapse into tappable references.

These aren’t custom components or heavy JavaScript—they’re plain HTML styled once in the prose stylesheet. They also double as subtle contextual calls-to-action where contextually relevant.

Every post now includes a TL;DR block at the top and an FAQ section at the bottom, both pulled from the same frontmatter fields used for structured data. This consistency improves readability and SEO without extra effort.

Typography and code formatting also became fully customizable. Monospace text renders exactly as intended, and code snippets use syntax highlighting chosen by the team—not dictated by a platform theme. For a publication focused on development tools, that level of control matters more than it might seem.

Ownership of the stack also simplified structured data. Since the TL;DR and FAQ live in frontmatter, embedding them into JSON-LD requires only a few lines of code at render time. The same source feeds both the page and the rich result—clean, maintainable, and future-proof.

None of this is baseline functionality. A closed platform offers a title, a body, and a subscribe box. Owning the stack means the reading experience is entirely yours to shape—and that, on its own, can justify the effort.

Email delivery with Resend

Newsletter delivery runs on Resend. Subscribers are managed through a Resend audience, with signups handled via a simple API endpoint on getbeton.ai. The sending domain is authenticated with SPF, DKIM, and DMARC, ensuring emails land in inboxes, not spam folders.

Each campaign is a small Node script that generates personalized email HTML, fetches subscribed contacts, and sends messages with a polite delay between deliveries. Every email includes a List-Unsubscribe header and a clear unsubscribe link, and the script automatically skips recipients who have already unsubscribed.

Tracking opens and clicks with PostHog

Losing open and click tracking was not an option. Rebuilding the analytics pipeline with PostHog kept those insights intact—and added precision.

Two lightweight endpoints handle the tracking:

  • A 1x1 tracking pixel records opens before serving a transparent GIF.
  • A click redirector wraps every link, logs the click event, appends UTM tags, and safely forwards the reader to the destination.

Both endpoints fire PostHog events—newsletter_opened and newsletter_link_clicked—tagged with campaign and recipient details. The open pixel is an Astro API route that returns a 1x1 GIF while asynchronously sending the event. Because the PostHog call is fire-and-forget, a slow response never blocks the image load.

// src/pages/api/track/pixel.png.ts
const PIXEL = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64'); // 1x1 gif

export const GET: APIRoute = async ({ url }) => {
  const campaign = url.searchParams.get('c') || 'unknown';
  const email = url.searchParams.get('e') || 'anonymous';
  
  await fetch(`${POSTHOG_HOST}/capture/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      api_key: POSTHOG_KEY,
      event: 'newsletter_opened',
      distinct_id: email,
      properties: { campaign, source: 'email' },
    }),
  }).catch(() => {});

  return new Response(PIXEL, {
    headers: { 'Content-Type': 'image/gif', 'Cache-Control': 'no-store' },
  });
};

The click redirector does the same, with one critical safeguard: it only redirects to trusted hosts. A naive redirector could become a phishing vector with your domain attached. Only approved domains are permitted, ensuring security without complexity.

// src/pages/api/track/click.ts
const ALLOWED_HOSTS = [
  'getbeton.ai',
  'www.getbeton.ai',
  'inspector.getbeton.ai',
  'github.com'
];

export const GET: APIRoute = async ({ url }) => {
  const target = url.searchParams.get('u') || '
  const campaign = url.searchParams.get('c') || 'unknown';
  const label = url.searchParams.get('l') || 'unknown';
  const email = url.searchParams.get('e') || 'anonymous';
  
  const parsed = new URL(target);
  if (!ALLOWED_HOSTS.some(h => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`))) {
    return new Response('Invalid host', { status: 400 });
  }

  await fetch(`${POSTHOG_HOST}/capture/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      api_key: POSTHOG_KEY,
      event: 'newsletter_link_clicked',
      distinct_id: email,
      properties: { campaign, label },
    }),
  }).catch(() => {});

  return Response.redirect(target, 302);
};

Own your stack, own the experience—and own the data that drives it forward.

AI summary

Learn how one tech team moved from Substack to an Astro-powered stack for better SEO, cleaner structured data, and a custom reader experience without sacrificing analytics.

Comments

00
LEAVE A COMMENT
ID #LOJI6X

0 / 1200 CHARACTERS

Human check

6 + 5 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.