<?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: Ranjeet Kumar Jena</title>
    <description>The latest articles on DEV Community by Ranjeet Kumar Jena (@ranjeet_kumarjena_4a6b1b).</description>
    <link>https://dev.to/ranjeet_kumarjena_4a6b1b</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4082656%2F362c530d-9c4a-4ca0-a07f-352e62fc8272.jpg</url>
      <title>DEV Community: Ranjeet Kumar Jena</title>
      <link>https://dev.to/ranjeet_kumarjena_4a6b1b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ranjeet_kumarjena_4a6b1b"/>
    <language>en</language>
    <item>
      <title>What SEO Should a Modern Web Framework Handle Automatically?</title>
      <dc:creator>Ranjeet Kumar Jena</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:38:50 +0000</pubDate>
      <link>https://dev.to/ranjeet_kumarjena_4a6b1b/what-seo-should-a-modern-web-framework-handle-automatically-45be</link>
      <guid>https://dev.to/ranjeet_kumarjena_4a6b1b/what-seo-should-a-modern-web-framework-handle-automatically-45be</guid>
      <description>&lt;p&gt;&lt;em&gt;21 August 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every framework has an SEO story now, and almost all of them are the same story: a helper that writes &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags. Next has Metadata, Nuxt has &lt;code&gt;useHead&lt;/code&gt;, Astro has whatever you import this month. They are fine. They are also the least interesting part of the problem, because writing a &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; was never what broke anyone's search traffic.&lt;/p&gt;

&lt;p&gt;The interesting question is a different one: &lt;strong&gt;what does the framework do when you do nothing at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A metadata API is opt-in by definition. It helps on the pages you remembered. The failures that actually cost traffic are the ones nobody remembered, because nothing errored — a 200 status on a page that doesn't exist, a canonical URL pointing at &lt;code&gt;http://&lt;/code&gt;, a link to a page the build silently skipped. None of those throw. None of them show up in a test. They show up in Search Console eleven weeks later.&lt;/p&gt;

&lt;p&gt;So here is a better test for a framework's SEO story:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a competent developer builds a content site and never once thinks about search engines, what is already correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want to work through the categories that answer that question, using &lt;a href="https://github.com/stoneware-dev/stoneware-core" rel="noopener noreferrer"&gt;Stoneware&lt;/a&gt; as the worked example — partly because I built it, and mostly because its defaults were chosen against exactly this test. Every code sample and every output below is real.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The document has to be complete in the first response
&lt;/h2&gt;

&lt;p&gt;This is the foundation, and it is upstream of everything else. If your content is assembled on the client, then everything downstream — canonical tags, structured data, sitemaps — is decoration on a page a crawler may or may not fully see.&lt;/p&gt;

&lt;p&gt;The honest version of this claim is measurable, not rhetorical. Here is a 21-route content site, built three ways from byte-identical content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     JS on an article page    pages with zero JS
  ─────────────────────────────────────────────────────────────
  Stoneware                        0 B                20 of 21
  Astro                            0 B                20 of 21
  Next.js (App Router)          576 KB                 0 of 21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stoneware and Astro send a document and nothing else. Next sends 576 KB of JavaScript to a page with no interactive element on it, and re-encodes the article a second time inside the HTML as an RSC payload.&lt;/p&gt;

&lt;p&gt;The test you can run yourself takes one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://your-site.example/some-article | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"a sentence from the article"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that finds nothing, no amount of metadata will help you.&lt;/p&gt;

&lt;p&gt;Stoneware's rule here is structural rather than advisory: a file under &lt;code&gt;routes/&lt;/code&gt; is never handed to the bundler, so it &lt;em&gt;cannot&lt;/em&gt; reach the client. Interactivity is opt-in per directory — a component under &lt;code&gt;islands/&lt;/code&gt; hydrates, everything else is a string the server produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; rendering to complete HTML by default, with client JavaScript as the exception you ask for.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Status codes, and the soft 404
&lt;/h2&gt;

&lt;p&gt;This is the most common indexing bug on content sites and almost nobody talks about it.&lt;/p&gt;

