<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ted</title>
    <description>The latest articles on DEV Community by Ted (@henry_dan_81513dd35a2f540).</description>
    <link>https://dev.to/henry_dan_81513dd35a2f540</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2256153%2Fd3b27e5a-9e82-4c4d-b481-9b835d4deea3.png</url>
      <title>DEV Community: Ted</title>
      <link>https://dev.to/henry_dan_81513dd35a2f540</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henry_dan_81513dd35a2f540"/>
    <language>en</language>
    <item>
      <title>Building an Astro Blog with Claude Code</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Tue, 19 May 2026 15:42:09 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/building-an-astro-blog-with-claude-code-4cjb</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/building-an-astro-blog-with-claude-code-4cjb</guid>
      <description>&lt;p&gt;In &lt;a href="https://tedagentic.com/posts/why-astro-over-lovable" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; I explained why I moved from Lovable.dev to Astro for SEO. Lovable builds React apps that serve empty HTML to Googlebot — Astro builds static HTML that Google can read instantly, no JavaScript required.&lt;/p&gt;

&lt;p&gt;This post is part 2: actually building the blog. But instead of walking you through every command manually, I handed it to Claude.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt
&lt;/h2&gt;

&lt;p&gt;One prompt. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build me an SEO-ready Astro blog from scratch with Tailwind, MDX, RSS feed, sitemap, and fonts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that, Claude installed everything, built the full project structure, and had a working blog ready to deploy. Here's exactly what happened — including the decisions it made that I didn't ask for, and where it hit problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Installed
&lt;/h2&gt;

&lt;p&gt;Claude ran the following commands in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create astro@latest astro-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template: &lt;strong&gt;Empty&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript: &lt;strong&gt;Yes, strict&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Install dependencies: &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Initialize git repo: &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then added the integrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx astro add tailwind
npx astro add mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fonts and RSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @fontsource-variable/inter @fontsource/jetbrains-mono
npm &lt;span class="nb"&gt;install&lt;/span&gt; @astrojs/rss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing it caught that I didn't ask about: it skipped &lt;code&gt;@astrojs/sitemap&lt;/code&gt; entirely. That integration crashes on Astro 4.16+ with a &lt;code&gt;Cannot read properties of undefined (reading 'reduce')&lt;/code&gt; error. Claude knew this and built a manual sitemap endpoint at &lt;code&gt;src/pages/sitemap.xml.ts&lt;/code&gt; instead. That's the kind of thing you only know if you've hit the bug — or in this case, if your agent has enough context to anticipate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Built
&lt;/h2&gt;

&lt;p&gt;The full project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── components/
│   ├── Header.astro
│   ├── PostList.astro
│   └── SeriesNav.astro
├── content/
│   └── posts/        # .md/.mdx files go here
├── layouts/
│   └── BaseLayout.astro
├── pages/
│   ├── index.astro
│   ├── about.astro
│   ├── archive.astro
│   ├── posts/[slug].astro
│   ├── tags/[tag].astro
│   ├── categories/[category].astro
│   ├── series/[series].astro
│   ├── rss.xml.ts
│   └── sitemap.xml.ts
└── styles/
    └── globals.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scope here is worth noting. I asked for a blog. Claude built a full content taxonomy — tags, categories, series navigation, archive page, RSS feed, sitemap. Every route it created serves a purpose for either discoverability or SEO. Tag and category pages in particular matter for topic clustering: they give Google a signal that this site covers a coherent subject area, not just a loose collection of posts, which helps individual posts inherit topical authority over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEO Decisions Baked Into BaseLayout
&lt;/h2&gt;

&lt;p&gt;This is the part most tutorials skip. The layout file is where most of your SEO either works or doesn't.&lt;/p&gt;

