<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
      <title>Mastro</title>
      <subtitle>Updates about the web framework and static site generator, as well as the Mastro Guide.</subtitle>
      <link rel="self" href="https://mastrojs.github.io/feed.xml" />
      <link rel="alternate" href="https://mastrojs.github.io/blog/" />
      <updated>2026-06-05T12:00:00.000Z</updated>
      
      <id>https://mastrojs.github.io/</id>
      
      
      
    <entry>
      <id>https://mastrojs.github.io/blog/2026-06-05-how-to-add-standard-site-support-to-your-website/</id>
      <title>How to easily add Standard.site support to your website</title>
      <updated>2026-06-05T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-06-05-how-to-add-standard-site-support-to-your-website/" />
      
      <content type="html">&lt;p&gt;&lt;em&gt;(Updated on June 15, 2026)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since &lt;a href=&quot;https://atproto.com/blog/standard-site-bluesky-timeline&quot;&gt;Bluesky added support for Standard.site&lt;/a&gt;, everyone with a blog seems to be racing to roll out an implementation. That’s awesome, and yet another example of how &lt;a href=&quot;https://atproto.com/&quot;&gt;ATproto&lt;/a&gt; feels like one of the more exciting things happening in tech right now.&lt;/p&gt;
&lt;h2&gt;What ATmosphere?&lt;/h2&gt;
&lt;p&gt;For a bit of background, see e.g. Steve Klabnik’s &lt;a href=&quot;https://steveklabnik.com/writing/how-does-bluesky-work/&quot;&gt;How Does BlueSky Work?&lt;/a&gt;
The TL;DR is that you can think of &lt;em&gt;the ATmosphere&lt;/em&gt; as a big distributed database, which is partially mirrored a few times via relays. Anybody with an account on a PDS (Personal Data Server – Bluesky being the biggest example) can make HTTP requests to it and read and write records in the database. A set of evolving, shared lexicons make sure your data remains portable and you’re not being locked in. The latest such lexicon to reach critical mass is &lt;a href=&quot;https://standard.site/&quot;&gt;Standard.site&lt;/a&gt; – it’s a standardized way to express metadata about publications and documents (e.g. blogs and their posts).&lt;/p&gt;
&lt;p&gt;One interesting way that some people use this is to use the ATmosphere as the backend database for their website.&lt;/p&gt;
&lt;h2&gt;Uploading blog posts&lt;/h2&gt;
&lt;p&gt;But most people already have their posts in a traditional database (e.g. via a CMS), or have a static site with Markdown files. How can you ensure that whenever you create a new blog post (or change an existing one), the corresponding records in the Atmosphere are created (or updated)?&lt;/p&gt;
&lt;p&gt;An ATproto record in the &lt;code&gt;site.standard.document&lt;/code&gt; &lt;em&gt;collection&lt;/em&gt; has a URI like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;at://&amp;quot; DID &amp;quot;/site.standard.document/&amp;quot; rkey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://atproto.com/specs/did&quot;&gt;DID&lt;/a&gt; (Decentralized Identifier) uniquely identifies your user, and the &lt;a href=&quot;https://atproto.com/specs/record-key&quot;&gt;rkey&lt;/a&gt; (record key) uniquely identifies the specific document in the collection.&lt;/p&gt;
&lt;p&gt;When creating a new document, the obvious approach (that most people currently seem to be following) is to push the data and let the PDS server auto-generate an rkey.&lt;/p&gt;
&lt;p&gt;But in order for things to be verified, you need to add your record’s AT-URI to the HTML of your blog post’s web page with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;lt;link rel=&amp;quot;site.standard.document&amp;quot;
  href={`at://${did}/site.standard.document/${rkey}`}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which means that if your rkey was generated by the PDS, you need to store it somewhere after it’s generated – e.g. in your CMS’s database, or in the YAML frontmatter of your markdown files.&lt;/p&gt;
&lt;p&gt;While this is fine, it can be a bit annoying. Especially for a website without a database, it makes things a bit hard to automate. If you set up your CI/CD pipeline to push to the ATmosphere, do you then have it create a new commit with the modified markdown frontmatter?&lt;/p&gt;
&lt;h2&gt;Deriving the rkey from the URL path&lt;/h2&gt;
&lt;p&gt;An alternative approach, which I first saw &lt;a href=&quot;https://bsky.app/profile/mackuba.eu/post/3mn5hqmvlts2w&quot;&gt;proposed by Kuba Suder&lt;/a&gt;, is to derive the rkey from the URL path of the web page. This is what I just implemented in the new &lt;a href=&quot;https://github.com/mastrojs/atproto&quot;&gt;@mastrojs/atproto&lt;/a&gt; package.&lt;/p&gt;
&lt;p&gt;It exports a &lt;code&gt;rkeyFromUrl&lt;/code&gt; function that deterministically derives an rkey from the path of a URL. Then, in the code generating the HTML, we simply use it like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;lt;link rel=&amp;quot;site.standard.document&amp;quot;
  href={`at://${agent.did}/site.standard.document/${rkeyFromUrl(doc.url)}`}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does mean however, that you cannot change the URL of your post after you’ve published it. But I suppose you shouldn’t be doing that anyway.&lt;/p&gt;
&lt;h2&gt;Running the script&lt;/h2&gt;
&lt;p&gt;Finally, we package everything up in a nice declarative way. You simply add a script to your codebase with something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createOrUpdateStandardSite, type Publication } from &amp;quot;@mastrojs/atproto&amp;quot;;
import { readMarkdownFiles } from &amp;quot;@mastrojs/markdown&amp;quot;;