&lt;p&gt;A dynamic route matches &lt;em&gt;any&lt;/em&gt; slug. &lt;code&gt;/blog/[slug]&lt;/code&gt; happily matches &lt;code&gt;/blog/asdfghjkl&lt;/code&gt;. Your template then looks up the post, doesn't find it, and — if you're not careful — renders "Post not found" with a &lt;strong&gt;200 OK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That page is now indexable. Google will crawl it, and it may index it. Worse, the pattern generates infinitely many of them.&lt;/p&gt;

&lt;p&gt;The fix has to be ergonomic or people won't use it. In Stoneware it's one call that throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PageProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stoneware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;PageProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// real 404, with your error page rendered into it&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it throws rather than returns, it works from a helper three calls deep without every function in between having to pass a sentinel back up. And because its return type is &lt;code&gt;never&lt;/code&gt;, TypeScript narrows &lt;code&gt;post&lt;/code&gt; to present afterwards — no non-null assertion.&lt;/p&gt;

&lt;p&gt;Here is what the framework answers without being asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  /no-such-page      404      the _404 page, Cache-Control: no-store
  /_404              404      a convention, never servable as a page
  notFound()         404      your _404 page, correct status
  a thrown error     500      the _500 page, no-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth stealing regardless of framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error responses are &lt;code&gt;no-store&lt;/code&gt;.&lt;/strong&gt; A 404 cached by a CDN outlives the deploy that adds the missing page. That's a self-inflicted outage with a long tail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A leading underscore means a file isn't servable.&lt;/strong&gt; Without that rule, &lt;code&gt;routes/_404.tsx&lt;/code&gt; would answer a real request at &lt;code&gt;/_404&lt;/code&gt; with a 200 — your error page, indexable as content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; making the correct status the easy one, and never serving its own conventions as pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Canonical URLs that survive a reverse proxy
&lt;/h2&gt;

&lt;p&gt;This one is invisible locally and bites almost every production deployment.&lt;/p&gt;

&lt;p&gt;Every platform that terminates TLS — Render, Railway, Fly, Vercel, nginx — forwards a &lt;strong&gt;plain HTTP&lt;/strong&gt; request to your app. So &lt;code&gt;new URL(request.url)&lt;/code&gt; reports &lt;code&gt;http://&lt;/code&gt; for a site served over &lt;code&gt;https://&lt;/code&gt;. Anything absolute you build from it is now wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; pointing at &lt;code&gt;http://&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;og:image&lt;/code&gt; on the insecure origin&lt;/li&gt;
&lt;li&gt;sitemap entries on the wrong scheme&lt;/li&gt;
&lt;li&gt;OAuth redirect URIs that don't match&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A canonical tag pointing at &lt;code&gt;http://&lt;/code&gt; tells a crawler that your &lt;code&gt;https://&lt;/code&gt; page is a duplicate of a page that redirects. Nothing errors. The site looks perfect to you.&lt;/p&gt;

&lt;p&gt;The framework can't just trust the forwarded headers — they're trivially forged by anyone who can reach your app directly, and a spoofed &lt;code&gt;X-Forwarded-Host&lt;/code&gt; poisons every absolute URL you emit, including password-reset links. So it has to be a decision, not a default:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;trustProxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// or STONEWARE_TRUST_PROXY in the environment&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"proto"&lt;/code&gt; honours the forwarded scheme only — safe on any host, and enough to fix the common case. &lt;code&gt;true&lt;/code&gt; also honours the forwarded host, which requires a proxy you actually control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; giving routes a URL that is already the public one, and making the proxy question a single explicit setting rather than something each developer rediscovers.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The metadata API — the part everyone builds
&lt;/h2&gt;

&lt;p&gt;This is table stakes, so I'll be brief. The value isn't in emitting tags; it's in the mistakes the API makes impossible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Java Quiz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Practice Java questions online.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/quiz/java&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Java Quiz&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Practice Java questions online."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/quiz/java"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields in, three tags out — omitted fields produce no tag rather than an empty one.&lt;/p&gt;