&lt;p&gt;Claude built &lt;code&gt;BaseLayout.astro&lt;/code&gt; with the following automatically wired up for every page:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical URL&lt;/strong&gt; — generated from the site URL and current path. Every page declares its own canonical, which prevents duplicate content issues if Vercel's preview URLs ever get indexed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open Graph tags&lt;/strong&gt; — &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;og:type&lt;/code&gt;. These control how the page appears when shared on Twitter, LinkedIn, or Slack. Claude wired them to the same frontmatter fields as the page meta, so there's no separate OG configuration needed per post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Twitter Card&lt;/strong&gt; — &lt;code&gt;summary_large_image&lt;/code&gt; by default, so post shares show a full image preview rather than a small thumbnail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD structured data&lt;/strong&gt; — three schemas per post page: &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;, and &lt;code&gt;BreadcrumbList&lt;/code&gt;. These are what Google uses to understand the page type, authorship, and navigation hierarchy. On a CSR site this has to load via JavaScript and might not be seen by crawlers. On Astro it's in the HTML before any JavaScript runs.&lt;/p&gt;

&lt;p&gt;All of this is automatic. Drop a new post file in &lt;code&gt;src/content/posts/&lt;/code&gt;, push, and every one of those meta tags is generated correctly with no manual input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Content Schema
&lt;/h2&gt;

&lt;p&gt;Claude defined the content schema in &lt;code&gt;src/content/config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;publishDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;updatedDate&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guides&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;builds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;imageAlt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;series&lt;/code&gt; and &lt;code&gt;part&lt;/code&gt; fields were something I asked for specifically — I knew I'd want to group related posts. Claude built &lt;code&gt;SeriesNav.astro&lt;/code&gt; to handle the series UI automatically: previous/next links within a series, plus a link back to the series index page. Posts without &lt;code&gt;series&lt;/code&gt; in their frontmatter render as standalone, no extra configuration needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Hit Problems
&lt;/h2&gt;

&lt;p&gt;It wasn't perfect. Two things needed manual fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vercel deploy config.&lt;/strong&gt; The first deploy failed because Vercel defaulted to the wrong output directory. Claude had to add a &lt;code&gt;vercel.json&lt;/code&gt; file to the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDirectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"framework"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that it deployed cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The GitHub push from the server.&lt;/strong&gt; Claude Code runs on my homeserver, not my local machine. To push to GitHub from the server, the git remote needs a personal access token embedded in the URL — standard HTTPS auth doesn't work in a headless environment. Claude flagged this and set the remote correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote set-url origin https://TOKEN@github.com/username/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One security note: treat this token like a password. Don't commit it, don't leave it in shell history. Either use a fine-grained GitHub token scoped to the single repo, or switch to SSH keys if you're on a machine you control long-term.&lt;/p&gt;

&lt;p&gt;Not a Claude Code limitation — just a server environment reality. Worth knowing if you're running agents on a remote machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push to GitHub and Deploy
&lt;/h2&gt;

&lt;p&gt;Once the remote was configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial blog build"&lt;/span&gt;
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on Vercel: Add New Project → import the repo → Deploy. Vercel detects Astro automatically but needs one config file to correctly set the output directory — that's the &lt;code&gt;vercel.json&lt;/code&gt; above. Once it's there, no further build configuration is needed.&lt;/p&gt;

&lt;p&gt;Within two minutes the blog was live at a Vercel URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A fully working, SEO-ready Astro blog — deployed on Vercel, with tags, categories, series navigation, RSS, sitemap, JSON-LD schema, Open Graph, and canonical URLs — built from a single prompt in one session. The full build ran in just over 7 minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Demo video coming soon.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://astro-demo-theta-tawny.vercel.app" rel="noopener noreferrer"&gt;astro-demo-theta-tawny.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Demonstrates
&lt;/h2&gt;

&lt;p&gt;The point isn't that Claude wrote the code. It's that agentic workflows collapse the gap between a decision and a working system.&lt;/p&gt;

