<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Peng.ly Blog</title><description>Occasional posts about code, security, and whatever else I&apos;ve been poking at</description><link>https://blog.peng.ly/</link><language>en-gb</language><item><title>The one inline script, and the hash that lets it run</title><link>https://blog.peng.ly/one-inline-script/</link><guid isPermaLink="true">https://blog.peng.ly/one-inline-script/</guid><description>Dark mode without a flash needs a script that runs before the page paints, and a strict CSP wants to know exactly which one. Here&apos;s how they get along.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The theme toggle has a classic problem. Your pick lives in &lt;code&gt;localStorage&lt;/code&gt;, and nothing can read it before the page paints unless a script runs first. Load that script the normal way and you get a flash of the wrong theme on every single page. Not exactly ideal.&lt;/p&gt;
&lt;h2 id=&quot;the-script&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/one-inline-script/#the-script&quot;&gt;The script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It goes inline in the &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt;, before any CSS, so it runs before anything gets drawn:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;figcaption&gt;src/lib/theme.ts&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; theme&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; localStorage&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.getItem&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;theme&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (theme) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;documentElement&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;dataset&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.theme &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; theme;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The real one’s squashed onto a single line, but it’s the same thing. The &lt;code&gt;try&lt;/code&gt; is there because browsers throw if you touch storage while cookies are blocked, and a theme isn’t worth breaking the page over.&lt;/p&gt;
&lt;h2 id=&quot;the-policy&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/one-inline-script/#the-policy&quot;&gt;The policy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This site sends a Content Security Policy that only lets scripts from its own origin run. An inline script breaks that rule, so the policy names this one by its SHA-256 hash:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-deleted)&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt; script-src &apos;self&apos; &apos;unsafe-inline&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-inserted)&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt; script-src &apos;self&apos; &apos;sha256-1Ub7...&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&apos;unsafe-inline&apos;&lt;/code&gt; would’ve worked too, but then any script that got injected into a page would run as well, which is the exact thing CSP is there to stop.&lt;/p&gt;
&lt;h3 id=&quot;keeping-the-two-in-step&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/one-inline-script/#keeping-the-two-in-step&quot;&gt;Keeping the two in step&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Change one character of the script and the hash stops matching. The browser blocks it, the console grumbles, and the theme starts flashing again. Nothing on the page tells you. So there’s a test that hashes the script and checks &lt;code&gt;vercel.json&lt;/code&gt; still has it:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;figcaption&gt;src/lib/theme.test.ts&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;vercel.json&apos;s CSP trusts the theme script as it is now&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.parse&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;vercel.json&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;utf8&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  // ...find the Content-Security-Policy header...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; hash&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; `&apos;sha256-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;createHash&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;sha256&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(themeScript)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.digest&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;base64&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;`&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;  assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.ok&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;csp&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.includes&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(hash)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; `script-src wants &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;hash&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;If it fails, the message has the new hash in it, ready to paste.&lt;/p&gt;
&lt;h2 id=&quot;everything-else&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/one-inline-script/#everything-else&quot;&gt;Everything else&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every other script here ships as its own file, so &lt;code&gt;&apos;self&apos;&lt;/code&gt; covers it. The JSON-LD blocks look like scripts but browsers never run them, so CSP leaves them alone. Styles get &lt;code&gt;&apos;unsafe-inline&apos;&lt;/code&gt;, because the syntax highlighting puts its colours in &lt;code&gt;style&lt;/code&gt; attributes, and an injected style is a much smaller worry than an injected script.&lt;/p&gt;
&lt;p&gt;You can see the whole policy in the response headers of this page, if you fancy a look.&lt;/p&gt;</content:encoded><category>security</category><category>astro</category></item><item><title>How a post gets written here</title><link>https://blog.peng.ly/how-a-post-gets-written-here/</link><guid isPermaLink="true">https://blog.peng.ly/how-a-post-gets-written-here/</guid><description>Folders, frontmatter, and the three MDX components worth the faff - callouts, figures with captions, and videos that only load when you ask.</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A post is a folder in &lt;code&gt;posts/&lt;/code&gt;, named after the address you want. Inside goes &lt;code&gt;index.md&lt;/code&gt;, or &lt;code&gt;index.mdx&lt;/code&gt; if you want components, plus any pictures. This one lives at &lt;code&gt;posts/how-a-post-gets-written-here/index.mdx&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-frontmatter&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#the-frontmatter&quot;&gt;The frontmatter&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every post starts with a block like this, and the build checks it. Leave out the description, put a capital in a tag or misspell a field, and the build stops and tells you what’s wrong.&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;figcaption&gt;index.md&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; How a post gets written here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; Between 50 and 160 characters, since that&amp;#39;s about what Google shows&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; 2026-10-08&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;updated&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; 2026-10-09 &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# optional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;tags&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;astro&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; mdx&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# lowercase, with hyphens if you need them&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;draft&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; # optional, drafts only show on the dev server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;cover&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; # optional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; ./cover.png&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  alt&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; What&amp;#39;s in the picture&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;---&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h2 id=&quot;callouts&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#callouts&quot;&gt;Callouts&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Four flavours, for the bits you’d otherwise put in bold and hope for the best.&lt;/p&gt;
&lt;div class=&quot;callout note&quot; role=&quot;note&quot;&gt;&lt;p class=&quot;title&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;9&quot;/&gt;&lt;path d=&quot;M12 8h.01M11 12h1v4h1&quot;/&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;div class=&quot;body&quot;&gt;A note, for context that’s handy but not essential.&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;callout tip&quot; role=&quot;note&quot;&gt;&lt;p class=&quot;title&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.2 1 2.1h5c0-.9.4-1.6 1-2.1A6 6 0 0 0 12 3z&quot;/&gt;&lt;/svg&gt;Pro tip&lt;/p&gt;&lt;div class=&quot;body&quot;&gt;The title’s optional, each type has its own.&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;callout warning&quot; role=&quot;note&quot;&gt;&lt;p class=&quot;title&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M12 4 2.5 20h19z&quot;/&gt;&lt;path d=&quot;M12 10v4M12 17h.01&quot;/&gt;&lt;/svg&gt;Heads up&lt;/p&gt;&lt;div class=&quot;body&quot;&gt;Something that’ll catch you out if you’re not paying attention.&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;callout danger&quot; role=&quot;note&quot;&gt;&lt;p class=&quot;title&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8.5 3h7L21 8.5v7L15.5 21h-7L3 15.5v-7z&quot;/&gt;&lt;path d=&quot;M12 8v5M12 16h.01&quot;/&gt;&lt;/svg&gt;Careful&lt;/p&gt;&lt;div class=&quot;body&quot;&gt;Something that breaks things, or leaks something it shouldn’t.&lt;/div&gt;&lt;/div&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;mdx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;lt;Callout type=&amp;quot;tip&amp;quot; title=&amp;quot;Pro tip&amp;quot;&amp;gt;The title&amp;#39;s optional, each type has its own.&amp;lt;/Callout&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;figures&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#figures&quot;&gt;Figures&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Pictures get turned into AVIF and WebP at a few, so a phone never downloads the desktop one. Wrap one in a &lt;code&gt;Figure&lt;/code&gt; and you get a caption as well.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot;&gt;&lt;source type=&quot;image/webp&quot;&gt;&lt;img src=&quot;https://blog.peng.ly/_astro/rail.CujaKaIE_Z24lKh9.png&quot; alt=&quot;A post page, with the date, reading time, tags and contents in a column to the left of the text&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-fit=&quot;cover&quot; data-astro-image-pos=&quot;center&quot; width=&quot;720&quot; height=&quot;409&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;&lt;p&gt;The rail, where the date, reading time, tags and contents live, out of the way of the words&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Plain Markdown images work too, they just don’t get a caption.&lt;/p&gt;
&lt;h2 id=&quot;video&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#video&quot;&gt;Video&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Embeds load nothing from YouTube until you press play, and even then it’s the no-cookie player. A video file sat next to the post works as well, with &lt;code&gt;src&lt;/code&gt; instead of &lt;code&gt;youtube&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;a class=&quot;facade&quot; href=&quot;https://www.youtube.com/watch?v=jNQXAC9IVRw&quot; data-youtube=&quot;jNQXAC9IVRw&quot;&gt;&lt;img src=&quot;https://blog.peng.ly/_astro/hqdefault_Z21rTW1.webp&quot; alt loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-fit=&quot;cover&quot; data-astro-image-pos=&quot;center&quot; width=&quot;480&quot; height=&quot;360&quot;&gt;&lt;span class=&quot;play&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8 5.5v13l10.5-6.5z&quot; fill=&quot;currentColor&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span&gt;Play &lt;span class=&quot;visually-hidden&quot;&gt;Me at the zoo, the first video ever uploaded to YouTube&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;figcaption&gt;Me at the zoo, the first video ever uploaded to YouTube&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;a class=&quot;facade&quot; href=&quot;https://blog.peng.ly/_astro/toggle.0Arorgn0.webm&quot; data-video=&quot;/_astro/toggle.0Arorgn0.webm&quot;&gt;&lt;img src=&quot;https://blog.peng.ly/_astro/toggle.DeE6Ruyf_Z24OGFa.webp&quot; alt loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-fit=&quot;cover&quot; data-astro-image-pos=&quot;center&quot; width=&quot;960&quot; height=&quot;540&quot;&gt;&lt;span class=&quot;play&quot;&gt;&lt;svg class=&quot;icon&quot; viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8 5.5v13l10.5-6.5z&quot; fill=&quot;currentColor&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span&gt;Play &lt;span class=&quot;visually-hidden&quot;&gt;The theme toggle on this blog, flipping between light and dark&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;figcaption&gt;The theme toggle on this blog, flipping between light and dark&lt;/figcaption&gt;&lt;/figure&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;mdx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;lt;Video youtube=&amp;quot;jNQXAC9IVRw&amp;quot; title=&amp;quot;Me at the zoo&amp;quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;lt;Video src={clip} title=&amp;quot;A clip that lives next to the post&amp;quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;code&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#code&quot;&gt;Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Highlighting happens at build time, so it costs nothing in the browser. Add a &lt;code&gt;title&lt;/code&gt; for a file name, and line numbers in braces to light some up.&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;figcaption&gt;src/lib/format.ts&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// a comfortable reading pace, and code blocks count like anything else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; readingTime&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(text&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; words&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; text&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.split&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;/\s&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.filter&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(Boolean).&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; Math&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.max&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; Math&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.round&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(words &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 230&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Every block gets a copy button once the page has loaded.&lt;/p&gt;
&lt;h2 id=&quot;publishing&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/how-a-post-gets-written-here/#publishing&quot;&gt;Publishing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Push to &lt;code&gt;main&lt;/code&gt; and Vercel does the rest. Drafts stay in the repo but never make it into the build, so they’re not in the feed, the sitemap or search either.&lt;/p&gt;</content:encoded><category>astro</category><category>mdx</category><category>writing</category></item><item><title>Noot noot, it&apos;s a blog</title><link>https://blog.peng.ly/hello-noot/</link><guid isPermaLink="true">https://blog.peng.ly/hello-noot/</guid><description>The first post, and a quick look round. Every post is a folder of Markdown, comes with a feed and a plain text copy, and there&apos;s a penguin.</description><pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the writing bit of &lt;a href=&quot;https://peng.ly&quot;&gt;peng.ly&lt;/a&gt;. Mostly code, security and whatever I’ve been poking at that week, written up so I remember it, and so you don’t have to find out the hard way.&lt;/p&gt;
&lt;h2 id=&quot;whats-here&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/hello-noot/#whats-here&quot;&gt;What’s here&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Each post lives in its own folder as plain Markdown, with any pictures sat right next to it. That’s the whole system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every post is tagged, and every tag gets &lt;a href=&quot;https://blog.peng.ly/tags/&quot;&gt;its own page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://blog.peng.ly/archive/&quot;&gt;archive&lt;/a&gt; has the lot, by year&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.peng.ly/search/&quot;&gt;Search&lt;/a&gt; runs in your browser, so nothing you type gets sent anywhere&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Spotted something wrong? The source for every post is &lt;a href=&quot;https://github.com/NotAFlightRisk/blog&quot;&gt;on GitHub&lt;/a&gt;, so a PR is very welcome.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;for-the-robots&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/hello-noot/#for-the-robots&quot;&gt;For the robots&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Plenty of what reads this stuff isn’t a person, so everything has a version a machine can pick up easily.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What&lt;/th&gt;&lt;th&gt;Where&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Full-text RSS&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://blog.peng.ly/rss.xml&quot;&gt;&lt;code&gt;/rss.xml&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A contents page for LLMs&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://blog.peng.ly/llms.txt&quot;&gt;&lt;code&gt;/llms.txt&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Every post in one file&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://blog.peng.ly/llms-full.txt&quot;&gt;&lt;code&gt;/llms-full.txt&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Any post as Markdown&lt;/td&gt;&lt;td&gt;its address, with &lt;code&gt;.md&lt;/code&gt; on the end&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sitemap&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://blog.peng.ly/sitemap-index.xml&quot;&gt;&lt;code&gt;/sitemap-index.xml&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;light-dark-or-whatever-your-system-says&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/hello-noot/#light-dark-or-whatever-your-system-says&quot;&gt;Light, dark, or whatever your system says&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The theme follows your system until you press the toggle up top, then it remembers your pick. Choose the one your system would’ve shown anyway and it goes back to following along.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The front page of the blog, in the light theme on the left and the dark one on the right&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1600&quot; height=&quot;559&quot; src=&quot;https://blog.peng.ly/_astro/light-and-dark.11V81Nb1_28GjLY.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;under-the-bonnet&quot;&gt;&lt;a href=&quot;https://blog.peng.ly/hello-noot/#under-the-bonnet&quot;&gt;Under the bonnet&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It’s &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, built to static files and served from Vercel. No JavaScript turns up unless a page needs it, and so far that’s search, the copy buttons on code and the theme toggle.&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;figcaption&gt;astro.config.ts&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; defineConfig&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  site&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;https://blog.peng.ly&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  trailingSlash&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;always&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  markdown&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Thanks for reading :)&lt;/p&gt;</content:encoded><category>meta</category><category>astro</category></item></channel></rss>