&lt;p&gt;The four things worth automating, all of which are silent failures by hand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Relative image paths become absolute.&lt;/strong&gt; A relative &lt;code&gt;og:image&lt;/code&gt; is dropped by most crawlers, and you find out when someone shares the link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Graph uses &lt;code&gt;property&lt;/code&gt;, not &lt;code&gt;name&lt;/code&gt;.&lt;/strong&gt; Writing &lt;code&gt;name="og:title"&lt;/code&gt; is the single most common hand-written metadata bug and it does nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social titles fall back to the top-level ones&lt;/strong&gt;, so the common case is written once instead of three times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The card type defaults correctly&lt;/strong&gt; — &lt;code&gt;summary_large_image&lt;/code&gt; when there's an image, &lt;code&gt;summary&lt;/code&gt; when there isn't, because a large-image card with no image renders as a bare link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's also a nice trick available to a framework that owns the render: it can tell when you've called the metadata helper from the wrong place. Tags in &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; are read by nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stoneware&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nf"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;was&lt;/span&gt; &lt;span class="nx"&gt;called&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nx"&gt;rendering&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;about&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;its&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nx"&gt;Those&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="nx"&gt;land&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;body&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;where&lt;/span&gt; &lt;span class="nx"&gt;nothing&lt;/span&gt; &lt;span class="nx"&gt;reads&lt;/span&gt; &lt;span class="nx"&gt;them&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Move&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="nx"&gt;into&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&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;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; the protocol details, the fallbacks, and telling you when the tags landed somewhere useless.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. &lt;code&gt;sitemap.xml&lt;/code&gt; — and why auto-generation is the wrong default
&lt;/h2&gt;

&lt;p&gt;Here's a place I think most frameworks get the philosophy wrong.&lt;/p&gt;

&lt;p&gt;A framework knows every route pattern in your project. It is &lt;em&gt;technically&lt;/em&gt; trivial to enumerate them into a sitemap. Several frameworks do, and it feels like a feature.&lt;/p&gt;

&lt;p&gt;It's a mistake. A sitemap is not a list of routes that exist — it's a list of pages you are asking a search engine to index. Those are different sets, and the difference is editorial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a checkout confirmation page&lt;/li&gt;
&lt;li&gt;anything behind a login&lt;/li&gt;
&lt;li&gt;paginated archives you'd rather have crawled through links&lt;/li&gt;
&lt;li&gt;a legal page you have to host but don't want ranking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All routes. None of them sitemap entries. A framework that guesses produces a file that is confidently wrong, and confidently wrong is worse than absent.&lt;/p&gt;

&lt;p&gt;So the right split is: &lt;strong&gt;the framework owns XML correctness; you own the editorial decision.&lt;/strong&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sitemap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stoneware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../lib/site.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;POSTS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../lib/posts.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;POSTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="p"&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;It's a route, not a config file — so it can query your database and stay correct with no build step. Derive entries from the same data the pages render and the sitemap cannot drift from the site.&lt;/p&gt;

&lt;p&gt;The parts that are genuinely easy to get wrong, and are therefore worth owning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XML escaping, including the apostrophe.&lt;/strong&gt; &lt;code&gt;'&lt;/code&gt; is legal in a URL and illegal unescaped in XML. HTML-escaping helpers get this wrong and produce a document some parsers reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative URLs are refused, not emitted.&lt;/strong&gt; A relative &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; parses fine and no crawler can use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date-only strings pass through unchanged.&lt;/strong&gt; Round-tripping &lt;code&gt;2026-08-13&lt;/code&gt; through &lt;code&gt;Date&lt;/code&gt; shifts it by the local UTC offset and publishes the wrong day for half the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits are enforced&lt;/strong&gt; — duplicates collapse, out-of-range &lt;code&gt;priority&lt;/code&gt; is refused, and &amp;gt;50,000 entries fails with a pointer to sitemap indexes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; the file format. Not the contents.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. &lt;code&gt;robots.txt&lt;/code&gt; should be a route too
&lt;/h2&gt;

&lt;p&gt;There's a persistent instinct to make &lt;code&gt;robots.txt&lt;/code&gt; a static file. Don't. The moment you have a staging environment you want &lt;code&gt;Disallow: /&lt;/code&gt;, and the moment you have a sitemap you want its absolute URL in there.&lt;/p&gt;