&lt;p&gt;The traditional path for this project: read the Astro docs, pick a template, configure Tailwind, wire up MDX, figure out why &lt;code&gt;@astrojs/sitemap&lt;/code&gt; crashes, build the content schema, write the base layout, set up JSON-LD manually, troubleshoot the Vercel config, push. Half a day minimum, probably longer.&lt;/p&gt;

&lt;p&gt;The agentic path: one prompt, review the output, fix two issues, ship.&lt;/p&gt;

&lt;p&gt;The output is the same. The time investment is not.&lt;/p&gt;

&lt;p&gt;That's what this blog documents — not AI as a novelty, but AI as operational infrastructure. The blog itself was built the same way the systems it writes about are built: give the agent a clear goal, stay in the loop on decisions that matter, and let it handle the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the SEO Foundation Matters More Than the Framework
&lt;/h2&gt;

&lt;p&gt;A lot of people building blogs with AI tools focus on the wrong thing. They want the blog to look good, deploy fast, and be easy to write in. Those matter. But the SEO foundation — the stuff that determines whether Google can read and understand your content — is decided before you write a single post.&lt;/p&gt;

&lt;p&gt;Here's what this build gets right by default:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every page is crawlable on the first request.&lt;/strong&gt; No JavaScript rendering required. Googlebot hits the URL, gets complete HTML, indexes it. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data is present and correct on every post.&lt;/strong&gt; The JSON-LD schema Claude wired into &lt;code&gt;BaseLayout.astro&lt;/code&gt; tells Google this is an Article, who authored it, when it was published, and where it sits in the site hierarchy. This is what enables rich results in SERPs — date stamps, breadcrumbs, author attribution. On a manually-built blog these are easy to get wrong or forget entirely. Here they're generated automatically from frontmatter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sitemap updates on every deploy.&lt;/strong&gt; The custom &lt;code&gt;sitemap.xml.ts&lt;/code&gt; endpoint queries the content collection and generates a fresh sitemap at build time. Every new post is automatically included with its correct URL and publish date. You never manually update a sitemap or worry about a new post being missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RSS keeps returning readers engaged.&lt;/strong&gt; The RSS feed at &lt;code&gt;/rss.xml&lt;/code&gt; is a low-effort retention mechanism. Readers who subscribe get notified of new posts without you needing to send an email or post on social. For a technical blog, a meaningful chunk of the audience prefers RSS — it's worth having from day one.&lt;/p&gt;

&lt;p&gt;None of this required separate configuration. It was all handled in the initial build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write Your First Post
&lt;/h2&gt;

&lt;p&gt;Drop a &lt;code&gt;.md&lt;/code&gt; file in &lt;code&gt;src/content/posts/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;First&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Post"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;short&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Google."&lt;/span&gt;
&lt;span class="na"&gt;publishDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-27"&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;astro"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seo"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guides"&lt;/span&gt;
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Your content here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push to GitHub. Vercel redeploys automatically. Done.&lt;/p&gt;




&lt;p&gt;The build is the easy part. The harder question is what you do next — how you monitor whether Google is actually finding and indexing your posts, and what to do when something goes wrong. That's what the rest of this series covers. Browse all posts in the &lt;a href="https://tedagentic.com/series/astro-seo-blog" rel="noopener noreferrer"&gt;Astro SEO Blog&lt;/a&gt; series.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>claudeai</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Astro vs Lovable for SEO: Why Static Sites Win</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Tue, 19 May 2026 15:18:29 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/astro-vs-lovable-for-seo-why-static-sites-win-3kgn</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/astro-vs-lovable-for-seo-why-static-sites-win-3kgn</guid>
      <description>&lt;p&gt;My monitoring script fired at 3am. A content site I manage — the top-ranking page on that domain, the one its entire internal linking structure pointed to — had dropped out of the Google index entirely.&lt;/p&gt;

&lt;p&gt;Not penalized. Not a manual action. Just gone — as if it never existed.&lt;/p&gt;

