<?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: Aulvem</title>
    <description>The latest articles on DEV Community by Aulvem (@aulvem).</description>
    <link>https://dev.to/aulvem</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%2F3937207%2F28940f06-b424-4a8e-b6f4-29de6af4bec4.png</url>
      <title>DEV Community: Aulvem</title>
      <link>https://dev.to/aulvem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aulvem"/>
    <language>en</language>
    <item>
      <title>How I run a small blog on Astro 5 + Content Collections</title>
      <dc:creator>Aulvem</dc:creator>
      <pubDate>Mon, 25 May 2026 14:16:46 +0000</pubDate>
      <link>https://dev.to/aulvem/how-i-run-a-small-blog-on-astro-5-content-collections-4m3i</link>
      <guid>https://dev.to/aulvem/how-i-run-a-small-blog-on-astro-5-content-collections-4m3i</guid>
      <description>&lt;p&gt;I run a small blog (&lt;a href="https://aulvem.com" rel="noopener noreferrer"&gt;aulvem.com&lt;/a&gt;) on Astro 5 + MDX + Content Collections, hosted statically on Cloudflare Pages. The interesting part isn't the stack — it's the operational rules I lean on the schema to enforce so that a writer (me) can't ship something half-broken.&lt;/p&gt;

&lt;p&gt;This post is a short tour of that setup: which packages I keep, which dependencies I deliberately don't add, and the three build-time checks that hold the writing flow together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, in eight runtime packages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.json (runtime deps, abridged)&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;span class="nl"&gt;"astro"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@astrojs/mdx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@astrojs/sitemap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@astrojs/rss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@astrojs/tailwind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rehype-external-links"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rehype-mermaid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.4"&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;Dev deps: &lt;code&gt;pagefind&lt;/code&gt; (full-text search), &lt;code&gt;sharp&lt;/code&gt; (local image processing), &lt;code&gt;playwright&lt;/code&gt; (build-time SVG render for mermaid), &lt;code&gt;typescript&lt;/code&gt;, &lt;code&gt;@types/node&lt;/code&gt;. No React. No Vue. No Vite plugins.&lt;/p&gt;

&lt;p&gt;The rule I started with: don't add a dependency on the hope it'll be useful later. Anything that doesn't have a written-down use case stays out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three build-time rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;category: reviews&lt;/code&gt; ⇔ &lt;code&gt;affiliate: true&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A post in the &lt;code&gt;reviews&lt;/code&gt; category is, by ad-network rules, advertising. It has to carry a disclosure banner and &lt;code&gt;rel="sponsored"&lt;/code&gt; on outbound links. Both of those are injected by rehype plugins gated on &lt;code&gt;affiliate: true&lt;/code&gt;. So the worst-case failure mode is publishing a review post with &lt;code&gt;affiliate&lt;/code&gt; left at its default — disclosure missing, sponsored rel missing.&lt;/p&gt;

&lt;p&gt;The Zod schema couples them with one &lt;code&gt;.refine()&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reviews&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;affiliate must be true iff category is 'reviews'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;affiliate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides compared with &lt;code&gt;===&lt;/code&gt;. Change only one and the build fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Typed structured data in frontmatter
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;HowTo&lt;/code&gt; and &lt;code&gt;FAQPage&lt;/code&gt; JSON-LD blocks pull from frontmatter rather than from parsed body text. Reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heading renames don't quietly break JSON-LD&lt;/li&gt;
&lt;li&gt;Zod validates the shape, so missing &lt;code&gt;answer&lt;/code&gt; fields are caught at build&lt;/li&gt;
&lt;li&gt;The JSON-LD generator can trust frontmatter without touching MDX
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;faq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Why&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Astro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5?"&lt;/span&gt;
    &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Markdown-centric,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;static-only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;output,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;schema-typed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frontmatter,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;core&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Astro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fits&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;four."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;lastmod&lt;/code&gt; from frontmatter
&lt;/h3&gt;

&lt;p&gt;Astro's official sitemap integration doesn't read &lt;code&gt;updatedDate&lt;/code&gt; from MDX frontmatter. Default &lt;code&gt;lastmod&lt;/code&gt; is the build time, which broadcasts "every post updated on every build" to search engines and AI search.&lt;/p&gt;

&lt;p&gt;I walk the collection at build time and pipe &lt;code&gt;updatedDate ?? pubDate&lt;/code&gt; into the sitemap entry as &lt;code&gt;lastmod&lt;/code&gt;. Paginated &lt;code&gt;noindex&lt;/code&gt; pages get dropped from the sitemap in the same pass — submitting them through the sitemap is a contradictory signal otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single source of truth for operational flow
&lt;/h2&gt;

&lt;p&gt;Adding a post, retiring a post, updating product pages — all of these are anchored in one doc (&lt;code&gt;docs/content-flow.md&lt;/code&gt;), and the scaffolding scripts pull from it. Same rule every time. That absorbs most of the "the approach drifts run to run" variance, which is the failure mode that gets me when I haven't touched the project in a few weeks.&lt;/p&gt;




&lt;p&gt;The full version with the decision history, what I dropped, and where Zod can't reach lives on Aulvem → &lt;a href="https://aulvem.com/blog/2026-05-17-aulvem-blog-architecture/" rel="noopener noreferrer"&gt;How this blog is built — Aulvem on Astro 5 and Content Collections&lt;/a&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