&lt;p&gt;The scaffolded version is deliberately boring:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`User-agent: *
Allow: /

Sitemap: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;siteURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&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;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="dl"&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;Cache-Control&lt;/span&gt;&lt;span class="dl"&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;public, no-cache&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's ordinary code, staging is a two-line change:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isProduction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isProduction&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`User-agent: *\nAllow: /\n\nSitemap: ...`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`User-agent: *\nDisallow: /\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serving a permissive &lt;code&gt;robots.txt&lt;/code&gt; from a staging domain is one of the fastest ways to get duplicate content indexed under a URL you don't control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; scaffolding both files on day one, so they exist in the first commit rather than being added after the first indexing problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Broken links, caught at build time
&lt;/h2&gt;

&lt;p&gt;Internal links that point nowhere leak crawl budget and dead-end users, and they're invariably found by a crawler weeks after they shipped.&lt;/p&gt;

&lt;p&gt;A framework that prerenders your site already has everything needed to check this — it knows every file it wrote and can read every link it emitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stoneware &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The export follows every same-origin &lt;code&gt;href&lt;/code&gt; &lt;strong&gt;and &lt;code&gt;src&lt;/code&gt;&lt;/strong&gt; in the pages it wrote and reports anything resolving to nothing. &lt;code&gt;src&lt;/code&gt; matters as much as &lt;code&gt;href&lt;/code&gt;: a missing stylesheet or script chunk is the same class of failure and much easier to ship unnoticed. It also reports any route skipped for lacking a &lt;code&gt;staticPaths()&lt;/code&gt; export. With &lt;code&gt;--strict&lt;/code&gt;, either one fails the build instead of printing a note you scroll past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; validating its own output before you deploy it.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Crawl budget is a caching problem
&lt;/h2&gt;

&lt;p&gt;Search engines re-crawl. If every re-crawl transfers the full document, you're paying for it in bandwidth and in crawl budget spent re-reading pages that didn't change.&lt;/p&gt;

&lt;p&gt;Stoneware gives every page a weak &lt;code&gt;ETag&lt;/code&gt; derived from the rendered HTML, with &lt;code&gt;Cache-Control: public, no-cache&lt;/code&gt;. That combination is widely misread: &lt;code&gt;no-cache&lt;/code&gt; does not mean "don't store", it means "revalidate before use". A crawler with a stored copy sends &lt;code&gt;If-None-Match&lt;/code&gt; and gets an empty &lt;strong&gt;304&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The validator changes exactly when the page changes — there's no &lt;code&gt;max-age&lt;/code&gt; to tune and no window in which a published change is invisible.&lt;/p&gt;

&lt;p&gt;I'll be honest about the cost, because it's real: since the validator is a hash of the output, producing it means producing the output. A 304 still runs the route and renders the page. Measured on a 14 KB document, a 304 costs about four-fifths of what the 200 costs. The saving is bandwidth, not server work. Static assets are the opposite — they're content-hashed, served &lt;code&gt;immutable&lt;/code&gt; for a year, and a 304 there is about twenty times cheaper than sending the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; correct validators on every response, so revalidation works without configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Analytics: the part nobody wants to write down
&lt;/h2&gt;

&lt;p&gt;Here's where I have to be honest about a trade-off rather than sell you a feature.&lt;/p&gt;

&lt;p&gt;Stoneware ships a restrictive Content-Security-Policy &lt;strong&gt;on by default&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;style&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;:;
&lt;span class="n"&gt;font&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;connect&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;object&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;; &lt;span class="n"&gt;base&lt;/span&gt;-&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
&lt;span class="n"&gt;form&lt;/span&gt;-&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;frame&lt;/span&gt;-&lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is possible because the framework never emits inline executable script — hydration payloads are JSON in a non-executable block, not string-concatenated into a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. So &lt;code&gt;script-src 'self'&lt;/code&gt; just works, with no nonce plumbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it blocks Google Analytics.&lt;/strong&gt; Out of the box, &lt;code&gt;gtag.js&lt;/code&gt; will not load. That is not an oversight; it's the point of a default-deny policy. But it means the framework owes you a clear path rather than a shrug.&lt;/p&gt;