&lt;p&gt;The diagnosis was quick. The site ran on a CSR stack — React, client-side rendered. Googlebot had visited, got a near-empty HTML shell, and moved on before JavaScript had a chance to build the DOM. At some point Google stopped waiting and dropped the page entirely.&lt;/p&gt;

&lt;p&gt;The fix took less than an hour. Within a day or two it was back — and when it reappeared, it had moved from position 3 to position 2. Fixing the rendering issue properly actually moved it up.&lt;/p&gt;

&lt;p&gt;That's the thing about CSR: it doesn't just risk a penalty. It quietly suppresses pages that should be ranking, and you won't know until they disappear.&lt;/p&gt;

&lt;p&gt;The fix was a custom prerender layer — a static HTML snippet injected directly into the React root div under the id &lt;code&gt;ssr-prerender&lt;/code&gt;. Googlebot sees real content immediately. Real users get the React app as normal, which replaces it on load. It works. But it's a patch, not a solution — something you bolt on after the fact, maintain separately, and have to remember every time you add a new page or change content.&lt;/p&gt;

&lt;p&gt;Client-side rendering means the server returns an empty HTML shell. The browser runs JavaScript to build the page. That works for users. It's unreliable for crawlers — and the cost doesn't show up immediately. It shows up weeks later when pages quietly stop ranking.&lt;/p&gt;

&lt;p&gt;Lovable is built for apps, not blogs. But plenty of people use it for content sites anyway — that's the mistake this post is about.&lt;/p&gt;

&lt;p&gt;When I decided to build this blog — a place to document agentic AI workflows that I actually want people to find — I wasn't going to patch my way through it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tools I Looked At
&lt;/h2&gt;

&lt;p&gt;Before landing on a stack, I mapped out the real options. Not just "what's popular" but what the actual tradeoffs are for a content site in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lovable
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpaax5lsejt5d7cafl8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpaax5lsejt5d7cafl8l.png" alt="Lovable.dev interface showing a prompt-to-app builder" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lovable is a prompt-to-app builder. You describe what you want, it generates a React codebase, you deploy. The UX is genuinely impressive for web apps — forms, dashboards, interactive tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (May 2026):&lt;/strong&gt; Lovable shipped SSR as a first-class feature. New apps are now server-side rendered — Googlebot gets real HTML on the first request, the crawlability problem is gone for new projects. Existing apps get automatic pre-rendering at build time, which fixes the indexability issue without a DIY script.&lt;/p&gt;

&lt;p&gt;The performance gap with SSG still exists. New Lovable apps ship a full React bundle and add server latency to every request — a static site serves pre-built HTML from a CDN edge node with no per-request compute. For a pure content blog where every page is the same for every visitor and LCP matters, SSG is still the faster path. But the hard wall that made Lovable the wrong choice for organic search is no longer there for new projects.&lt;/p&gt;

&lt;p&gt;I still use it for clients who need web apps or interactive content. For a pure content site with no dynamic requirements, the SSG argument below still holds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Cursor is a VSCode fork with an embedded AI assistant — excellent for developers who want faster coding without giving up control. It doesn't pick a framework or manage deployments, so the SEO tradeoffs are entirely yours to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Use
&lt;/h2&gt;

&lt;p&gt;My setup is deliberately boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ThinkCentre PC, bare metal Ubuntu, no cloud VM&lt;/li&gt;
&lt;li&gt;Claude Code over SSH as the agent&lt;/li&gt;
&lt;li&gt;Astro 4 — SSG, zero client JS by default&lt;/li&gt;
&lt;li&gt;Vercel for deploy, auto-deploys on every push to master&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code handles file editing, refactoring, and multi-step builds. I review diffs, approve writes, and push. The agent runs in my terminal — no subscription to a hosted IDE, no vendor controlling my environment.&lt;/p&gt;

&lt;p&gt;Astro was the obvious framework choice once I ruled out React-based options. Every page is static HTML at build time. No JavaScript ships to the browser unless you explicitly add it. Googlebot sees exactly what a user sees, because there's no rendering step to skip.&lt;/p&gt;