const identifier = &amp;quot;your.bsky.social&amp;quot;;
const password = process.env.ATPROTO_PASSWORD;
const pubUrl = new URL(&amp;quot;https://example.com/news/&amp;quot;);

const publication: Publication = {
  url: pubUrl,
  name: &amp;quot;Peter&#39;s News&amp;quot;,
};

const posts = await readMarkdownFiles(&amp;quot;data/posts/*.md&amp;quot;);
const docs = posts.map((p) =&amp;gt; ({
  title: p.meta.title,
  publishedAt: new Date(p.meta.date),
  url: new URL(p.slug, pubUrl),
}));

await createOrUpdateStandardSite({ identifier, password }, publication, docs);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever you run your script, it will fetch the existing records from your PDS, diff them against your current input, update the existing ones, and upload any new records. Regardless of whether your run it manually, or in your CI/CD pipeline.&lt;/p&gt;
&lt;h2&gt;Does it work?&lt;/h2&gt;
&lt;p&gt;Try posting a link to this blog post on Bluesky, and you should see the shiny “View publication” button appear!&lt;/p&gt;
&lt;p&gt;If you want the same for your blog, go ahead and use the &lt;a href=&quot;https://github.com/mastrojs/atproto&quot;&gt;@mastrojs/atproto&lt;/a&gt; package. Bug reports and contributions welcome. Happy publishing to the ATmosphere!&lt;/p&gt;
&lt;h2&gt;Version 0.2&lt;/h2&gt;
&lt;p&gt;The first version of this blog post (and first version of the &lt;code&gt;@mastrojs/atproto&lt;/code&gt; library) used the simplest approach possible to derive an rkey from a URL path: strip all characters that may not appear in any rkey (e.g. slashes and other special characters).&lt;/p&gt;
&lt;p&gt;While this approach works on Bluesky (and the validator didn&#39;t complain about it at the time), I was made aware that the Standard.site schema doesn&#39;t specify an rkey of type &lt;a href=&quot;https://atproto.com/specs/record-key#record-key-type-any&quot;&gt;&lt;code&gt;any&lt;/code&gt;&lt;/a&gt;, but requires one of type &lt;a href=&quot;https://atproto.com/specs/tid&quot;&gt;&lt;code&gt;TID&lt;/code&gt;&lt;/a&gt; (Timestamp Identifier). There is an &lt;a href=&quot;https://tangled.org/standard.site/lexicons/issues/7#comment-3mnm5xd5prb22&quot;&gt;ongoing discussion&lt;/a&gt; whether this requirement can be relaxed, as it would simplify things a bit. But there is no discernible sign that the schema might be changed anytime soon.&lt;/p&gt;
&lt;p&gt;That&#39;s why in version 0.2 of &lt;code&gt;@mastrojs/atproto&lt;/code&gt;, you can specify whether you want the rkey of type &lt;code&gt;TID&lt;/code&gt; (which is now the default), &lt;code&gt;any&lt;/code&gt;, or supply your own rkey. If a date in format &lt;code&gt;YYYY-MM-DD&lt;/code&gt; or &lt;code&gt;YYYY/MM/DD&lt;/code&gt; is detected anywhere in the URL path, that will be used as the timestamp of the TID. The rest of the path is used for the remaining bits of the TID. Either way, the rkey is derived deterministically from the document&#39;s URL path – which is what this library set out to solve.&lt;/p&gt;
&lt;p&gt;I already put a lot of thought and care into version 0.1. The idea that you declaratively provide all information as part of a function call instead of a pure CLI like &lt;a href=&quot;https://sequoia.pub/&quot;&gt;Sequoia&lt;/a&gt; was novel AFAIK. (In v0.2, this API is even safer since it&#39;s using &lt;code&gt;URL&lt;/code&gt;s instead of string paths.) Then there is the flow that when you run the script for the first time, it will show you the URLs and rkeys, and once confirmed, create a &lt;code&gt;.well-known/site.standard.publication&lt;/code&gt; file. And only on subsequent runs, it will publish and/or update documents in the Atmosphere. You can even do that in your CI/CD pipeline, which I find pretty neat.&lt;/p&gt;
&lt;p&gt;That&#39;s why I had mixed feelings when I discovered somebody had copied the source code of &lt;code&gt;@mastrojs/atproto&lt;/code&gt;, removed the types and tests, and made a few relatively minor changes (&lt;a href=&quot;https://github.com/mastrojs/atproto/tree/a36b6080e93a6ee3b0bb5db6ee0251a830e240bf/src&quot;&gt;source&lt;/a&gt; -&amp;gt; &lt;a href=&quot;https://github.com/Wilto/ATapult/tree/d5dd0fffe682c23d472c7be454640ab1b144cd9a/src&quot;&gt;copy&lt;/a&gt;).
Yet it proved I was onto something. I approached them and asked whether they were interested in a collaboration, even offering to switch to their project name and repo, but the replies were evasive. Yet the next day, they published an announcement blog post that doesn&#39;t even mention the original project (the README now says &amp;quot;Credit to @mastrojs/atproto for the inspiration&amp;quot;). Oh well, community building is hard.&lt;/p&gt;
&lt;p&gt;Sadly, this also means they and their users are losing out on all the improvements of v0.2 outlined above. (Their version derives the rkey from the document&#39;s &lt;code&gt;publishedAt&lt;/code&gt; instead of the URL path, which has the potential for clashes if your granularity is dates, and the timestamp calculation is off by a factor of 1000.)&lt;/p&gt;
&lt;p&gt;It seems that integrating existing websites with the Atmosphere is a problem worth solving once, and worth solving well. If you&#39;re working on this as well, I would be more than happy to collaborate! Do you have better ideas? Is there anything that doesn&#39;t work for you yet? Should we export more functions for you to integrate it in your existing stack? Just &lt;a href=&quot;https://github.com/mastrojs/atproto/issues&quot;&gt;open a GitHub issue&lt;/a&gt; or talk to us on &lt;a href=&quot;https://stt.gg/k7QMEaP1&quot;&gt;Stoat&lt;/a&gt; or &lt;a href=&quot;https://bsky.app/profile/mastrojs.bsky.social&quot;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-05-26-component-scoped-css-without-build-step/</id>
      <title>Four ways to do component-scoped CSS without a complex build step</title>
      <updated>2026-05-26T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-05-26-component-scoped-css-without-build-step/" />
      
      <content type="html">&lt;p&gt;People have been exploring different ways to organize their CSS for almost as long as CSS has been around. In the beginning with methodologies like BEM, later with tooling like CSS modules, CSS-in-JS, and now &lt;a href=&quot;/blog/2025-11-27-why-not-just-use-inline-styles-tailwind/&quot;&gt;with Tailwind&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But with browsers now &lt;a href=&quot;/guide/css/#want-to-learn-more-css%3F&quot;&gt;natively supporting&lt;/a&gt; CSS nesting, variables and &lt;code&gt;@scope&lt;/code&gt; rules, let&#39;s look at four approaches to do things without resorting to complex tooling.&lt;/p&gt;
&lt;h2&gt;1. One big CSS file&lt;/h2&gt;
&lt;p&gt;Still a great way to get started. Don&#39;t overcomplicate things for a small website! And you always need a file with some globals to set up CSS variables, fonts, etc. anyway. If you&#39;re following Heydon Pickering&#39;s way of &lt;a href=&quot;https://www.smashingmagazine.com/2016/11/css-inheritance-cascade-global-scope-new-old-worst-best-friends/&quot;&gt;applying a thorough base style to all your HTML elements&lt;/a&gt; (which I highly recommend), a single file can carry you a long way.&lt;/p&gt;
&lt;h2&gt;2. A plain CSS file for each component&lt;/h2&gt;
&lt;p&gt;But maybe you&#39;ve started organizing your template files into components. In that case, you may want to colocate your CSS for each component in the same folder (e.g. &lt;code&gt;components/Header.css&lt;/code&gt;). But if you have more than a dozen components, serving each CSS file separately starts negatively affecting performance.&lt;/p&gt;
&lt;p&gt;We&#39;ll be using &lt;a href=&quot;/docs/html-components/&quot;&gt;Mastro server components&lt;/a&gt; in the following examples, but you probably can adapt the approach to whatever setup you&#39;re using. A Mastro component for your website&#39;s header might look as follows. (Note that the &lt;code&gt;Header&lt;/code&gt; component in turn uses the &lt;code&gt;Navigation&lt;/code&gt; component.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { html } from &amp;quot;@mastrojs/mastro&amp;quot;;
import { Navigation } from &amp;quot;./Navigation.js&amp;quot;;

export const Header = () =&amp;gt;
  html`
    &amp;lt;header&amp;gt;
      &amp;lt;p&amp;gt;My awesome website&amp;lt;/p&amp;gt;
      ${Navigation()}
    &amp;lt;/header&amp;gt;
  `;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The simplest way to &lt;a href=&quot;/guide/bundling-assets/&quot;&gt;bundle&lt;/a&gt; all your CSS is to just read out all your CSS files, and concatenate them. &lt;a href=&quot;/blog/2026-01-29-everything-is-a-route-one-interface-for-servers-static-sites-and-assets/&quot;&gt;In Mastro, everything is a route&lt;/a&gt;. And a route to do that would look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { findFiles, readTextFile } from &amp;quot;@mastrojs/mastro&amp;quot;;

export const GET = async () =&amp;gt; {
  const files = await findFiles(&amp;quot;components/**/*.css&amp;quot;);
  const contents = await Promise.all(files.map(readTextFile));
  return new Response(
    contents.join(&amp;quot;\n\n&amp;quot;),
    { headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;text/css&amp;quot; } },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The route can be consumed by putting the following in your HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;/styles.css&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&#39;re using Mastro for static site generation, the resulting &lt;code&gt;styles.css&lt;/code&gt; file will be generated along with all the other static pages. If you&#39;re using Mastro as a server, you should mark the route for &lt;a href=&quot;/guide/bundling-assets/#build-step&quot;&gt;pregeneration&lt;/a&gt;. (Note that this approach is not yet supported when running Mastro in-browser with the VSCode extension.)&lt;/p&gt;
&lt;p&gt;It&#39;s true that this doesn&#39;t minimize or otherwise transform your CSS. But your CDN or server most likely supports gzip compression out of the box, so there is no big performance hit. And you control exactly what gets shipped to the browser – no magic transforms or outdated prefixes are ever applied.&lt;/p&gt;
&lt;h2&gt;3. Inlining the styles into the component&lt;/h2&gt;
&lt;p&gt;CSS &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope&quot;&gt;@scope rules&lt;/a&gt; are now supported in every browser (including Safari &amp;gt;= 17.4). They are a browser-native solution to what BEM, CSS modules, etc. have been trying to do: scoping the styles to only apply to specific DOM subtrees.&lt;/p&gt;
&lt;p&gt;One way to use them is by putting them directly in the HTML where they&#39;re used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { html } from &amp;quot;@mastrojs/mastro&amp;quot;;
import { Navigation } from &amp;quot;./Navigation.js&amp;quot;;

export const Header = () =&amp;gt;
  html`
    &amp;lt;header&amp;gt;
      &amp;lt;p&amp;gt;My awesome website&amp;lt;/p&amp;gt;
      ${Navigation()}

      &amp;lt;style&amp;gt;
        @scope {
          :scope {
            background-color: cyan;
          }
          p {
            font-size: 3rem;
          }
        }
      &amp;lt;/style&amp;gt;
    &amp;lt;/header&amp;gt;
  `;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;:scope&lt;/code&gt; selector will apply to the parent element of the &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag in the DOM, which is known as the scope root. In this example, it&#39;s the &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; element. Usually, that would be the root element of your component.&lt;/p&gt;
&lt;p&gt;But the real win is that the selectors inside the &lt;code&gt;@scope&lt;/code&gt; only apply to elements inside the scope root. In this example, only paragraphs inside this &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; element will get a font-size of &lt;code&gt;3rem&lt;/code&gt;. All the other paragraphs on your website will remain untouched!&lt;/p&gt;
&lt;p&gt;But at this point, the styles would also apply to whatever is rendered by the &lt;code&gt;Navigation&lt;/code&gt; component, because it&#39;s inside the &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;. For the styles to only apply from the &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; but stop at the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; element (excluding it), you could use &amp;quot;donut scoping&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;style&amp;gt;
  @scope to (nav) {
    :scope {
      background-color: cyan;
    }
    p {
      font-size: 3rem;
    }
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inlining the styles directly into the HTML like that, without making an additional HTTP request to a CSS route, is actually great for first page load performance – as long as you have the component only once or twice on the page. But for subsequent page loads, it would be better if we go back to putting the CSS in an external route that can be cached by the browser. What is faster depends a lot on how many times you use your components on any given page, how much CSS you have, and on how many pages your users typically visit.&lt;/p&gt;
&lt;h2&gt;4. Server-side CSS-in-JS&lt;/h2&gt;
&lt;p&gt;The other way to use @scope rules is to put them in an external stylesheet, and identify the scope root with a selector. With a bit of server-side JavaScript, we can still colocate the styles with the component&#39;s HTML.&lt;/p&gt;
&lt;p&gt;Still using the donut scoping technique, but introducing the convention that every component root has a &lt;code&gt;data-scope&lt;/code&gt; attribute (hat tip to Julia Evans &lt;a href=&quot;https://social.jvns.ca/@b0rk/116612656494336520&quot;&gt;who dug out that example from the CSS spec&lt;/a&gt;), this could look as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { css, html } from &amp;quot;@mastrojs/mastro&amp;quot;;
import { Navigation } from &amp;quot;./Navigation.js&amp;quot;;

const root = &amp;quot;header&amp;quot;;

export const Header = () =&amp;gt;
  html`
    &amp;lt;header data-scope=${root}&amp;gt;
      &amp;lt;p&amp;gt;My awesome website&amp;lt;/p&amp;gt;
      ${Navigation()}
    &amp;lt;/header&amp;gt;
  `;

export const styles = css`
  @scope ([data-scope=${root}]) to ([data-scope]) {
    :scope {
      background-color: cyan;
    }
    p {
      font-size: ${2 * 3}rem;
    }
  }
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of &lt;code&gt;data-scope&lt;/code&gt;, you could also use a different convention to uniquely identify component roots. For example using classes, with the convention that they need to contain a dash, like &lt;code&gt;@scope (.my-header) to ([class*=&amp;quot;-&amp;quot;])&lt;/code&gt;. Or using unregistered &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements&quot;&gt;custom elements&lt;/a&gt; with e.g. &lt;code&gt;@scope (my-header) to (:not(:defined))&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The above uses Mastro&#39;s &lt;a href=&quot;https://jsr.io/@mastrojs/mastro/doc/~/css&quot;&gt;&lt;code&gt;css&lt;/code&gt;&lt;/a&gt; tag literal (new in Mastro v0.8.5, feel free to copy its &lt;a href=&quot;https://github.com/mastrojs/mastro/blob/main/src/core/responses.ts#L105&quot;&gt;one-line-implementation&lt;/a&gt;). Just like the &lt;code&gt;html&lt;/code&gt; tag literal, it enables &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-fast.fast-tagged-templates&quot;&gt;syntax highlighting&lt;/a&gt; and embedding server-side JavaScript expressions (&lt;a href=&quot;/guide/client-side-vs-server-side-javascript-static-vs-ondemand-spa-vs-mpa/&quot;&gt;not client-side&lt;/a&gt;, like many other CSS-in-JS solutions). This allows you to calculate things in server-side JavaScript that you might previously have used SCSS for. Heck, you could insert randomly generated class names and roll your own CSS Modules implemention with a few lines of code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { css, html } from &amp;quot;@mastrojs/mastro&amp;quot;;
const name = (prefix) =&amp;gt; `${prefix}-${Math.random().toString(36).substring(2, 7)}`;

const root = name(&amp;quot;header&amp;quot;);

export const Header = () =&amp;gt;
  html`
    &amp;lt;header class=${root}&amp;gt;
      &amp;lt;p&amp;gt;My awesome website&amp;lt;/p&amp;gt;
    &amp;lt;/header&amp;gt;
  `;

export const styles = css`
  .${root} {
    background-color: cyan;
    &amp;gt; p {
      font-size: ${2 * 3}rem;
    }
  }
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Either way, to collect all the exported &lt;code&gt;styles&lt;/code&gt; under a &lt;code&gt;/styles.css&lt;/code&gt; route, use for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { findFiles, readTextFile } from &amp;quot;@mastrojs/mastro&amp;quot;;

export const GET = async () =&amp;gt; {
  const base = await readTextFile(&amp;quot;base.css&amp;quot;).catch(() =&amp;gt; &amp;quot;&amp;quot;);
  const files = await findFiles(&amp;quot;components/**/*.{js,ts}&amp;quot;);
  const styles = await Promise.all(
    files.map(f =&amp;gt; import(&amp;quot;../&amp;quot; + f).then(m =&amp;gt; m.styles))
  );
  return new Response(
    base + styles.join(&amp;quot;&amp;quot;),
    { headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;text/css&amp;quot; } },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By leaning into CSS features built into every modern browser, and leveraging &lt;a href=&quot;/blog/2026-01-29-everything-is-a-route-one-interface-for-servers-static-sites-and-assets/&quot;&gt;Mastro&#39;s flexible routes system&lt;/a&gt;, we&#39;re getting most of the functionality of common build tools – but none of the complexity. We don&#39;t require any additional dependencies, and we&#39;re in full control of what gets sent to the browser.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-05-23-is-AI-causing-a-repeat-of-frontends-lost-decade/</id>
      <title>Is AI causing a repeat of Frontend’s Lost Decade?</title>
      <updated>2026-05-23T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-05-23-is-AI-causing-a-repeat-of-frontends-lost-decade/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;What AI is doing to the jobs of programmers feels very familiar to a lot of us frontend developers – because it has happened to us before.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s first look at the transformation of the frontend and agentic coding through the lens of &lt;em&gt;deskilling&lt;/em&gt;, and then look at both changes through the lens of &lt;em&gt;a higher level of abstraction&lt;/em&gt;. Finally, we’ll look at previous changes, like the advent of copy-pasta from Stack Overflow, and how the Bauhaus movement reacted to rising industrialization.&lt;/p&gt;
&lt;h2&gt;Deskilling&lt;/h2&gt;
&lt;p&gt;Just like AI is deskilling programming now, JavaScript frameworks have deskilled frontend development in the last decade. As someone who started with HTML/CSS and a bit of PHP, later did Ruby on Rails, and then was frontend team lead of a major Swiss newspaper (Next.js at the time), I&#39;ve seen the transformation first-hand. And no need to take my word for it! I’m &lt;a href=&quot;https://ohhelloana.blog/overthinking-ai/&quot;&gt;not the first&lt;/a&gt; to &lt;a href=&quot;https://www.baldurbjarnason.com/2024/the-deskilling-of-web-dev-is-harming-us-all/&quot;&gt;say so&lt;/a&gt;. Alex Russell called it &lt;a href=&quot;https://www.youtube.com/watch?v=7ge8iwaNNAw&quot;&gt;Frontend&#39;s Lost Decade&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What is deskilling? &lt;a href=&quot;https://en.wikipedia.org/wiki/Deskilling&quot;&gt;From Wikipedia&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Deskilling is the process by which skilled labor within an industry or economy is eliminated by the introduction of technologies operated by semi- or unskilled workers. This results in cost savings [...] and reduces barriers to entry, weakening the bargaining power of [workers].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let’s see how this applies to the frontend, and then to agentic coding.&lt;/p&gt;
&lt;h3&gt;The deskilling of the frontend&lt;/h3&gt;
&lt;p&gt;A lot of programmers may not know this, but frontend used to be a highly specialized skill, requiring knowledge of semantic HTML, CSS, the differences of various browsers, accessibility, progressive enhancement, network performance, interface design and user testing – to just name a few. To distinguish what they’re doing from what “frontend” has become, practitioners of this arcane art nowadays often refer to it as the “front of the frontend”.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;deskilling of the frontend&lt;/em&gt; was the introduction of frameworks and other tooling that treats the browser as a mere compilation target – just like any other app runtime (e.g. JVM or iOS). Then you can just load in &lt;a href=&quot;https://paulmakeswebsites.com/writing/shadcn-radio-button/&quot;&gt;the monstrosity that is a Shadcn radio button&lt;/a&gt;, and don’t need to understand the underlying HTML, any subtleties involving different browsers, page load performance, and accessibility.&lt;/p&gt;
&lt;p&gt;As the Wikipedia quote above points out, this “results in cost savings” for businesses, since they then can easily put any general programmer to work on the frontend. Often, a “full-stack developer” is unfortunately not somebody who deeply understands the frontend &lt;em&gt;and&lt;/em&gt; the backend, but a generalist who just knows enough to wrangle a JavaScript framework to do both. This allows businesses to easily &lt;a href=&quot;https://www.seangoedecke.com/seeing-like-a-software-company/&quot;&gt;switch programmers around between different projects&lt;/a&gt;. The same generalist can even also do native apps with React Native and Electron! To finish the Wikipedia quote: this “reduces barriers to entry” (which is something I’ve always cherished), but it also “weakens the bargaining power of workers”.&lt;/p&gt;
&lt;h3&gt;AI is deskilling programming&lt;/h3&gt;
&lt;p&gt;What’s currently happening to programmers more generally seems very similar to what web developers in particular have been going through already. The skilled job of writing code manually is being “eliminated by the introduction of technologies, operated by semi- or unskilled workers.”&lt;/p&gt;
&lt;p&gt;We still don’t know exactly what skillset the workers wrangling agentic AI will need to have at the end of this transformation, and at what price point we’ll arrive at – for both labour, and for local and remote LLMs. But it is already clear now, that businesses absolutely will use this technology for cost savings and weakening of the bargaining power of workers.&lt;/p&gt;
&lt;h3&gt;A profound sense of loss&lt;/h3&gt;
&lt;p&gt;Just like artisans and craftsmen that were replaced by assembly line workers more than a century ago, we feel a profound sense of loss. We grieve that a skill, that we spent half a lifetime honing, is not being valued by the market anymore. And we’re saddened that the new process results in lower quality work, and that a lot of people just don’t seem to care.&lt;/p&gt;
&lt;h2&gt;Operating at a higher level of abstraction&lt;/h2&gt;
&lt;p&gt;An alternative way to look at “deskilling” is of course to argue that this is just increasing efficiency using automation. And what engineer doesn’t like automating things? It’s a big part of our job after all!&lt;/p&gt;
&lt;p&gt;In this framing, the new technology introduced simply works on a higher level of abstraction, allowing the person operating it to focus on the bigger picture, without having to bother about unimportant details. But exactly which details are deemed “unimportant” is a very consequential and sometimes subjective decision. And eventually, the details &lt;a href=&quot;https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/&quot;&gt;always leak through&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;“Modern” frontend: a tower of leaky abstractions&lt;/h3&gt;
&lt;p&gt;It’s common for an abstraction to come at a cost of performance. But since computers are very fast nowadays, we were often willing to trade some runtime performance for increased developer productivity (garbage collection is one example). And for high-powered servers under moderate load, this is a very sensible tradeoff. But mobile phones on slow networks are a different beast.&lt;/p&gt;
&lt;p&gt;By using a heavy client-side JavaScript framework like React, and a lot of packages from that ecosystem, you’re abstracting over things &lt;a href=&quot;https://gbbns.co/journal/accessibility-problem-isnt-design/&quot;&gt;like accessibility&lt;/a&gt;, and &lt;a href=&quot;https://infrequently.org/series/performance-inequality/&quot;&gt;performance on lower-end phones, or on slow networks&lt;/a&gt;. In effect, you’re choosing not to think about those things, and you’re choosing not to care about them.&lt;/p&gt;
&lt;h3&gt;Agentic coding: an undeterministic abstraction&lt;/h3&gt;
&lt;p&gt;By using agentic AI to code a feature or fix a bug, you’re describing the change at a higher level of abstraction, writing fewer words than writing all code by hand. The AI will fill in the details you omitted by looking at its training data and surrounding context – sometimes guessing well, other times not. Whether you find this useful or not depends to a large degree on your opinions on what’s important when coding.&lt;/p&gt;
&lt;p&gt;But compared to previous abstractions in programming, agentic coding is a very leaky abstraction. It’s not deterministic like a compiler, and slight variations in input or model can give very different results. That has led people to compare AI to “junior engineers”, since those are also not deterministic. But one difference is of course that people are capable of learning, without you having to endlessly tweak their AGENTS.md or SKILL.md files.&lt;/p&gt;
&lt;h3&gt;LLMs as an extension of copy-pasta from Stack Overflow&lt;/h3&gt;
&lt;p&gt;As such, the best analogy for using LLMs I’ve found so far is how a Google search used to behave. It was a skill all of us had to learn at some point: choosing just the right keywords, so that the right forum post (and later Stack Overflow post) would surface on the first Google results page. Just like prompting an LLM, in order to return the right assemblage of its training data, a fuzzy web search is a lookup in a very high-dimensional space. And just like with LLMs, the lookup used to be very sensitive to slight variations of wording, and changes to Google’s search index.&lt;/p&gt;
&lt;p&gt;In recent years, among other things, Google has changed the search to normalize entered terms much more aggressively. For people who were not versed in the dark art of Google-fu, this made the search much easier to use. But for those of us that had acquired that skill, it made Google search much less powerful. Specialized keywords used to bring us directly to an answer. Now they get normalized to a synonym, or to a closely associated word, and we land on a more generic page.&lt;/p&gt;
&lt;p&gt;But the advent of Google, and later Stack Overflow, irreversibly changed programming. Instead of reading the f***ing manual, programmers could now just blindly copy &amp;amp; paste answers from Stack Overflow, and surprisingly often got something that kind of worked. Seen through this lens, LLMs are just a continuation of the same trend: tools and abstractions that make people that know what they’re doing slightly faster, and enable people who don’t really know what they’re doing to arrive at something that often kind of works. And you know what? That’s great!&lt;/p&gt;
&lt;p&gt;But we shouldn’t fool ourselves: at some point the abstraction will leak. And then somebody has to invest the time to actually deeply understand what’s going on – and fix it. Just like we taught junior programmers to read and understand the Stack Overflow answer before using it, now we need to teach people to read and understand the stuff the LLM spits out, and to understand how it fits into the existing codebase.&lt;/p&gt;
&lt;h2&gt;Does quality matter?&lt;/h2&gt;
&lt;p&gt;Unfortunately, some programmers never progressed to the stage of trying to really understand the Stack Overflow answer. Why bother if it works? And while not publicly acknowledged, a lot of companies were actually happy with this approach. What’s different now is that companies go out of their way to publicly proclaim how much AI they’re using, without even pretending to look at the output.&lt;/p&gt;
&lt;p&gt;While there are definitely &lt;a href=&quot;https://htmx.org/essays/yes-and/&quot;&gt;valid use-cases for LLMs&lt;/a&gt;, there are also lots of new ways to mess up your code, and to mess up your organization’s communication and processes. This seems especially challenging to figure out as a team. Just like with code review, there are widely &lt;a href=&quot;https://ky.fyi/posts/ai-burnout&quot;&gt;differing views on how to use and integrate LLMs&lt;/a&gt; into our workflows (if at all). And if the team is not aligned on what things they value, this can really throw a wrench in the works.&lt;/p&gt;
&lt;p&gt;It’s also a sad fact of life that a lot of companies are doing very well, even though they’re churning out abysmal software. Despite what we programmers would like to believe, business success and software quality are very rarely correlated. Usually, other factors simply dominate. Often, software projects are treated as black boxes, known to fail about as often as they succeed, and are derisked in various ways (in the worst case, a different team will have another go at it).&lt;/p&gt;
&lt;p&gt;And it’s been the same for frontend development. Unfortunately, a terrible website has a relatively small impact on the bottom line. Does a slow website and lots of cookie banners hurt conversion? Sure, but that effect is relatively small compared to other factors like brand loyalty and pricing. And all the competitors have slow websites as well! Besides, nobody was ever fired for choosing React.&lt;/p&gt;
&lt;p&gt;Does that mean we should stop caring about our users and about our craft? No. But it does mean that it’s become even harder to find a job where you’re allowed to do so. Hopefully, the pendulum will swing back a bit, once the hype has blown over, and we’ve a better understanding what tasks LLMs are actually a good fit for and what not. But it’s safe to say that our profession will not be the same as before.&lt;/p&gt;
&lt;h2&gt;The Bauhaus movement&lt;/h2&gt;
&lt;p&gt;What did previous generations of craftspeople do when everyday goods and buildings suddenly could be mass-produced by industrial processes? One reaction was to copy the style of old, and make the industry crank out widgets and buildings that at least looked like they were handcrafted.&lt;/p&gt;
&lt;p&gt;Countering this trend of &lt;a href=&quot;https://en.wikipedia.org/wiki/Historicism&quot;&gt;historicism&lt;/a&gt;, an alternative approach was developed by the &lt;a href=&quot;https://en.wikipedia.org/wiki/Bauhaus&quot;&gt;Bauhaus movement&lt;/a&gt; of the early 20th century. Instead of pitting factory workers against craftspeople, their stated goal was to have them work together, and redevelop the arts and crafts with industrial manufacturing processes in mind. The Bauhaus urged designers to go back into the workshops, and work with the materials themselves. Still with the goal of arriving at designs that would then be mass produced. But always keeping the end user and mind, and deeply caring about them. Modern industrial design, as exemplified by Dieter Rams and Johnathan Ive, can trace its roots straight back to the Bauhaus.&lt;/p&gt;
&lt;h2&gt;Caring about quality and the user&lt;/h2&gt;
&lt;p&gt;How can we translate this line of thinking to software?&lt;/p&gt;
&lt;p&gt;Software sits somewhere in-between craft on one hand (the program we write gets shipped “as is” to our users, without first going through a manufacturing step), and industrial design on the other hand (we ship the same thing to potentially thousands of users, who we never get to see interacting with our product).&lt;/p&gt;
&lt;p&gt;The need for being able to write code by hand is clear. Just like industrial designers need to know the materials their products will be made of, a web designer needs to be intimately familiar with HTML and CSS.&lt;/p&gt;
&lt;p&gt;While it’s great that tools like Google, Stack Overflow, &lt;a href=&quot;https://jessitron.com/2020/08/04/back-when-software-was-a-craft/&quot;&gt;ready-to-use libraries&lt;/a&gt;, and now LLMs are making things easier for beginners, this also means that the natural barrier to get anything working at all is continuously being lowered.&lt;/p&gt;
&lt;p&gt;While there is a high barrier to entry to a field, it’s difficult to find absolutely terrible pieces of work. Once a craftsman was taught how to build a wooden chair, they invariably were also taught how to not do a terrible job at it.&lt;/p&gt;
&lt;p&gt;The industrialization enabled lots of cheap plastic products, designed by people who didn’t take the time to think how they would be used and by whom – yet good industrial design is still a thing. The invention of the word processor enabled lots of terribly formatted documents – yet typography and graphic design still exist. And software like Wix and Next.js enabled the creation of lots of websites that load terribly slow and are not accessible – yet there are still practitioners of the front of the frontend out there. Likewise, AI is enabling lots of AI slop – but this doesn’t mean we don’t still need people who know what they’re doing, and who care about what they’re doing.&lt;/p&gt;
&lt;h2&gt;How will it shake out?&lt;/h2&gt;
&lt;p&gt;But like in other industries, doing things properly will become an ever smaller slice of the pie. But because it’s now easier and cheaper to do so, the size of the pie will continue to grow. It’s very hard to say at this point in time whether the absolute number of people, being payed to do things well, will increase or decrease. If you ask me, there are way too many poorly designed plastic products out there. And it’s a sad fact that designing new type faces is not a sustainable full-time job anymore. But at the same time, there is still so much great work being done in all those domains.&lt;/p&gt;
&lt;p&gt;And you know what? Sometimes churning out a quick prototype or MVP is the right thing to do. If you don’t have product-market-fit yet, quick iteration and learning is more important than future-proofing everything. But you need to know what you’re trying to learn, and how you validate those learnings. And when the time has come, it’s usually better to take a step back and do things right from the start. For example, good performance is &lt;a href=&quot;https://calendar.perfplanet.com/2025/fast-by-default/&quot;&gt;very hard to achieve in a badly architected frontend later&lt;/a&gt;. And it&#39;s easier to start with a simple stack and add functionality later on than the reverse. Both of these, &lt;a href=&quot;/docs/why-mastro/&quot;&gt;Mastro explicitly encourages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For any part of the system, you need to know what tradeoffs you’re making, and then decide whether you should buy a service, use an open source library, have an LLM churn it out, or write it yourself. When the hype has died down, the industry will realize it’s just one more tool in the toolbox. But until then, we’re going to see a lot of ugly things: ugly code, broken communications, and awful layoffs under the guise of AI.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-04-16-typescript-full-stack-web-framework-for-agents/</id>
      <title>TypeScript full-stack web framework for agents and humans</title>
      <updated>2026-04-16T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-04-16-typescript-full-stack-web-framework-for-agents/" />
      
      <content type="html">&lt;p&gt;I would be lying if I said the &lt;a href=&quot;/&quot;&gt;Mastro web framework&lt;/a&gt; was designed from the ground up for AI agents. It was designed for humans, and to be as simple and minimal as possible, while still offering a great DX (developer experience). But it turns out, these properties make it exceptionally well-suited for AI agents as well.&lt;/p&gt;
&lt;p&gt;The whole &lt;a href=&quot;https://github.com/mastrojs/mastro/tree/main/src&quot;&gt;source code of Mastro&lt;/a&gt; is just ~800 lines of TypeScript, which easily fits into an LLM’s context (or a human’s). And just like types prevent humans from making stupid mistakes, they do the same for LLMs.&lt;/p&gt;
&lt;p&gt;To say that AI in general, and agentic coding in particular, are controversial topics may be an understatement. I’m not here to tell you what to think about AI. Personally, I see a number of downsides, but also some potential. LLMs can produce a lot of slop and terrible code. But when used properly, they can also be a big help, especially &lt;a href=&quot;https://htmx.org/essays/yes-and/&quot;&gt;when learning&lt;/a&gt;. I have hopes for local models, but as of today, they’re not quite where Claude Code and Codex seem to be.&lt;/p&gt;
&lt;h2&gt;Agentic coding a website&lt;/h2&gt;
&lt;p&gt;The best way to get to know something new is usually to just try it out. Always wanted to create or overhaul your personal website or blog? Now is the time! Or just create a silly dummy website!&lt;/p&gt;
&lt;p&gt;Create a &lt;a href=&quot;/#powerful-for-experienced-developers&quot;&gt;new Mastro project&lt;/a&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pnpm create @mastrojs/mastro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following &lt;code&gt;AGENTS.md&lt;/code&gt; file worked quite well for me:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;We&#39;re using the Mastro web framework to build a statically generated site.

- To look up Mastro docs, use web search on
  `https://api.github.com/repos/mastrojs/mastrojs.github.io/contents/data/docs`
- Use web search with `site:developer.mozilla.org` to look up web platform
  features. Use the APIs if they&#39;re &amp;quot;Baseline Widely available&amp;quot;.
- Write semantic HTML.
- CSS
  - Never add inline CSS
  - Avoid adding classes if you can use HTML element selectors
  - Use a [two-layer approach](https://theadminbar.com/semantics-and-primitives-color-system/)
    for CSS variables (primitives like `--blue: #0000ff`,
    and semantics like `--link-color: var(--blue)`).
  - Ask the user whether they want to only have a single global
    [styles.css](routes/styles.css) file (good for smaller projects),
    or whether they want to colocate CSS files with their HTML
    in [components](components/) and [bundle the CSS](https://mastrojs.github.io/guide/bundling-assets/#bundling-css).
- Only use [client-side JavaScript](https://mastrojs.github.io/guide/interactivity-with-javascript-in-the-browser/)
  if absolutely necessary. If so, use `&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;`.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: if you’re using Claude, make sure you also have a file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;@AGENTS.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if you&#39;re not using Chrome for anything important (that you wouldn&#39;t be comfortable sharing with Anthropic), you can install the &lt;a href=&quot;https://claude.com/claude-for-chrome&quot;&gt;Claude Chrome extension&lt;/a&gt;. Then you can tell it for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;@browser go to localhost:8000 and check for discrepancies
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How did it go?&lt;/h2&gt;
&lt;p&gt;Does that setup work for you? What did you change in your &lt;code&gt;AGENTS.md&lt;/code&gt; file? Let us know &lt;a href=&quot;https://bsky.app/profile/mastrojs.bsky.social&quot;&gt;on Bluesky&lt;/a&gt; or &lt;a href=&quot;https://github.com/mastrojs/mastro/discussions/categories/general&quot;&gt;GitHub&lt;/a&gt;!&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-03-09-whatever-happened-to-js-service-workers/</id>
      <title>Whatever happened to JavaScript Service Workers?</title>
      <updated>2026-03-09T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-03-09-whatever-happened-to-js-service-workers/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;What&#39;s the right way to think about Service Workers in 2026? Are they HTTP proxies, offline-capable web apps, or free frontend servers?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not to be confused with Web Workers, which are relatively simple background threads for a website, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers&quot;&gt;Service Workers&lt;/a&gt; are a special kind of Web Worker. They have been available in all major browsers since 2017, but after a couple of years of hype, they largely disappeared from the web discourse again. Let’s fix that!&lt;/p&gt;
&lt;p&gt;It’s sometimes a bit hard to grasp what Service Workers exactly are and what use-cases they enable. First, let’s look at three different views.&lt;/p&gt;
&lt;h2&gt;Service Workers as network request proxies&lt;/h2&gt;
&lt;p&gt;This may be technically the most correct view (which is the best kind of correct).&lt;/p&gt;
&lt;p&gt;After visiting a website with a service worker, it’s automagically installed in the user’s browser. From then on, it can intercept network requests from that website and handle them, e.g. acting as an HTTP proxy that caches network requests.&lt;/p&gt;
&lt;p&gt;There are various caching strategies to choose from, like downloading everything upfront, only using the cache when there is no network connection, stale-while-revalidate, and many more.&lt;/p&gt;
&lt;h2&gt;Service Workers as offline-capable apps&lt;/h2&gt;
&lt;p&gt;Yet service workers are not limited to caching resources as they are. Since they are implemented in JavaScript, they can also generate HTML pages on-the-fly – either by reading data from an on-device cache (like the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage&quot;&gt;CacheStorage&lt;/a&gt; or &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API&quot;&gt;IndexedDB&lt;/a&gt;), or by reading from a JSON API server.&lt;/p&gt;
&lt;p&gt;In this architecture, the service worker code is often called the “app shell”. It can be updated independently from, and less often than the content of the app – which is typically text and images.&lt;/p&gt;
&lt;p&gt;If that sounds a lot like a native mobile app that periodically pulls in content, or like a Single-Page-App (&lt;a href=&quot;/guide/client-side-vs-server-side-javascript-static-vs-ondemand-spa-vs-mpa/#spa-vs-mpa&quot;&gt;SPA&lt;/a&gt;), you’re not wrong. All three kinds of apps run code on-device in order to render some kind of GUI.&lt;/p&gt;
&lt;p&gt;Like mobile apps – but unlike SPAs – Service Worker apps work even when the device is offline – but only once they’re installed and have downloaded all the data they need to work offline.&lt;/p&gt;
&lt;p&gt;Finally, if you add a manifest file with the app’s home-screen icon, you have a PWA – a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps&quot;&gt;Progressive Web App&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Service Workers as running a free frontend server on the client&lt;/h2&gt;
&lt;p&gt;Making a request to a JSON API and rendering the response to HTML can equally be seen as basically what a stateless frontend server does. But while you’ve got to pay someone to keep your server running, a service worker is free forever!&lt;/p&gt;
&lt;p&gt;While an edge network (e.g. Cloudflare Workers) is closer to your users than a traditional server, nothing beats already being installed in your user’s browser. But yes, the service worker first needs to be installed. This can be as easy as having a static loading page, or having the user install the PWA first.&lt;/p&gt;
&lt;p&gt;But sometimes you just need that first page load to be already server-side rendered and up-to-date. The good news is that this is totally doable. The bad news is that it does add a bunch of complexity. It’s a problem anyone who’s been trying to server-side render SPAs (e.g. with Next.js) is familiar with – although a service worker is much more similar to a server environment than a browser window with its DOM and state.&lt;/p&gt;
&lt;p&gt;Of course, there are things you can do in a browser window that you cannot do in a service worker. For example triggering animations, playing audio, or displaying popups. While a lot can be done with modern CSS alone, you may sometimes still need client-side JavaScript inside the browser window for that kind of interactivity. But intercepting a page navigation and rendering the HTML in the client – the quintessential SPA – is much better done off the main thread and inside a service worker. That even lets you &lt;a href=&quot;/blog/2026-01-13-html-http-streaming/&quot;&gt;stream in the HTML&lt;/a&gt;, so that users can start interacting with the top of your page, while the bottom is still loading. And those interactions will be buttery smooth, since no JavaScript is blocking the main thread.&lt;/p&gt;
&lt;h2&gt;What’s the catch?&lt;/h2&gt;
&lt;p&gt;That all sounds great. And the &lt;a href=&quot;https://almanac.httparchive.org/en/2025/pwa#service-worker&quot;&gt;2025 Web Almanac says&lt;/a&gt; that of the top 1000 pages by page rank, roughly 30% are managed by a service worker – which is more than I expected. There is definitely value here for developers of established websites. But why isn&#39;t every website running a service worker?&lt;/p&gt;
&lt;p&gt;The whole thing comes at the cost of having to figure out an installation, updating and content caching strategy. This is probably the primary reason service workers are not in such widespread use as originally imagined. Because cache invalidation is just really hard to reason about.&lt;/p&gt;
&lt;p&gt;A cache does speed things up and enable offline use-cases. But it can also serve outdated data – potentially forever. Most web developers are used to their changes taking effect immediately. Without getting deep into HTTP caching headers, the default on the web is for things to update within a few minutes or hours.&lt;/p&gt;
&lt;p&gt;The default with service workers is that they may update only after 24 hours – and unless you call &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting&quot;&gt;&lt;code&gt;skipWaiting()&lt;/code&gt;&lt;/a&gt;, only after all existing tabs are closed and a new tab is opened. The reason for this is the cache that may be shared by different versions of the service worker. But I&#39;d argue it&#39;s still a bad default. While there are ready-made solutions like &lt;a href=&quot;https://developer.chrome.com/docs/workbox/&quot;&gt;Workbox&lt;/a&gt;, not everyone wants to install a library that may be somewhat of a black box.&lt;/p&gt;
&lt;p&gt;I&#39;m thinking of putting some effort into one or two Mastro starter templates with service workers with good defaults. Perhaps one for the &amp;quot;offline-capable app&amp;quot; use-case, and one for the &amp;quot;frontend server on the client&amp;quot; use-case. What do you think, would that be useful? Or do you have experience implementing a sort of service worker kill-switch? Let us know &lt;a href=&quot;https://github.com/mastrojs/mastro/discussions/categories/general&quot;&gt;on GitHub&lt;/a&gt; or &lt;code&gt;@&lt;/code&gt; us &lt;a href=&quot;https://bsky.app/profile/mastrojs.bsky.social&quot;&gt;on Bluesky&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What does Mastro bring to the table for Service Workers?&lt;/h2&gt;
&lt;p&gt;Mastro is primarily a server-side web framework and static site generator. But since it’s so lightweight, it’s also well suited to do routing and HTML-rendering inside a service worker.&lt;/p&gt;
&lt;p&gt;Mastro works well for either of the two architectures outlined above: an “app shell” that always renders everything in the service worker, or an “isomorphic” app that renders the initial page on the server (or Cloudflare Worker), and subsequent page navigations in the Service Worker – sharing all the code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To get started, use this simple &lt;a href=&quot;https://github.com/mastrojs/mastro/tree/main/examples/bundled-service-worker&quot;&gt;Mastro Service Worker example project&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-01-29-everything-is-a-route-one-interface-for-servers-static-sites-and-assets/</id>
      <title>Everything is a route: how to use one interface for servers, static-site- and asset-generation</title>
      <updated>2026-01-29T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-01-29-everything-is-a-route-one-interface-for-servers-static-sites-and-assets/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;In Unix, everything is a file. In Mastro, everything is a route. You use the same standards-based Request/Response-API not only for writing your server, but also for static site and asset generation. Let me show you how.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Request/Response-API is the &lt;a href=&quot;https://marvinh.dev/blog/modern-way-to-write-javascript-servers/&quot;&gt;modern way to write JavaScript servers&lt;/a&gt;. For example to return &lt;code&gt;text/plain&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const myHandler = async (req: Request) =&amp;gt; {
  return new Response(&amp;quot;Hello world!&amp;quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s such a great interface that Mastro decided to go all-in on it, and use it for all of the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1.-server-side-rendering&quot;&gt;on-demand server-side rendering&lt;/a&gt; (SSR) of HTML pages, JSON REST APIs, RSS feeds, etc.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2.-static-site-generation&quot;&gt;static site generation&lt;/a&gt; (SSG)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3.-asset-generation&quot;&gt;asset generation&lt;/a&gt; of resized images, CSS/JS bundles etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. Server-side rendering&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/guide/client-side-vs-server-side-javascript-static-vs-ondemand-spa-vs-mpa/#static-site-generation-vs-running-a-server&quot;&gt;Server-side rendering&lt;/a&gt; is the obvious part. After all, that’s the use-case that popularized the Request/Response-API (e.g. when writing Cloudflare Workers). In Mastro, this looks as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { html, htmlToResponse } from &amp;quot;@mastrojs/mastro&amp;quot;;
import { Layout } from &amp;quot;../components/Layout.ts&amp;quot;;

export const GET = () =&amp;gt;
  htmlToResponse(
    Layout({
      title: &amp;quot;Hi&amp;quot;,
      children: html`&amp;lt;p&amp;gt;Hello world!&amp;lt;/p&amp;gt;`,
    })
  );

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But even then, a lot of frameworks only use this Request/Response-API for JSON REST APIs, 404s and redirects, but not for normal HTML pages. For the 200-OK HTML case, this gets rid of the &lt;code&gt;htmlToResponse&lt;/code&gt; function call – but it makes the other cases &lt;a href=&quot;https://github.com/withastro/astro/issues/14684&quot;&gt;awkward&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;2. Static site generation&lt;/h2&gt;
&lt;p&gt;During &lt;a href=&quot;/guide/client-side-vs-server-side-javascript-static-vs-ondemand-spa-vs-mpa/#static-site-generation-vs-running-a-server&quot;&gt;static site generation&lt;/a&gt;, all the HTML of the website is generated upfront before being deployed to a static file server or CDN. In a way, it’s the inverse of on-demand server-side rendering.&lt;/p&gt;
&lt;p&gt;But if you forget in what order things happen, it is in fact a strict subset of the cases you encounter when on-demand rendering: the url-path of a GET request will fully determine what static file is served – query parameters and HTTP headers are ignored. As such, using a full Request object may be overkill.&lt;/p&gt;
&lt;p&gt;But as a web developer, having one unified interface is nice. You don&#39;t have to switch back and forth between different ways of doing things. And it allows you to effortlessly change a route from being server-side rendered to being statically pregenerated. To do so, the only thing you need to add to the file above is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const pregenerate = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your whole site it statically generated, you don’t even need to add that line.&lt;/p&gt;
&lt;p&gt;When you execute the Mastro generate script, it will call all your route handlers with synthetic requests and create files from the output. (Tangent: to write side-effect-free unit tests for these route handlers, you can do the same: simply call the handler with a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Request/Request&quot;&gt;&lt;code&gt;new Request(url)&lt;/code&gt;&lt;/a&gt;. This requires no mocking, or spinning up a whole browser like with Playwright.)&lt;/p&gt;
&lt;p&gt;Regardless of whether you have a server or a static site in production, for development you need a local server. As a bonus, this way of defining pages lets us use the production server also for local development. This ensures there are no differences between your development setup and production. The only difference is that for local development, Node.js/Deno/Bun are called with the &lt;code&gt;--watch&lt;/code&gt; flag to leverage their built-in file watcher.&lt;/p&gt;
&lt;h2&gt;3. Asset generation&lt;/h2&gt;
&lt;p&gt;Most web frameworks have some sort of functionality to pregenerate static assets that are expensive to compute, like for example resized images, or bundled CSS or JavaScript. In Ruby on Rails for example, this is called the &lt;em&gt;asset pipeline&lt;/em&gt;. In Vite-based frameworks it&#39;s usually a series of Vite plugins.&lt;/p&gt;
&lt;p&gt;But what are assets if not simply pregenerated static files? Indeed, from Mastro’s point of view, it doesn’t matter at all whether you want to pregenerate an HTML file (i.e. static site generation), or a CSS or JavaScript file (i.e. asset generation) – or even a binary file like a transformed image. Just slap that &lt;code&gt;pregenerate = true&lt;/code&gt; line from above on your asset route, and you can use Mastro to precompute it before starting your server. And again, if your complete website is static, you don’t even need that line.&lt;/p&gt;
&lt;p&gt;For example, to generate a &lt;code&gt;styles.css&lt;/code&gt; file containing the bundled CSS of all your components, create a route file named &lt;code&gt;styles.css.server.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { findFiles, readTextFile } from &amp;quot;@mastrojs/mastro&amp;quot;;

export const GET = async () =&amp;gt; {
  const files = await findFiles(&amp;quot;components/**/*.css&amp;quot;);
  const contents = await Promise.all(files.map(readTextFile));
  return new Response(
    contents.join(&amp;quot;\n\n&amp;quot;),
    { headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;text/css&amp;quot; } },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since the interface is the same as for any other HTTP route, if you know web standard HTML, you already know how to load this into your HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;/styles.css&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Isn’t that beautiful? You can also extend this approach to do &lt;a href=&quot;/blog/2026-05-26-component-scoped-css-without-build-step/&quot;&gt;component-scoped CSS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And again, we get the snappy development server for free, which computes the bundle on the fly and lazily. If you don&#39;t load &lt;code&gt;/styles.css&lt;/code&gt;, it isn&#39;t computed either.&lt;/p&gt;
&lt;p&gt;The same approach works for any other kind of content, e.g. for images. Using the &lt;a href=&quot;https://jsr.io/@mastrojs/images&quot;&gt;&lt;code&gt;@mastrojs/images&lt;/code&gt; package&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { createImagesRoute } from &amp;quot;@mastrojs/images&amp;quot;;

export const { GET, getStaticPaths } = createImagesRoute({
  hero: {
    transform: (image) =&amp;gt; image.resize(300, 300),
  },
  hero2x: {
    transform: (image) =&amp;gt; image.resize(600, 600),
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This declares two image presets: &lt;code&gt;hero&lt;/code&gt; and &lt;code&gt;hero2x&lt;/code&gt;. Assuming you have a file &lt;code&gt;images/blue-marble.jpg&lt;/code&gt;, you would request resized versions in WebP format as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img alt=&amp;quot;Planet Earth&amp;quot;
  src=&amp;quot;/_images/hero/blue-marble.jpg.webp&amp;quot;
  srcset=&amp;quot;/_images/hero2x/blue-marble.jpg.webp 2x&amp;quot;
  &amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The beautiful thing here is that we didn&#39;t have to build image transformations into Mastro itself, nor does Mastro have a complex plugin API that we&#39;d need to keep compatible. &lt;code&gt;@mastrojs/images&lt;/code&gt; is an independent package, that simply exports the &lt;code&gt;GET&lt;/code&gt; function that adheres to the standard Request/Response-API. (It also exports the &lt;code&gt;getStaticPaths&lt;/code&gt; function, which is Mastro-specific, but that just returns the paths used for static site generation as strings – hardly a complex API.)&lt;/p&gt;
&lt;p&gt;Similarly, to bundle JavaScript, we could call e.g. &lt;code&gt;esbuild&lt;/code&gt; in the route handler. For an example of that, have a look at the &lt;a href=&quot;/guide/bundling-assets/&quot;&gt;bundling and assets chapter in the Mastro Guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By inverting the flow of a classic asset pipeline, and leveraging the standards-based Request/Response-API, we managed to unify the development server, production server, static site generation, and asset generation mechanisms. That&#39;s how Mastro gets by with just &lt;a href=&quot;https://github.com/mastrojs/mastro/tree/main/src#readme&quot;&gt;~800 lines of implementation code&lt;/a&gt;.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2026-01-13-html-http-streaming/</id>
      <title>Improve Time to First Byte by streaming your HTML</title>
      <updated>2026-01-13T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2026-01-13-html-http-streaming/" />
      
      <content type="html">&lt;p&gt;If you have a statically generated website hosted on a CDN, it’s probably very fast (unless you add too much client-side JavaScript).&lt;/p&gt;
&lt;p&gt;However, for dynamically generated pages that load content from a database, one of the most overlooked ways to speed up the perceived page load speed is HTTP streaming.&lt;/p&gt;
&lt;p&gt;Unless you do streaming on your server, when you load rows from your database, it’s usually loaded into an array (or a similar structure), and only when the whole database query is done, the server continues assembling the HTML. And only when the whole page’s HTML is completely assembled, the server starts sending it over the wire to the browser. In JavaScript, this usually has the &lt;code&gt;await&lt;/code&gt; keyword in front of the &lt;code&gt;db.query()&lt;/code&gt; call. This is especially slow if you have a slow database, or a database call returning many rows.&lt;/p&gt;
&lt;h2&gt;Streaming&lt;/h2&gt;
&lt;p&gt;Enter HTTP streaming. In HTTP/1.1, this was known as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding&quot;&gt;chunked transfer encoding&lt;/a&gt; (some of you may remember it from PHP’s &lt;code&gt;flush()&lt;/code&gt; function). But in HTTP/2 and HTTP/3, streaming is built in at the lower levels of the protocol.&lt;/p&gt;
&lt;p&gt;The idea is simple: if for example the header of our page doesn’t depend on a database call, we can immediately send it over the wire to the browser. This dramatically improves &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Time_to_first_byte&quot;&gt;time to first byte&lt;/a&gt;, and users can start reading the top of our page, while our server is still waiting for the database.&lt;/p&gt;
&lt;p&gt;But there is more: if we have a page displaying hundreds or even thousands of rows from our database (perhaps a table, dashboard, or just your blog’s index page), we can also stream the database’s response, and display the HTML that was generated form the first result row, while the last row hasn&#39;t even left the database yet. All we have to do is not block the streaming anywhere in the whole chain from the database driver, to the HTML templates, all the way to your web hosting provider and CDN proxy. If there’s an &lt;code&gt;await&lt;/code&gt; or similar anywhere in that chain, which blocks until the whole page is loaded, then it cannot be streamed.&lt;/p&gt;
&lt;h2&gt;JavaScript/TypeScript example&lt;/h2&gt;
&lt;p&gt;Let’s look at a simple example using the &lt;a href=&quot;https://mastrojs.github.io/&quot;&gt;Mastro&lt;/a&gt; server framework to generate the HTML, and &lt;a href=&quot;https://www.kysely.dev/&quot;&gt;Kysely&lt;/a&gt; to query the database. Here’s the page handler for an HTTP GET to our page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { html, htmlToResponse } from &amp;quot;@mastrojs/mastro&amp;quot;;
import { Layout } from &amp;quot;../components/Layout.ts&amp;quot;;
import { db } from &amp;quot;../db/db.ts&amp;quot;;
import { mapIterable } from &amp;quot;../db/iterable.ts&amp;quot;;

export const GET = (req: Request) =&amp;gt; {
  const rows = db
    .selectFrom(&amp;quot;person&amp;quot;)
    .select([&amp;quot;first_name&amp;quot;])
    .where(&amp;quot;gender&amp;quot;, &amp;quot;=&amp;quot;, &amp;quot;woman&amp;quot;)
    .stream();
  return htmlToResponse(
    Layout({
      title: &amp;quot;Hello World&amp;quot;,
      children: html`
        &amp;lt;p&amp;gt;Welcome!&amp;lt;/p&amp;gt;
        &amp;lt;ul&amp;gt;
          ${mapIterable(rows, (row) =&amp;gt;
            html`&amp;lt;li&amp;gt;${row.first_name}&amp;lt;/li&amp;gt;`)}
        &amp;lt;/ul&amp;gt;
      `,
    }),
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that we’re doing &lt;code&gt;rows = db.stream()&lt;/code&gt; instead of &lt;code&gt;rows = await db.execute()&lt;/code&gt;. While the latter would return an array, the former returns a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols&quot;&gt;JavaScript Iterable&lt;/a&gt;. More specifically, an &lt;code&gt;AsyncIterable&lt;/code&gt; containing Promises.&lt;/p&gt;
&lt;p&gt;While JavaScript already has standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/map&quot;&gt;Iterator helpers&lt;/a&gt;, &lt;a href=&quot;https://github.com/tc39/proposal-async-iterator-helpers&quot;&gt;Async Iterator Helpers&lt;/a&gt; are unfortunately still a work in progress. Thus we had to roll our own &lt;code&gt;mapIterable&lt;/code&gt; functions, which loops the Iterable just like you would &lt;code&gt;map&lt;/code&gt; over an array. But importantly, it streams results through as they come in: the result of the &lt;code&gt;mapIterable&lt;/code&gt; is again an &lt;code&gt;AsyncIterable&lt;/code&gt;, which can be passed to the &lt;code&gt;html&lt;/code&gt; template. Finally, the &lt;code&gt;htmlToResponse&lt;/code&gt; function constructs a standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response&quot;&gt;JavaScript Response&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Feel free to check out and play around with the &lt;a href=&quot;https://github.com/mastrojs/mastro/tree/main/examples/postgresql&quot;&gt;whole example project&lt;/a&gt;. Finally, to stream JSON data as server-sent events, see &lt;a href=&quot;/guide/caching-service-workers-streaming/#http-streaming&quot;&gt;HTTP Streaming in the guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This article was originally published on the Web Performance Calendar. Read the &lt;a href=&quot;https://calendar.perfplanet.com/2025/improve-ttfb-and-ux-with-http-streaming/&quot;&gt;original article&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2025-11-27-why-not-just-use-inline-styles-tailwind/</id>
      <title>Why not just use &lt;del&gt;inline styles&lt;/del&gt; Tailwind?</title>
      <updated>2025-11-27T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2025-11-27-why-not-just-use-inline-styles-tailwind/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;Are WYSIWYG word processors, inline styles, and Tailwind conceptually the same? How to make the best use of modern CSS and HTML elements? And what do we actually gain by adding abstractions like style names and components, and what do we lose?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are many different ways to render text to pixels using a computer. Let’s go through some! We&#39;ll start with the lower level ones (closer to the hardware, or at least to the characters), and then subsequently add more and more abstractions, also known as adding &lt;em&gt;levels of indirection&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Direct control&lt;/h2&gt;
&lt;p&gt;If you’ve ever changed the font-size and font-family of individual words or paragraphs of text in a &lt;a href=&quot;https://en.wikipedia.org/wiki/WYSIWYG&quot;&gt;WYSIWYG&lt;/a&gt; word processor like Microsoft Word, you’re familiar with this approach to layouting.&lt;/p&gt;
&lt;p&gt;In web development, we’ve also had several iterations of this same basic and direct interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the deprecated &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/font&quot;&gt;HTML font element&lt;/a&gt; &lt;code&gt;&amp;lt;font size=&amp;quot;7&amp;quot;&amp;gt;Hi&amp;lt;/font&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CSS inline styles &lt;code&gt;&amp;lt;span style=&amp;quot;font-size: 20px&amp;quot;&amp;gt;Hi&amp;lt;/span&amp;gt;&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;Tailwind: &lt;code&gt;&amp;lt;span class=&amp;quot;text-xl&amp;quot;&amp;gt;Hi&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The nice thing about it is that it gives you direct and immediate control over the text you’re looking at right now. The not so nice thing about it is that if you’re not careful, it  quickly leads to an inconsistent mess of different styles.&lt;/p&gt;
&lt;h2&gt;Reusable styles&lt;/h2&gt;
&lt;p&gt;To solve these inconsistencies, we add our first abstraction; our first level on indirection: we give names to different styles that we then reuse in different places. This ensures all those places look the same. If we change the style, all places that use will automatically change.&lt;/p&gt;
&lt;p&gt;In desktop publishing software like Adobe Indesign, you set up character- and paragraph-styles. In LaTeX you add a bunch of macros. For the web, CSS was invented as a solution for this very problem – to replace the &lt;code&gt;&amp;lt;font&amp;gt;&lt;/code&gt; tag. In web development circles, this idea is known as the “separation of concerns” – where you separate between content (written in semantic HTML), and layout/styling (written in CSS).&lt;/p&gt;
&lt;h2&gt;Reusable components&lt;/h2&gt;
&lt;p&gt;But people kept building ever more complicated websites, and chunks of HTML were repeated in various places, but slightly inconsistent. So we added another level of indirection: components (or in older templating systems called &lt;em&gt;includes&lt;/em&gt; or &lt;em&gt;partials&lt;/em&gt;) – blocks of several HTML elements that you can reuse in multiple places. (Conceptually similar to a function that you can call in multiple places.)&lt;/p&gt;
&lt;p&gt;It’s a powerful, and often very useful, abstraction. In fact, it’s so powerful that people started to use it everywhere for everything. And since everything was a component now, people wondered why they still needed the abstraction that was CSS. If the only place you were using a &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; tag in your whole codebase was in the &lt;code&gt;&amp;lt;Title&amp;gt;&lt;/code&gt; component – then why not add the styles right there? That’s one reason why Tailwind is so popular.&lt;/p&gt;
&lt;p&gt;And to be fair, if everything is a component, then why not? But should everything be a component? Do I need a &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; component if I have a perfectly good &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; HTML element? The answer is, as always, it depends. Perhaps if it’s a very, very complicated button, with lots of different variants. But didn’t we want a consistent layout?&lt;/p&gt;
&lt;p&gt;The one gripe I have with components is that they are usually a build-time abstraction: you don’t see them anymore in the browser’s dev tools element inspector. Unless you use web components, but then you have to deal with the shadow DOM, which is its own, hard to penetrate, level of indirection. Or unless you use your framework’s custom developer tools extension, but there you usually don’t see the HTML elements anymore, &lt;em&gt;only&lt;/em&gt; your components.&lt;/p&gt;
&lt;h2&gt;Modern HTML and CSS&lt;/h2&gt;
&lt;p&gt;Meanwhile, HTML and CSS have not stood still. There are plenty more semantic HTML elements nowadays (like &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;search&amp;gt;&lt;/code&gt;, etc.), and you can style them with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Attribute_selectors&quot;&gt;attribute selectors&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has&quot;&gt;parent selectors&lt;/a&gt;, and &lt;a href=&quot;https://mastrojs.github.io/guide/css/#want-to-learn-more-css%3F&quot;&gt;much more&lt;/a&gt;. But to use those effectively, you need to be aware of exactly what HTML elements you have on your page, and how they’re nested. And if you overuse components, that abstraction makes it harder to see directly in your code what HTML you&#39;ll have in your browser.&lt;/p&gt;
&lt;p&gt;To be clear, I’m not saying you should never use components. But I’d recommend going with the least powerful abstraction that still solves your problem. And more often than not, this is semantic HTML with a little bit of CSS. Indeed, there is a danger with CSS that you can get yourself into a mess if you don&#39;t restrict yourself enough. Since CSS is so powerful, sometimes you have to be mindful of every character. It pays off to adhere to a few principles like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Child_combinator&quot;&gt;direct child combinator&lt;/a&gt; &lt;code&gt;&amp;gt;&lt;/code&gt; instead of the space wherever possible. (The space selects all children, regardless how deeply nested.)&lt;/li&gt;
&lt;li&gt;Don’t invent new classes for everything, but use element selectors where possible. (Read this &lt;a href=&quot;https://www.smashingmagazine.com/2016/11/css-inheritance-cascade-global-scope-new-old-worst-best-friends/&quot;&gt;great piece by Heydon Pickering&lt;/a&gt; for elaboration of this point.)&lt;/li&gt;
&lt;li&gt;Take a bit of time to set up a few &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties&quot;&gt;CSS variables&lt;/a&gt;, then restrict yourself to using those instead of magic numbers for spacing and sizes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And sure, maybe you’ll need to factor out a few things to reusable components once your website grows. And you may even want to place their CSS file in the same folder as the component, and use the name of the component as a class. (In HTML5, class attributes are case-sensitive. So one way to mark your classes as “tied to a component” is to uppercase them.) But that doesn&#39;t mean you need a bundler or baroque build step: if you have many CSS files and are worried about performance, you can just &lt;a href=&quot;https://mastrojs.github.io/guide/bundling-assets/#bundling-css&quot;&gt;concatenate them&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Data and reusable templates&lt;/h2&gt;
&lt;p&gt;On another front, long before there was any talk about components, people wanted to reuse the same content on different pages. Possibly the simplest example is to reuse the titles of your blog posts on the index page of your blog. It would be annoying having to keep the index page and the detail pages manually in sync by always updating both, when you change the title of a blog post.&lt;/p&gt;
&lt;p&gt;Thus, another level of indirection was introduced. Instead of having plain HTML files, you have some kind of separate data format: either a database, or maybe just plain markdown and YAML or JSON files.&lt;/p&gt;
&lt;p&gt;As with any of the levels of indirection we’ve been talking about here, this especially shines for &lt;em&gt;high-volume&lt;/em&gt; productions (i.e. your website has lots of content). And you want all that content to be laid out &lt;em&gt;consistently&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Jeff Eaton has a &lt;a href=&quot;https://aarhus24.boye-co.com/wp-content/uploads/2024/01/CMS-Kickoff-2024-Buried-In-Blocks.pdf&quot;&gt;wonderful slide deck about CMSes&lt;/a&gt; with this grid in it:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;low volume&lt;/th&gt;
&lt;th&gt;high volume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;consistent&lt;/td&gt;
&lt;td&gt;piece of cake&lt;/td&gt;
&lt;td&gt;structure, templates, APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unpredictable&lt;/td&gt;
&lt;td&gt;custom design, development&lt;/td&gt;
&lt;td&gt;here be dragons&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you’re doing &lt;em&gt;low volume, unpredictable&lt;/em&gt; stuff, all these levels of indirection might only get in your way. Why separate content and layout, and why reuse styles, if you only have a handful of things to style anyway? Then you might be better off just using a graphical tool like Webflow, or slapping those inline-styles or &lt;a href=&quot;https://dev.to/toboreeee/its-almost-2026-why-are-we-still-arguing-about-css-vs-tailwind-291f&quot;&gt;Tailwind classes&lt;/a&gt; on your stuff.&lt;/p&gt;
&lt;h2&gt;Tailwind&lt;/h2&gt;
&lt;p&gt;Did I just say Tailwind is conceptually the same as inline-styles? Yes. Tailwind’s answer to &lt;a href=&quot;https://v3.tailwindcss.com/docs/utility-first#why-not-just-use-inline-styles&quot;&gt;why not just use inline styles?&lt;/a&gt; says something about a &amp;quot;predefined design system&amp;quot; (which you can easily set up with CSS variables as well), and about responsive/hover/focus (which are trivial in plain CSS). But they don’t seem to disagree with the claim that at least conceptually, Tailwind is equivalent to inline styles.&lt;/p&gt;
&lt;p&gt;What Tailwind unfortunately does impose is a build step, which is another giant level of indirection – especially since it&#39;s not a simple transform, but comes with a lot of logic and non-standard CSS keywords like &lt;code&gt;@theme&lt;/code&gt; and &lt;code&gt;@utility&lt;/code&gt;. And if you inspect an element in your browser&#39;s developer tools to debug something, you&#39;ll have to understand all that indirection as well.&lt;/p&gt;
&lt;p&gt;But there are plenty of utility-class CSS frameworks out there that don&#39;t require a build step, if that&#39;s what you&#39;re after. Whether the elimination of unused CSS is worth the build step is for you to decide. But I wouldn’t sweat the CSS size too much. Consider how big your HTML payload is anyway with all those utility classes, static CSS usually has fairly long cache live-times set, and finally both compress well over gzip/brotli.&lt;/p&gt;
&lt;h2&gt;Levels of indirection&lt;/h2&gt;
&lt;p&gt;What’s the take-away of all this? You may have heard this &lt;a href=&quot;https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering&quot;&gt;quote&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We can solve any problem by introducing an extra level of indirection – except the problem of having too many levels of indirection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The difficulty is knowing in which situation you find yourself today.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2025-11-16-generate-og-image-canvas/</id>
      <title>How to generate og:images from text with Canvas</title>
      <updated>2025-11-16T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2025-11-16-generate-og-image-canvas/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;Use the web-standard Canvas API to render text to PNG – without spinning up a whole web browser. Here&#39;s how we implemented this approach in &lt;a href=&quot;https://jsr.io/@mastrojs/og-image&quot;&gt;&lt;code&gt;@mastrojs/og-image&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A few weeks ago, Scott Jehl posted about the &lt;a href=&quot;https://scottjehl.com/posts/open-graph/&quot;&gt;pain of generating Open Graph images&lt;/a&gt;: those images you link to with an &lt;code&gt;og:image&lt;/code&gt; meta tag from your HTML, which are then displayed on social media or messenger apps when somebody shares a link to that page. (Side-note: a single &lt;code&gt;&amp;lt;meta property=&amp;quot;og:image&amp;quot;&lt;/code&gt; tag, together with the standard &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;meta name=&amp;quot;description&amp;quot;&lt;/code&gt;, is kind of &lt;a href=&quot;https://getoutofmyhead.dev/link-preview-meta-tags/&quot;&gt;enough&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Most tools that generate those preview PNGs download and launch a full-blown web browser to take a screenshot of some HTML and CSS that you wrote. While that works, I&#39;ve had something lighter in mind. All I wanted, was to render the title of the page on a nice-looking background. That sounded doable with the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/&quot;&gt;standard Canvas API&lt;/a&gt;. Since we don&#39;t actually have a browser running when we statically generte our website (d&#39;uh), I used &lt;a href=&quot;https://www.npmjs.com/package/canvaskit-wasm&quot;&gt;canvaskit-wasm&lt;/a&gt;, which emulates a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; in Node.js or Deno.&lt;/p&gt;
&lt;p&gt;Now we just have to call &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillText&quot;&gt;fillText(text, x, y)&lt;/a&gt; and we&#39;re done, right? Not quite. &lt;code&gt;fillText&lt;/code&gt; only ever prints a single line of text. What we want is to start a new line when there&#39;s no more horizontal space. We need to call &lt;code&gt;fillText&lt;/code&gt; again with as much of the remaining text as fits on the next line (and with a y-coordinate of one line-height more than before). And then we continue doing that until we have no more text. If that sounds suspiciously like a layout algorithm to you, you would be right. We even added support for &lt;a href=&quot;https://en.wikipedia.org/wiki/Soft_hyphen&quot;&gt;soft hyphens&lt;/a&gt;: if the word doesn&#39;t fit, but contains at least one soft hyphen, we will hyphenate at the right place. But since we didn&#39;t feel like shipping hyphenation dictionaries, you&#39;ll need to supply your own soft hyphens to the input.&lt;/p&gt;
&lt;p&gt;Feel free to look at the &lt;a href=&quot;https://github.com/mastrojs/og-image/&quot;&gt;source code&lt;/a&gt;. That whole text layouting part is in the &lt;code&gt;util.ts&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Next up was publishing it as the &lt;a href=&quot;https://jsr.io/@mastrojs/og-image&quot;&gt;@mastrojs/og-image package&lt;/a&gt;. There you can also see usage examples. The single exported function takes a bunch of options like font, color and padding, to get you started. But in typical Mastro fashion, the API is more powerful than it seems at first glance: using &lt;code&gt;background&lt;/code&gt;, you can supply your own function to draw the background image using the standard Canvas API. If you come up with a nice background generating function, please share it on &lt;a href=&quot;https://github.com/mastrojs/mastro/discussions/categories/show-and-tell&quot;&gt;GitHub Discussions&lt;/a&gt;. That would be almost like a theme gallery!&lt;/p&gt;
&lt;p&gt;Last step for me was to use the package for this very website: for this blog, Mastro docs, and the guide. Running &lt;code&gt;deno task generate&lt;/code&gt; to generate the whole static site, including rasterizing and writing to disk the 42 &lt;code&gt;og.png&lt;/code&gt; files, takes 2.6 seconds on my MacBook Air M2. Without the image generation, it was 0.6 seconds. I&#39;m not sure how long it takes to spin up a browser and take screenshots, but pretty sure it&#39;s longer.&lt;/p&gt;
&lt;p&gt;Don&#39;t forget to share this post on social media to actually see our glorious, generated image 🤓 If on Bluesky, please mention us with &lt;a href=&quot;https://bsky.app/profile/mastrojs.bsky.social&quot;&gt;@mastrojs.bsky.social&lt;/a&gt;!&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2025-11-06-migration-from-express/</id>
      <title>How to incrementally migrate from Express to the standard Request/Response API</title>
      <updated>2025-11-06T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2025-11-06-migration-from-express/" />
      
      <content type="html">&lt;p&gt;You want to migrate to &lt;a href=&quot;https://marvinh.dev/blog/modern-way-to-write-javascript-servers/&quot;&gt;the modern way to write JavaScript servers&lt;/a&gt;, which is &lt;a href=&quot;https://blog.val.town/blog/the-api-we-forgot-to-name/&quot;&gt;supported across JavaScript runtimes&lt;/a&gt;. &lt;code&gt;Deno.serve&lt;/code&gt;, &lt;code&gt;Bun.serve&lt;/code&gt;, Cloudflare Workers etc. – they all use the same standards-based &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Request&quot;&gt;&lt;code&gt;Request&lt;/code&gt;&lt;/a&gt;/&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response&quot;&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/a&gt; API for route handlers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const handler = (req: Request) =&amp;gt; {
  return new Response(`Hello ${req.url}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But how do you incrementally migrate from your existing Express app? Simple: start by adding your new server handlers to your existing Express server using the &lt;code&gt;node-fetch-server&lt;/code&gt; conversion wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createRequestListener } from &amp;quot;@remix-run/node-fetch-server&amp;quot;;
import express from &amp;quot;express&amp;quot;;

import { handler } from &amp;quot;./new-server.ts&amp;quot;;

const app = express();

// here go legacy express routes you haven&#39;t migrated yet

app.all(&amp;quot;*rest&amp;quot;, async (req, res) =&amp;gt; {
  const listener = createRequestListener(handler);
  return listener(req, res);
})

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows you to incrementally move over each route from the express way of doing things to the new Request/Response API. Crucially, it allows you to have both old and new route running in the same server in production, so you don&#39;t have to stop everything and your team can continue to ship new features and bug fixes during the migration.&lt;/p&gt;
&lt;p&gt;Finally, when all routes are migrated, you can remove Express from your codebase. For example, using Deno:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { handler } from &amp;quot;./new-server.ts&amp;quot;;

Deno.serve(handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migrating to Mastro&lt;/h2&gt;
&lt;p&gt;If you&#39;re migrating to the &lt;a href=&quot;/&quot;&gt;Mastro framework&lt;/a&gt;, you would replace &lt;code&gt;new-server.ts&lt;/code&gt; in the above examples with importing the Mastro server. During the migration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import mastro from &amp;quot;@mastrojs/mastro/server&amp;quot;;
import { createRequestListener } from &amp;quot;@remix-run/node-fetch-server&amp;quot;;
import express from &amp;quot;express&amp;quot;;

const app = express();

// here go legacy express routes you haven&#39;t migrated yet

app.all(&amp;quot;*rest&amp;quot;, async (req, res) =&amp;gt; {
  const listener = createRequestListener(mastro.fetch);
  return listener(req, res);
})

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then when you&#39;ve removed Express but are still on Node.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import mastro from &amp;quot;@mastrojs/mastro/server&amp;quot;;
import { createRequestListener } from &amp;quot;@remix-run/node-fetch-server&amp;quot;;
import * as http from &amp;quot;node:http&amp;quot;;

const server = http.createServer(createRequestListener(mastro.fetch));
server.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or when you&#39;ve migrated to Deno:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import mastro from &amp;quot;@mastrojs/mastro/server&amp;quot;;

Deno.serve(mastro.fetch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Congratulations and welcome to the future!&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2025-10-29-what-struggled-with-porting-mastro-to-bun/</id>
      <title>What I learned porting Mastro to Bun</title>
      <updated>2025-10-29T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2025-10-29-what-struggled-with-porting-mastro-to-bun/" />
      
      <content type="html">&lt;p&gt;In our last blog post, I talked about &lt;a href=&quot;/blog/2025-10-27-what-learned-porting-from-deno-to-node-js/&quot;&gt;What I learned porting Mastro from Deno to Node.js&lt;/a&gt;. The next step was of course to get Mastro working on Bun as well.&lt;/p&gt;
&lt;p&gt;And again, there were some gotchas that took me some time to resolve. Let’s go through them.&lt;/p&gt;
&lt;h2&gt;fs.glob&lt;/h2&gt;
&lt;p&gt;The first thing I immediately ran into was easy to understand. Calling the Node.js &lt;code&gt;fs.glob&lt;/code&gt; function with the &lt;code&gt;withFileTypes&lt;/code&gt; option in Bun, while type-checking fine, results in a “not-implemented” runtime error. There’s a &lt;a href=&quot;https://github.com/oven-sh/bun/issues/22018&quot;&gt;Bun GitHub issue&lt;/a&gt; about that, with @robobun making a half-hearted attempt at a PR. You might wonder who @robobun is, and looking at their profile, it says “the official helper for Bun” – appears to be some AI bot.&lt;/p&gt;
&lt;p&gt;Anyway, I just resorted to calling &lt;code&gt;fs.glob&lt;/code&gt; without the &lt;code&gt;withFileTypes&lt;/code&gt; option when in Bun, and making a call to &lt;code&gt;stat&lt;/code&gt; for each file, to determine whether it’s a folder or a file.&lt;/p&gt;
&lt;h2&gt;URLPattern&lt;/h2&gt;
&lt;p&gt;Next one up. While all three major browsers, Deno, and Node.js now support &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URLPattern&quot;&gt;URLPattern&lt;/a&gt;, there is still an open &lt;a href=&quot;https://github.com/oven-sh/bun/issues/2286&quot;&gt;issue about it in Bun&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was also relatively easy to fix by loading the &lt;a href=&quot;https://www.npmjs.com/package/urlpattern-polyfill&quot;&gt;urlpattern-polyfill&lt;/a&gt;, which I added as a dependency to &lt;a href=&quot;https://github.com/mastrojs/template-basic-bun&quot;&gt;Mastro’s Bun starter template&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;bun create&lt;/h2&gt;
&lt;p&gt;The first tricky one was that running&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bun create @mastrojs/mastro@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;failed to detect that we were running in Bun, and would download the wrong template – even when I checked for &lt;code&gt;typeof Bun&lt;/code&gt; or &lt;code&gt;process.versions.bun&lt;/code&gt;, which are the &lt;a href=&quot;https://bun.com/guides/util/detect-bun&quot;&gt;official ways&lt;/a&gt; to detect that.&lt;/p&gt;
&lt;p&gt;But after publishing that package a few times, waiting for it to propagate through the CDNs, and running the command again, I was certain: neither of these work when inside &lt;code&gt;bun create&lt;/code&gt;. Probably they&#39;re shimming it somehow to fake Node.js compatibility. But I couldn&#39;t find any official docs on that.&lt;/p&gt;
&lt;p&gt;Thus we do what other such packages seem to do:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;process.env.npm_config_user_agent?.startsWith(&amp;quot;bun&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bun.write&lt;/h2&gt;
&lt;p&gt;When generating the static files, I wanted to call &lt;code&gt;Bun.write&lt;/code&gt; instead of the &lt;a href=&quot;/blog/2025-10-27-what-learned-porting-from-deno-to-node-js/#deno-namespace-adieu&quot;&gt;weird Node.js code&lt;/a&gt;. That was the second weird gotcha: &lt;a href=&quot;https://bun.com/reference/bun/write&quot;&gt;Bun.write&lt;/a&gt; apparently doesn&#39;t take a standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream&quot;&gt;&lt;code&gt;ReadableStream&lt;/code&gt;&lt;/a&gt;, only: &lt;code&gt;string | ArrayBufferLike | TypedArray&amp;lt;ArrayBufferLike&amp;gt; | Blob | BlobPart[]&lt;/code&gt;. Just to be sure, I tried passing it a &lt;code&gt;ReadableStream&lt;/code&gt;. It didn’t fail at runtime, rejoice! But wait. What do we find when opening the generated file in a text editor? The file contains the contents &lt;code&gt;[object ReadableStream]&lt;/code&gt; 🤡&lt;/p&gt;
&lt;p&gt;So what does that mean? Does &lt;code&gt;Bun.write&lt;/code&gt; just not support streaming into a file? Will it always have to wait for the whole thing to be written into a Buffer in memory first? The &lt;a href=&quot;https://bun.com/docs/guides/write-file/stream&quot;&gt;official workaround&lt;/a&gt; is to go through a &lt;code&gt;Response&lt;/code&gt; object, which might or might not stream 🤷🏽‍♂️&lt;/p&gt;
&lt;p&gt;Be that as it may, I already had a &lt;code&gt;Response&lt;/code&gt; object coming from the Mastro route handler. So I decided to special-case it and directly pass it that &lt;code&gt;Response&lt;/code&gt; object (instead of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response/body&quot;&gt;&lt;code&gt;response.body&lt;/code&gt;&lt;/a&gt;, which is a &lt;code&gt;ReadableStream&lt;/code&gt;). Then I ran the program. And... nothing. Like, literally nothing happened. It didn’t print “Generated static site and wrote to generated/ folder” like Mastro usually does. And indeed, while it created the folder, it was empty. But exit code was zero.&lt;/p&gt;
&lt;p&gt;So I put some &lt;code&gt;console.log&lt;/code&gt;s and an additional try-catch around it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;console.log(&amp;quot;before&amp;quot;);
try {
  const nrBytesWritten = await Bun.write(outFilePath, response);
  console.log(&amp;quot;after&amp;quot;, nrBytesWritten);
} catch (e) {
  console.log(&amp;quot;caught&amp;quot;, e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above logged &amp;quot;before&amp;quot;, and again exited with an exit code of zero. Yes, it just silently seemed to crash. Wow.&lt;/p&gt;
&lt;p&gt;For all that the &lt;code&gt;Bun.write&lt;/code&gt; docs like to advertise &amp;quot;the fastest syscalls available to copy from input into destination&amp;quot;, that doesn&#39;t help a whole lot if it doesn&#39;t work properly.&lt;/p&gt;
&lt;p&gt;And no, I cannot be bothered to create a reproducible test case right now. For the record, this was in Bun v1.3.1 on macOS 15.7.1, and I ran it in the &lt;a href=&quot;https://github.com/mastrojs/template-basic-bun&quot;&gt;Mastro template for Bun&lt;/a&gt; after editing &lt;code&gt;node_modules/@mastrojs/mastro/src/generator.js&lt;/code&gt; directly. It might have something to do with &lt;code&gt;response&lt;/code&gt; coming from a dynamically imported module (the route handler), or I don&#39;t know. I am happy to accept pull requests though, should anybody feel called to make &lt;code&gt;Bun.write&lt;/code&gt; work in Mastro.&lt;/p&gt;
&lt;p&gt;But for now, the moral of the story seems to be once again to just use Node.js&#39;s old-school and verbose API. At least that works – even in Bun.&lt;/p&gt;
&lt;p&gt;Either way, it would actually be interesting to update &lt;a href=&quot;https://github.com/mb21/bench-framework-markdown/commit/87e5713b01d298394f866ec3cb86da46db910ada&quot;&gt;our benchmark&lt;/a&gt; to see whether streaming the &lt;code&gt;Response.body&lt;/code&gt; into the file is actually really faster and/or consumes less memory, compared to serializing to a string first, and test it in Deno, Node.js and Bun.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While those were weird, the good news is that overall, supporting Bun required very few &lt;a href=&quot;https://github.com/mastrojs/mastro/commit/9073c2059471a1dcb796378d8c459be0adf6b6a3&quot;&gt;code changes to Mastro&lt;/a&gt;.&lt;/p&gt;
</content>
    </entry>
  
    <entry>
      <id>https://mastrojs.github.io/blog/2025-10-27-what-learned-porting-from-deno-to-node-js/</id>
      <title>What I learned porting Mastro from Deno to Node.js</title>
      <updated>2025-10-27T12:00:00.000Z</updated>
      
    <author>
      <name>Mauro Bieg</name>
      
      
    </author>
  
      <link rel="alternate" href="https://mastrojs.github.io/blog/2025-10-27-what-learned-porting-from-deno-to-node-js/" />
      
      <content type="html">&lt;p&gt;&lt;strong&gt;The Mastro web framework and static site generator initially ran only in the browser and Deno – it was living in the future, so to speak. Here&#39;s the story of how I ported it to Node.js (which has a surprising amount of functionality built-in nowadays), and what I would do differently next time.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first part of Mastro that I coded was actually &lt;a href=&quot;https://mastrojs.github.io/reactive/&quot;&gt;Reactive Mastro&lt;/a&gt; (a tiny reactive GUI library), which needs to run only in the browser. Then, I reused that minimal way to construct HTML in the &lt;a href=&quot;https://mastrojs.github.io/&quot;&gt;Mastro&lt;/a&gt; web framework and static site generator.&lt;/p&gt;
&lt;p&gt;I started developing Mastro on Deno. The idea was (and still is) to build a modern tool from first principles – with minimal dependencies. Deno’s extensive standard library, early adoption of web standards like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API&quot;&gt;URLPattern&lt;/a&gt;, and builtin TypeScript support, made it a relatively easy choice. It was like building an MVP of Mastro in a future version of the JavaScript ecosystem. I don’t even remember whether Bun was already around at that time, or whether Deno’s philosophy of a green-field approach just more appealed to me for my green-field project. At that time, I didn’t know yet whether Mastro would be a one-off experiment, or whether it would turn into something with legs. But after getting it running on Deno, I found it had promise.&lt;/p&gt;
&lt;p&gt;In just ~800 lines of TypeScript, I had something that covered like 90% of my use-cases when building websites. It was a very nice bonus that I got the static site generator running as a &lt;em&gt;VSCode for the Web&lt;/em&gt; extension, running completely inside the browser. (&lt;a href=&quot;https://vscode.dev/github/mastrojs/template-basic&quot;&gt;Try it online&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Deno 2.0 and Node.js compatibility&lt;/h2&gt;
&lt;p&gt;Then Bun and Deno 2.0 happened, having extensive Node.js compatibility, and even &lt;a href=&quot;https://blog.cloudflare.com/nodejs-workers-2025/&quot;&gt;Cloudflare joined in&lt;/a&gt;. The Node.js builtins seem to have become the standard library across JavaScript server runtimes – for better or worse. And to be fair, the commitment of the Node.js project to stability is actually something I’d like to see more of in the JavaScript ecosystem.&lt;/p&gt;
&lt;p&gt;With Node.js now supporting TypeScript natively, I started to wonder: how much work would it take to make Mastro run in Node.js? And would the result still be minimal, maintainable, and without a build-step? Or would it be too riddled with compromises and if-Deno-else-Node cases?&lt;/p&gt;
&lt;h2&gt;Deno namespace adieu&lt;/h2&gt;
&lt;p&gt;So I got to work. Targeting Node.js v24 LTS, we got builtin TypeScript type-stripping, &lt;code&gt;--watch&lt;/code&gt; flag, a test runner, and &lt;code&gt;URLPattern&lt;/code&gt; support. The first step was obviously to remove all calls to functions in the &lt;code&gt;Deno&lt;/code&gt; namespace (like &lt;code&gt;Deno.readTextFile&lt;/code&gt;), and replace them with the Node.js equivalents, which run just fine in Deno as well. This was mostly painless.&lt;/p&gt;
&lt;p&gt;Pretty much the only case where I decided to still use a Deno-only function when it was available was for writing a stream to a file. Because look at that – please let me know if there&#39;s a better way!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const writeFile = async (path: string, data: ReadableStream&amp;lt;Uint8Array&amp;gt;) =&amp;gt; {
  if (typeof Deno === &amp;quot;object&amp;quot;) {
    return Deno.writeFile(path, data);
  } else {
    const { createWriteStream } = await import(&amp;quot;node:fs&amp;quot;);
    const { Readable } = await import(&amp;quot;node:stream&amp;quot;);
    return new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt;
      Readable.fromWeb(data as any)
        .pipe(createWriteStream(path))
        .on(&amp;quot;finish&amp;quot;, resolve)
        .on(&amp;quot;error&amp;quot;, reject)
    );
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Mastro’s route handlers return a standard Response object, and all we have to do now when generating a static file is to call &lt;code&gt;writeFile(&amp;quot;index.html&amp;quot;, response.body)&lt;/code&gt; (using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response/body&quot;&gt;&lt;code&gt;response.body&lt;/code&gt; property&lt;/a&gt;), which seems extremely lean. Now let&#39;s look at the case of running a server (either for local development, or for production).&lt;/p&gt;
&lt;h2&gt;Polyfilling Request/Response API&lt;/h2&gt;
&lt;p&gt;Using the standards-based &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; API for route handlers was one of the very first design decisions for Mastro. Val Town’s Steve Krouse called this &lt;a href=&quot;https://blog.val.town/blog/the-api-we-forgot-to-name/&quot;&gt;the API we forgot to name&lt;/a&gt;, and Marvin Hagemeister more recently &lt;a href=&quot;https://marvinh.dev/blog/modern-way-to-write-javascript-servers/&quot;&gt;the modern way to write JavaScript servers&lt;/a&gt;. Now, on Deno, you can pass such a handler to &lt;code&gt;Deno.serve&lt;/code&gt;, and you’re pretty much done: it will start a server, and you can open your website in the browser.&lt;/p&gt;
&lt;p&gt;Unfortunately, this is not something Node.js supports (and &lt;a href=&quot;https://github.com/nodejs/help/issues/4174&quot;&gt;apparently has no plans to&lt;/a&gt;). Thus we need a polyfill: fortunately, the &lt;a href=&quot;https://www.npmjs.com/package/@remix-run/node-fetch-server&quot;&gt;&lt;code&gt;@remix-run/node-fetch-server&lt;/code&gt; package&lt;/a&gt; does exactly that, seems high-quality, and has no dependencies itself. In keeping with Mastro’s philosophy of exposing primitives and simple helper functions wherever possible, I decided to simple add &lt;code&gt;node-fetch-server&lt;/code&gt; to &lt;a href=&quot;https://github.com/mastrojs/template-basic-node&quot;&gt;Mastro’s Node.js starter template&lt;/a&gt;, where people can configure it to their liking, or even swap it out with something else. Check it out in the &lt;code&gt;server.ts&lt;/code&gt; file, it’s still relatively bare-bones – even if not quite as clean as in &lt;a href=&quot;https://github.com/mastrojs/template-basic-deno&quot;&gt;Mastro’s Deno starter template&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;HTTP imports and stdlib&lt;/h2&gt;
&lt;p&gt;Node.js also doesn’t support http imports. In fact, they recently &lt;a href=&quot;https://github.com/nodejs/node/pull/53822&quot;&gt;reverted the &lt;code&gt;--experimental-network-imports&lt;/code&gt; flag&lt;/a&gt;, because it wasn’t clear how this would work within Node’s security model. This forced me to reorganize some code, and move some non-core Mastro helpers out to &lt;a href=&quot;https://jsr.io/@mastrojs&quot;&gt;their own packages&lt;/a&gt;. Arguably, this was for the best anyway, as it makes the modular nature of Mastro clearer. These packages are each only a single file, wrapping a carefully chosen external dependency.&lt;/p&gt;
&lt;p&gt;The last two functions from Deno’s standard library that I needed were &lt;a href=&quot;https://jsr.io/@std/media-types/doc/~/contentType&quot;&gt;&lt;code&gt;contentType&lt;/code&gt;&lt;/a&gt; from &lt;code&gt;@std/media-types&lt;/code&gt; and &lt;a href=&quot;https://jsr.io/@std/http/doc/~/serveFile&quot;&gt;&lt;code&gt;serveFile&lt;/code&gt;&lt;/a&gt; from &lt;code&gt;@std/http&lt;/code&gt;. The former wasn’t a problem, because that package is marked as Node.js-compatible. The latter I ended up simply copying into the Mastro codebase (with proper attribution). They are both pure functions that run on Node.js without any problems.&lt;/p&gt;
&lt;p&gt;And voila, we have Mastro fully running on Node.js! But hold on, we still need to publish it as a package somewhere.&lt;/p&gt;
&lt;h2&gt;NPM vs JSR&lt;/h2&gt;
&lt;p&gt;If you haven’t been following the Deno ecosystem, you may be surprised to hear that there’s an alternative to the NPM package registry now: &lt;a href=&quot;https://jsr.io/&quot;&gt;JSR&lt;/a&gt;. While its search absolutely sucks, its auto-generated docs pages are quite nice. And most importantly for me personally: I can simply push my TypeScript files to it, and it will make the transpiled JavaScript files available for consumption with Node.js. No need for me to install and update TypeScript, &lt;code&gt;@types/node&lt;/code&gt;, nor understand the various fields in &lt;code&gt;tsconfig.json&lt;/code&gt;. That&#39;s all contained in the &lt;code&gt;deno&lt;/code&gt; executable.&lt;/p&gt;
&lt;p&gt;While &lt;code&gt;pnpm&lt;/code&gt; is recommended to consume JSR packages, &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;yarn&lt;/code&gt; also work through a &lt;a href=&quot;https://jsr.io/docs/npm-compatibility&quot;&gt;compatibility layer&lt;/a&gt;. For the Mastro VSCode extension, I use &lt;a href=&quot;https://esm.sh/&quot;&gt;esm.sh&lt;/a&gt; to import the Mastro JSR package in the browser.&lt;/p&gt;
&lt;p&gt;Thus I have a very simple and lean setup for maintaining a single code-base that runs in the browser, Deno and Node.js – with almost no duplicated code. (After quite some fiddling and experimenting, Bun works as well now, but that&#39;s a story for another blog post.)&lt;/p&gt;
&lt;h2&gt;JSR and npx&lt;/h2&gt;
&lt;p&gt;To create a new Mastro project, I used to advertise &lt;code&gt;deno run -A jsr:@mastrojs/mastro@0.3.0/init&lt;/code&gt;, which would download and run the &lt;code&gt;init&lt;/code&gt; export directly from the &lt;code&gt;@mastrojs/mastro&lt;/code&gt; package from JSR and run it. Simple.&lt;/p&gt;
&lt;p&gt;But remember, Node.js doesn’t do scripts over HTTP. In the Node.js ecosystem it’s all &lt;code&gt;npx&lt;/code&gt; nowadays. Or apparently &lt;code&gt;npm create&lt;/code&gt;, which runs &lt;code&gt;npx&lt;/code&gt; and just prepends &lt;code&gt;create-&lt;/code&gt; to the package name?! But then at least you can do &lt;code&gt;pnpm create&lt;/code&gt; instead of &lt;code&gt;pnpm dlx&lt;/code&gt; 🙈 Either way, unfortunately JSR doesn’t support generating a &lt;code&gt;package.json&lt;/code&gt; with a &lt;a href=&quot;https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin&quot;&gt;&lt;code&gt;bin&lt;/code&gt; field&lt;/a&gt; – which is what &lt;code&gt;npx&lt;/code&gt; or &lt;code&gt;npm/pnpm create&lt;/code&gt; needs when you execute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm create @mastrojs/mastro@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus what I ended up doing was simply moving that script out to its own package, and publish that one to NPM. So now the above command works. Because I didn’t want to add TypeScript and a build step just for that, I opted to write &lt;a href=&quot;https://github.com/mastrojs/mastro/blob/main/create-mastro/index.js&quot;&gt;that script&lt;/a&gt; in plain JavaScript, put the TypeScript type annotations in JSDoc comments, and add a &lt;code&gt;//@ts-check&lt;/code&gt; at the top of the file. That way, I can run &lt;code&gt;deno check&lt;/code&gt; on it and it’s fully type-checked.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If I’d start a new project today that needs to run on many JavaScript engines, I’d obviously plan ahead a bit more, and use Node.js builtins over the Deno namespace where possible.&lt;/p&gt;
&lt;p&gt;But overall, I’m quite happy with how things turned out. Deno allowed me to “live in the future”, before the same features landed in Node.js. And for some things, this is still the case; like &lt;code&gt;Deno.serve&lt;/code&gt; (which you can polyfill), or not having to &lt;code&gt;npm install&lt;/code&gt; and deal with the &lt;code&gt;node_modules&lt;/code&gt; folder.&lt;/p&gt;
&lt;p&gt;I hope by reading this, you can avoid some of the pitfalls in your next project.&lt;/p&gt;
</content>
    </entry>
  
    </feed>
  