&lt;p&gt;The policy is additive — you name extra origins per directive and the defaults survive:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scriptSrc&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;https://www.googletagmanager.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;connectSrc&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;https://*.google-analytics.com&lt;/span&gt;&lt;span class="dl"&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;https://*.analytics.google.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;imgSrc&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;https://*.google-analytics.com&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;www&lt;/span&gt;.&lt;span class="n"&gt;googletagmanager&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;google&lt;/span&gt;-&lt;span class="n"&gt;analytics&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;connect&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;google&lt;/span&gt;-&lt;span class="n"&gt;analytics&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;analytics&lt;/span&gt;.&lt;span class="n"&gt;google&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;'self'&lt;/code&gt; and every directive you didn't mention are preserved — you're widening a policy, not replacing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part that catches people:&lt;/strong&gt; allowing the domain is not enough. The standard GA snippet includes an &lt;em&gt;inline&lt;/em&gt; &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that initialises &lt;code&gt;dataLayer&lt;/code&gt;, and &lt;code&gt;script-src&lt;/code&gt; without &lt;code&gt;'unsafe-inline'&lt;/code&gt; blocks it regardless of which domains you allow. You have two honest options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move that initialisation into a file under &lt;code&gt;public/&lt;/code&gt; and load it with &lt;code&gt;&amp;lt;script src="/analytics.js"&amp;gt;&lt;/code&gt; — same-origin, so &lt;code&gt;'self'&lt;/code&gt; covers it.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;'unsafe-inline'&lt;/code&gt; to &lt;code&gt;script-src&lt;/code&gt;, and understand that you have just disabled your main defence against XSS to save one file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I think a framework's job here is to make option 1 easy and option 2 explicit, rather than to quietly ship &lt;code&gt;'unsafe-inline'&lt;/code&gt; so that every analytics snippet pastes cleanly. A lot of frameworks make the opposite choice by default and never mention it.&lt;/p&gt;

&lt;p&gt;It's also worth saying: a server-first site with no client JavaScript has &lt;em&gt;unusually good&lt;/em&gt; options here. Server-side request logging via an observer hook gives you page views with no third-party script, no cookie banner, and no CSP exception at all:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pageview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="p"&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;&lt;code&gt;event.route&lt;/code&gt; is the &lt;em&gt;pattern&lt;/em&gt; — &lt;code&gt;/blog/[slug]&lt;/code&gt;, not &lt;code&gt;/blog/hello-world&lt;/code&gt; — which is what you actually want as a metrics dimension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a framework should handle automatically:&lt;/strong&gt; a secure default, an additive escape hatch, and documentation honest enough to say which of your tools it breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a framework should &lt;em&gt;not&lt;/em&gt; do
&lt;/h2&gt;

&lt;p&gt;A short list, because overreach here is common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It should not write your metadata.&lt;/strong&gt; Making tags easy is the job; deciding what they say is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It should not audit your content.&lt;/strong&gt; Heading structure, alt text, internal linking, whether the page is worth reading — none of that is checkable by a build tool, and tools that claim to check it mostly count characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It should not enumerate your sitemap.&lt;/strong&gt; See above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It should not promise rankings.&lt;/strong&gt; Any framework that markets itself on search rankings is making a claim about someone else's algorithm that it cannot keep. The honest version is: here is what is in the HTML, verify it yourself.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The scorecard
&lt;/h2&gt;

&lt;p&gt;If you're evaluating a framework — or building one — these are the questions I'd ask, in roughly the order they'll cost you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Should be automatic?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Content present in the first HTML response&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A dynamic route with no data returns 404&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, one obvious call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework conventions aren't servable as pages&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error responses are never cached&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLs are correct behind a TLS-terminating proxy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;One explicit setting&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata protocol details and fallbacks&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning when metadata lands somewhere useless&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, in development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap XML correctness&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap &lt;em&gt;contents&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;robots.txt&lt;/code&gt; scaffolded and environment-aware&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Scaffolded&lt;/strong&gt;, then yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken internal links caught at build&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, with a flag to fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct cache validators on every response&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party analytics working out of the box&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — and it should say so&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most frameworks score well on exactly one row of that table: metadata. That's the row that's easiest to build and the one that matters least, because it's the row you were already thinking about.&lt;/p&gt;

&lt;p&gt;The rows that quietly cost traffic are the ones where the failure is a 200 status, a correct-looking &lt;code&gt;http://&lt;/code&gt;, or a link to a page that isn't there — all silent, all invisible in development, all discovered by a crawler long after they shipped.&lt;/p&gt;