&lt;p&gt;The content schema is straightforward MDX files in &lt;code&gt;src/content/posts/&lt;/code&gt;. Writing a new post is dropping a Markdown file and pushing. No CMS, no API calls, no paid tier. The overhead is real but front-loaded — you configure image optimization, redirects, and the content schema once. After that it doesn't move. That's a different kind of maintenance than patching a prerender layer every time you add a page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F802lo9xzki7t3xkowdx2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F802lo9xzki7t3xkowdx2.png" alt="Vercel deploy error during blog setup" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Astro vs Lovable for SEO: The Real Comparison
&lt;/h2&gt;

&lt;p&gt;This is the question that matters if you're using AI-assisted tools to build content sites.&lt;/p&gt;

&lt;p&gt;Lovable is built for speed — you describe a UI, it ships React. That's genuinely useful for products, dashboards, anything interactive. But for a content site where organic search is the point, React's default rendering model is the wrong starting point.&lt;/p&gt;

&lt;p&gt;Googlebot has a two-wave crawl model. First wave: fetch the raw HTML. Second wave: render JavaScript. The second wave happens on a delay — sometimes hours, sometimes days — and isn't guaranteed for every page on every crawl. For a new site or a page with low crawl budget, Google may only ever see the first wave. On a CSR site, that first wave is an empty shell.&lt;/p&gt;

&lt;p&gt;Astro doesn't have this problem. The HTML is complete at build time — every page is just a file on disk, served directly. Googlebot gets the same document a user gets, on the first request, every time. An Astro page can be indexed the same day it's published. A Lovable page might take two weeks and a manual fetch request in Search Console to force the second render wave.&lt;/p&gt;

&lt;p&gt;For a blog where every post is a potential traffic source, that's not a minor difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEO Tradeoff That Actually Matters
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody says clearly: &lt;strong&gt;your platform choice is an SEO decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every framework has a default rendering strategy. If that default is client-side, you're opting into a crawl risk. You can mitigate it, but you can't fully eliminate it — and every mitigation adds operational overhead.&lt;/p&gt;

&lt;p&gt;To be precise: the risk is probabilistic, not guaranteed. High-authority domains with strong crawl frequency and deep internal linking can rank fine on CSR — Google's renderer has improved significantly, and a well-established site gives Googlebot reason to come back for the second wave. The risk is highest where the stakes are also highest: new sites, low crawl budget pages, and content that needs to index fast to catch a trend or a seasonal window. That's exactly where most content blogs live.&lt;/p&gt;

&lt;p&gt;Static site generators like Astro, Eleventy, and Hugo don't have this problem. The HTML is there at request time, before any crawler or user arrives. There's nothing to render, nothing to wait for, nothing to patch around.&lt;/p&gt;

&lt;p&gt;Beyond crawlability, static sites have compounding SEO advantages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Astro ships zero JavaScript by default — the browser gets a finished HTML file and paints immediately. On a React CSR site, the browser downloads a JS bundle, parses it, executes it, then renders. That sequence adds hundreds of milliseconds to LCP on a cold load, and LCP is a Core Web Vitals signal Google uses as a ranking factor. My Astro blog consistently scores 95+ on PageSpeed Insights mobile. My client sites on CSR stacks rarely break 70 without active optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data.&lt;/strong&gt; In Astro, JSON-LD schema lives in the base layout — defined once, present on every page at build time, no runtime dependency. On a CSR site, your structured data is injected by JavaScript. If the script errors, is blocked, or executes after Googlebot's render window closes, your schema is invisible. You won't see this failure in GSC until it's already cost you rich results.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice — the relevant slice of an Astro base layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
const { title, description, canonicalURL } = Astro.props;
const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": title,
  "description": description,
  "url": canonicalURL,
};
---
&amp;lt;head&amp;gt;
  &amp;lt;link rel="canonical" href={canonicalURL} /&amp;gt;
  &amp;lt;script type="application/ld+json" set:html={JSON.stringify(schema)} /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canonical and structured data are rendered into the HTML at build time — no hydration, no runtime dependency, no silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical URLs.&lt;/strong&gt; Static generation makes canonicals deterministic. Every page has one URL, set at build time, baked into the HTML. CSR frameworks with dynamic routing can silently generate duplicate URLs under different query parameters — &lt;code&gt;/posts/my-article&lt;/code&gt; and &lt;code&gt;/posts/my-article?ref=homepage&lt;/code&gt; both return 200s, both get crawled, and both dilute the canonical signal unless you've explicitly handled it. With Astro, this class of problem doesn't exist.&lt;/p&gt;