&lt;p&gt;Those are the ones worth automating.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stoneware is a Bun-native, server-first web framework — MIT, &lt;a href="https://github.com/stoneware-dev/stoneware-core" rel="noopener noreferrer"&gt;stoneware-dev/stoneware-core&lt;/a&gt;. Every measurement above comes from a reproducible benchmark of a 21-route content site; the harness is public.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Built a Web Framework From Scratch on Bun — Here's What I Learned</title>
      <dc:creator>Ranjeet Kumar Jena</dc:creator>
      <pubDate>Tue, 18 Aug 2026 06:43:22 +0000</pubDate>
      <link>https://dev.to/ranjeet_kumarjena_4a6b1b/i-built-a-web-framework-from-scratch-on-bun-heres-what-i-learned-ndg</link>
      <guid>https://dev.to/ranjeet_kumarjena_4a6b1b/i-built-a-web-framework-from-scratch-on-bun-heres-what-i-learned-ndg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I kept running into the same problem across projects: pages that were, structurally, documents — a blog post, a portfolio, a product page — shipping a client-side JavaScript runtime whether or not anything on the page actually needed to be interactive. Even frameworks built around "islands" still start from the assumption that JavaScript is the default and you carve out exceptions. A hydration shim ships. A router ships. Something ships, even to a page that's just text and images.&lt;/p&gt;

&lt;p&gt;That inversion bothered me enough to build my own answer to it. Stoneware is a Bun-native, server-first framework built on one rule: HTML is the default, and JavaScript is opt-in, enforced by where a file lives rather than a flag you have to remember to set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Two decisions do almost all the work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML by default.&lt;/strong&gt; Every route renders to a complete HTML string on the server. A page with no interactive components ships zero bytes of JavaScript — not a small runtime, not a hydration shim, nothing. The absence of a script tag is the actual output, not an optimization applied after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript only where interaction is needed.&lt;/strong&gt; A component becomes interactive if — and only if — it lives under &lt;code&gt;islands/&lt;/code&gt;. Anything under &lt;code&gt;routes/&lt;/code&gt; never ships client code. There's no per-file directive to remember and no way to accidentally make a page interactive. The file's location &lt;em&gt;is&lt;/em&gt; the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bun
&lt;/h2&gt;

&lt;p&gt;I didn't want to bolt a framework onto Node and call it done. Stoneware is built directly on Bun's primitives — &lt;code&gt;Bun.serve&lt;/code&gt;, &lt;code&gt;Bun.build&lt;/code&gt;, &lt;code&gt;Bun.escapeHTML&lt;/code&gt;, &lt;code&gt;Bun.CSRF&lt;/code&gt;, &lt;code&gt;Bun.FileSystemRouter&lt;/code&gt; — which means it's Bun-native, not "Node-compatible, running on Bun."&lt;/p&gt;

&lt;p&gt;A few things fell out of that choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime.&lt;/strong&gt; Server, build, and test all run on one runtime. No separate process for bundling, no separate test runner to configure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build tooling.&lt;/strong&gt; &lt;code&gt;Bun.build&lt;/code&gt; handles island bundling directly — no reaching for a second bundler and reconciling two configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast development loop.&lt;/strong&gt; &lt;code&gt;stoneware dev&lt;/code&gt; reloads fast because there's no extra toolchain being shelled out to underneath it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single ecosystem.&lt;/strong&gt; One lockfile, one package manager, one test runner (&lt;code&gt;bun test&lt;/code&gt;). Fewer places for versions to drift out of sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scaffolding a new project still works with plain &lt;code&gt;npx create-stoneware my-site&lt;/code&gt; before Bun is even installed — everything after that (dev server, build) runs on Bun, and the CLI tells you if it's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Stoneware renders a page
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   ↓
Server
   ↓
SSR
   ↓
HTML
   ↓
Browser
   ↓
Interactive islands only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request comes in, &lt;code&gt;Bun.FileSystemRouter&lt;/code&gt; resolves it to a route function, that function runs on the server and returns markup, Stoneware renders it to a complete HTML string, and that's what ships to the browser. The browser only hydrates the islands actually referenced on that specific page — everything else stays exactly as static HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Islands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static HTML stays static.&lt;/strong&gt; If a page doesn't import anything from &lt;code&gt;islands/&lt;/code&gt;, it ships no script tag at all. Not a minimal one — none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive components become islands.&lt;/strong&gt; Drop a file under &lt;code&gt;islands/&lt;/code&gt; and it hydrates on the client. That's the entire mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client JavaScript is opt-in.&lt;/strong&gt; Hydration directives like &lt;code&gt;client:visible&lt;/code&gt;, &lt;code&gt;client:idle&lt;/code&gt;, and &lt;code&gt;client:media&lt;/code&gt; control &lt;em&gt;when&lt;/em&gt; an island hydrates. But &lt;em&gt;whether&lt;/em&gt; a page ships any JS at all is already decided by file location before any directive comes into play.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Islands use &lt;a href="https://github.com/preactjs/signals" rel="noopener noreferrer"&gt;Preact Signals&lt;/a&gt; directly, re-exported as &lt;code&gt;stoneware/signals&lt;/code&gt;, rather than a bespoke reactivity engine — that's a deliberate scope boundary. Templates themselves are plain functions: props in, markup out. No classes, no hooks, no lifecycle methods to learn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers, measured on Stoneware's own docs site in production:&lt;/strong&gt; the whole client runtime — signals, hydration, DOM — comes to roughly 3.4 KB gzipped. A single island (a counter component) adds about 0.2 KB. A page with no islands adds zero bytes and no script tag. In a benchmark building the same 16-page site with matching content and five interactive components across Stoneware, Astro 5.18, and Next.js 15.5, Stoneware shipped 14.2 KB of JS against Astro's 193.1 KB and Next.js's 346.0 KB — roughly 13–24x less for the same interactivity. LCP followed the same pattern (1217 ms vs. 2253 ms vs. 2965 ms), because Stoneware has no fixed client-runtime cost sitting on the critical path. Numbers like this are easy to cherry-pick, so treat the ordering as the finding and the exact figures as indicative — bytes here are uncompressed, and production gzip narrows the gap somewhat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Stoneware isn't resumable — yet
&lt;/h2&gt;

&lt;p&gt;I want to be straight about this one instead of dressing it up: Stoneware does not currently do resumable SSR. I considered it early, and deliberately deferred it rather than half-build it.&lt;/p&gt;

&lt;p&gt;Resumability — in the sense frameworks like Qwik use the term — means the server can serialize enough of the application's state and event listeners into the HTML that the client can "resume" execution without replaying the component tree from scratch. It's a genuinely hard problem: you need a serialization format for closures and state, a way to lazily load only the code a given interaction needs, and a runtime that can reconstruct execution context from that serialized state on demand. Doing it properly touches nearly every other architectural decision in the framework — the build pipeline, the islands model, the signals runtime.&lt;/p&gt;

&lt;p&gt;Islands already solve the version of this problem I actually had: most of a page needs no JavaScript at all, and the parts that do only need it once, on load. Resumability solves a different problem — deferring &lt;em&gt;when&lt;/em&gt; that JavaScript executes, down to the level of individual event listeners — and it's a real one for a certain class of app. It's just not a problem Stoneware's target use case (content-heavy sites) hits often enough to justify the complexity right now. It's on the list, deliberately unshipped rather than accidentally missing, and it'll get built when I'm confident I can do it without compromising the parts of the architecture that already work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static export
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stoneware export&lt;/code&gt; prerenders every route to static HTML. The output is plain files — no server process required to serve them — which means they deploy to any static host or CDN: Cloudflare Pages, Netlify, GitHub Pages, S3 behind CloudFront, whatever you've already got.&lt;/p&gt;

&lt;p&gt;That's a separate path from &lt;code&gt;stoneware build --target vercel&lt;/code&gt;, which emits a server bundle with Vercel's Bun-preset entrypoint for projects that need the full server runtime rather than a static export. If your site doesn't need server-side logic per request, export is the simpler and cheaper option — it's also the fastest build path measured so far, at well under a second for a 16-page site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Auto-escaping, CSRF verification, and a restrictive CSP are on before you write a line of configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSRF&lt;/strong&gt; verification runs automatically via &lt;code&gt;Bun.CSRF&lt;/code&gt;, tied to a per-environment secret you set once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP&lt;/strong&gt; is restrictive by default rather than opt-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure defaults&lt;/strong&gt; extend to how server and client agree on what's safe: one shared module decides what a given attribute is allowed to contain, so something like a &lt;code&gt;javascript:&lt;/code&gt; URL gets refused both on first render and on every subsequent client-side update — not just at the initial SSR pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The unsafe path exists, but it's intentionally named to stand out in a code review: &lt;code&gt;raw()&lt;/code&gt; and &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; are both greppable on purpose. If you see either in a diff, that's the point where you look twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building a framework taught me
&lt;/h2&gt;