&lt;p&gt;For a content blog that needs to rank, none of this is optional. It's the baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Middle Path
&lt;/h2&gt;

&lt;p&gt;Astro isn't the only way out of CSR. It's worth naming the alternatives so this doesn't read as "Astro or nothing."&lt;/p&gt;

&lt;p&gt;Next.js gives you two paths to HTML-first rendering. &lt;code&gt;output: 'export'&lt;/code&gt; is a full static export — no server features, no API routes, no ISR, just HTML files on disk with the same crawlability as Astro. &lt;code&gt;getStaticProps&lt;/code&gt; is SSG inside a hybrid Next.js app that still has server routes, API endpoints, and ISR — more flexible, but you're choosing the rendering strategy per page deliberately rather than accepting a default. If you're already comfortable in Next.js, either path works. Astro islands (partial hydration) let you drop interactive React or Svelte components into otherwise static pages without shipping a full JS bundle. For a content site that needs one or two dynamic widgets, that's a clean solution.&lt;/p&gt;

&lt;p&gt;The principle is the same regardless of which tool you pick: &lt;strong&gt;HTML-first by default, JavaScript added deliberately.&lt;/strong&gt; Astro makes that the default. Next.js makes it possible but requires you to choose it. Lovable doesn't give you the option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Tell Someone Starting Now
&lt;/h2&gt;

&lt;p&gt;If you're building a web app — a tool, a dashboard, something with real interactivity — Lovable is a legitimate choice. Ship fast, iterate, worry about SEO when the product is solid.&lt;/p&gt;

&lt;p&gt;If you're building a content site — a blog, a documentation site, a resource hub where organic traffic is the point — don't use a CSR framework as your foundation. The crawl risk is real, the fixes are patches, and you'll spend time maintaining workarounds that a static site simply doesn't need.&lt;/p&gt;

&lt;p&gt;One honest limit worth naming: pure SSG pre-builds every page identically at deploy time. That works perfectly for content. It gets awkward when every page needs to render differently per user — a fully personalised feed, for example. For that you switch Astro to &lt;code&gt;output: 'hybrid'&lt;/code&gt; or &lt;code&gt;output: 'server'&lt;/code&gt; mode, which enables server-side rendering on specific routes while keeping everything else static. Auth and Supabase integration work fine in Astro either way — login flows, protected routes via middleware, user dashboards as islands. The constraint isn't "you can't do auth," it's "fully personalised page-level content at scale wants SSR, not SSG."&lt;/p&gt;

&lt;p&gt;Astro is free, deploys to Vercel in one click, and handles all the SEO fundamentals out of the box. The learning curve is light if you know HTML and basic JavaScript. It's not the only answer — but for a new content site where every page needs to index fast, it's the lowest-friction path to getting that right.&lt;/p&gt;

&lt;p&gt;Read &lt;a href="https://tedagentic.com/posts/claude-built-my-astro-blog" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; to see how I built this blog in a single Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 3 covers the Lovable SSR update in detail — what pre-rendering for existing apps actually fixes, what it doesn't, and when the Astro argument still holds: &lt;a href="https://tedagentic.com/posts/lovable-ssr-update" rel="noopener noreferrer"&gt;Lovable Shipped SSR. Here's What That Actually Changes.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