&lt;p&gt;This is the part I'd actually want another framework author to read.&lt;/p&gt;

&lt;p&gt;The concrete one: my own portfolio site — built with Stoneware, using an Odisha temple-inspired stone-and-terracotta design — surfaced a production CSS bug in the framework itself. Styles that worked fine in dev broke under the production build path, which meant the bug wasn't in my application code at all — it was in how Stoneware handled asset output between the two modes. Finding it meant accepting that the thing I was debugging wasn't my site; it was the tool I'd built to make the site.&lt;/p&gt;

&lt;p&gt;That's the pattern that repeated in different shapes throughout the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asset tracing problems&lt;/strong&gt; — figuring out which assets a given build actually depends on gets harder than it sounds once islands, co-located CSS, and static export all need to agree on the same dependency graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment problems&lt;/strong&gt; — a static export and a server bundle are genuinely different artifacts with different failure modes, and testing "does this deploy correctly" means testing both paths, not one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static export edge cases&lt;/strong&gt; — routes that behave correctly under &lt;code&gt;stoneware dev&lt;/code&gt; don't always behave the same way once prerendered, especially anything touching dynamic data at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP integration&lt;/strong&gt; — a strict-by-default CSP is easy to state as a feature and hard to keep true once real pages need real inline styles or third-party embeds without quietly weakening the policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency boundaries&lt;/strong&gt; — deciding what belongs in &lt;code&gt;stoneware/signals&lt;/code&gt; versus the framework core versus application code is a boundary you have to keep re-drawing as the framework grows, not a decision you make once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework bugs vs. application bugs&lt;/strong&gt; — the hardest debugging sessions were the ones where I didn't yet know which side of that line I was on. Building the docs site as a real Stoneware consumer (not a special-cased internal example) was what forced that distinction to actually matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is dramatic. It's the ordinary tax of building the tool and the thing built with the tool at the same time, and I think that tax is underrated in how "just build a framework" gets talked about online.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Stoneware is today
&lt;/h2&gt;

&lt;p&gt;Stoneware is open source under MIT, currently at v0.1. There's a test suite (&lt;code&gt;bun test&lt;/code&gt;) alongside the source. The documentation lives in its own repository and is itself built with Stoneware — it consumes the framework from npm the same way any other project would, so the docs can't quietly depend on unreleased behavior.&lt;/p&gt;

&lt;p&gt;Real usage so far is my own portfolio site, which is also where the production CSS bug turned up.&lt;/p&gt;

&lt;p&gt;What's deliberately &lt;strong&gt;not&lt;/strong&gt; in v0.1: streaming SSR, resumability (see above), multi-framework islands, edge/serverless targets, and a local-first data layer. Each of those was considered and set aside rather than overlooked — the reasoning for each is in the framework's &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / contribute
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx create-stoneware my-site   &lt;span class="c"&gt;# npx create-stoneware my-site also works&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
bun &lt;span class="nb"&gt;install
&lt;/span&gt;bun run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see how the pieces fit together — the request pipeline, the render model, and the reasoning behind the parts that are easy to undo by accident — that's in &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; in the core repo.&lt;/p&gt;

&lt;p&gt;Stoneware is still early. I'm not trying to claim that another framework is needed for every project. I'm interested in seeing whether this architecture makes sense to other developers.&lt;/p&gt;

&lt;p&gt;If you're interested in Bun, SSR, islands, resumability, or framework internals, I'd love technical feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/stoneware-dev/stoneware-core" rel="noopener noreferrer"&gt;Stoneware Core on GitHub&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/stoneware-dev/stoneware-docs" rel="noopener noreferrer"&gt;Stoneware Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bunjs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
