<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mateusz Muszarski</title>
    <description>The latest articles on DEV Community by Mateusz Muszarski (@muchassak).</description>
    <link>https://dev.to/muchassak</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177825%2F9516f0c3-66c5-4ba9-9fd0-d47ef79cee4b.jpg</url>
      <title>DEV Community: Mateusz Muszarski</title>
      <link>https://dev.to/muchassak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muchassak"/>
    <language>en</language>
    <item>
      <title>How I work with Claude Code: memory, gates and the mistakes that built them</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:26:36 +0000</pubDate>
      <link>https://dev.to/muchassak/how-i-work-with-claude-code-memory-gates-and-the-mistakes-that-built-them-1n8l</link>
      <guid>https://dev.to/muchassak/how-i-work-with-claude-code-memory-gates-and-the-mistakes-that-built-them-1n8l</guid>
      <description>&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://muszarski.com/en/blog/how-i-work-with-claude-code/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude Code has two habits that hurt on a long project: it forgets what you agreed once the context gets compacted, and it stops when the work looks done. So my setup does two jobs. It remembers for Claude (a wiki, a lessons file, three hooks), and it doesn't accept "done" until tests, gates and screenshots agree. Here is that setup, and the mistakes behind each piece.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; CLAUDE.md is a 76-line router into a wiki; hooks hand Claude the right pages for every prompt and bring the rules back after every compaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrections compound:&lt;/strong&gt; every correction becomes a one-line rule, and a rule that slips back becomes a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done means proven:&lt;/strong&gt; typecheck, tests, e2e, Lighthouse, and screenshots Claude has to open and look at, in every language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale:&lt;/strong&gt; subagents for research and fact-checks, background builds, and a few hard rules for parallel sessions on one repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post assumes you know the basics (CLAUDE.md, plan mode, &lt;code&gt;/clear&lt;/code&gt;); if not, the &lt;a href="https://code.claude.com/docs/en/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers them in ten minutes. Both habits above come from the docs: quality drops as the context window fills, a full window gets compacted into a summary, and without a check Claude can run, "you become the verification loop".&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it remember what I said last week?
&lt;/h2&gt;

&lt;p&gt;Not in CLAUDE.md alone. Mine is a router: a table that maps a task ("copy, wording, CTA labels") to the wiki page that holds the knowledge, plus the rules that must never break. The wiki holds the rest: 33 pages, plus a folder of research notes. Three hooks, configured in &lt;code&gt;.claude/settings.json&lt;/code&gt;, do the remembering:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SessionStart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;new chat, resume, &lt;code&gt;/clear&lt;/code&gt;, &lt;strong&gt;every compaction&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;injects a card: how we work, what's in flight in git, the lessons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every prompt&lt;/td&gt;
&lt;td&gt;scores the prompt against the routing table and injects up to 5 best-matching pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when Claude tries to finish&lt;/td&gt;
&lt;td&gt;blocks once if code changed but the wiki didn't&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is what Claude saw before it read my prompt "have it consistent over languages" while I was writing this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[project wiki] Before acting, read the pages that match this task:
- Locales, translation, Lingui, Polish, Spanish, language switcher,
  hreflang, URL prefix, slugs: .claude/project-info/site/i18n.md
Change behaviour a page describes -&amp;gt; update that page + append a line to
.claude/project-info/log.md in the same change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compaction part is the one that matters. Claude Code re-reads the root CLAUDE.md after a compaction, but not the pages it read along the way or what I said in chat. The card brings the rules back, because a script prints it again from files in git. How the wiki is laid out and how the router scores a prompt is in &lt;a href="https://muszarski.com/en/blog/llm-wiki-project-memory-for-claude-code/" rel="noopener noreferrer"&gt;An LLM wiki: project memory for Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a correction turn into a test?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Correction.&lt;/strong&gt; I tell Claude it got something wrong, or a tool trips it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule.&lt;/strong&gt; One dated line in lessons.md, in the imperative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every session.&lt;/strong&gt; The SessionStart hook injects the rules, after every compaction too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check.&lt;/strong&gt; A rule that slips back becomes a test, a lint rule or a hook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;↻ Every new check is one correction I never have to give again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The best example is a bug that never threw an error. Scroll-driven animations on this site are written in CSS. The build folded &lt;code&gt;animation&lt;/code&gt; and &lt;code&gt;animation-timeline&lt;/code&gt; into one shorthand that Chrome rejects whole, so the effect silently never ran. Nothing failed: the code looked right and Claude reported it done. On 2026-10-08 it became a rule: write these in longhands. By the next day three rules had slipped back to the shorthand anyway, so now a unit test reads every stylesheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;never use the animation shorthand next to animation-timeline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shorthand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;;&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;animation&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*:/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shorthand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule shapes what Claude tries. A test decides whether it shipped. Today's rule came from this very post: I asked for a rewrite and got an English-only draft, so now "en and pl posts are one story" sits in the lessons file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I hand it a big task?
&lt;/h2&gt;

&lt;p&gt;In slices. The site was built one section at a time, and each slice ran the same loop: research the section, build it with real content, check it (screenshots, gates, all three languages), fix the weakest point. The loop repeats until one more pass finds nothing worth fixing. Nothing starts while the last slice has a failing check.&lt;/p&gt;

&lt;p&gt;Two rules make long, unattended runs safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No invented facts.&lt;/strong&gt; When a fact is missing, Claude writes a placeholder, marks it with a searchable tag in the code and logs it in one register. The launch build fails while any marker is left. That's what let me ask, one evening, for every remaining section in one go without questions, and review real pages the next morning instead of made-up numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambitious first, then practical.&lt;/strong&gt; My first look for the site was safe and I called it ugly. Since then, a design question gets three or four rendered directions, desktop and phone, and I pick. Claude is fast at making options; I'm the one who chooses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does "done" mean when an agent says it?
&lt;/h2&gt;

&lt;p&gt;On this project, "done" is a list of commands with exit codes, not a sentence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Fails on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;typecheck, lint, unit tests&lt;/td&gt;
&lt;td&gt;types, style, content rules (like the test above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e2e in Playwright&lt;/td&gt;
&lt;td&gt;axe WCAG 2.2 AA on every route, sideways scroll from 320 to 2560 px, text spilling out of its box in any language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse, mobile&lt;/td&gt;
&lt;td&gt;performance under 95, any other category under 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launch gate in the build&lt;/td&gt;
&lt;td&gt;a placeholder left in a launch build, a missing share image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two mistakes taught me to read the output, not the summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first red task hides the rest.&lt;/strong&gt; &lt;code&gt;bun run check&lt;/code&gt; stops at the first failing task, so a failing test once hid a typecheck error that got committed. Now every task's result gets read, and the typecheck runs again after a test fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pipe hides the exit code.&lt;/strong&gt; While writing this post, a background build ended with exit code 0 while its log said &lt;code&gt;Build failed&lt;/code&gt;: the command piped the output through &lt;code&gt;tail&lt;/code&gt;, so the shell reported &lt;code&gt;tail&lt;/code&gt;'s code, and the preview served 404s. Now it's a lesson: grep the log for the failure, or use &lt;code&gt;set -o pipefail&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you make it look at its own work?
&lt;/h2&gt;

&lt;p&gt;Tests don't see a ragged line break or a label spilling out of a button. So Claude takes screenshots with Playwright from phone to desktop width (390 to 1920 px), in English, Polish and Spanish, and then opens every file. Claude Code reads images, so "I checked the page" can mean it actually looked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcs12q8j9mx30i8h1k6ih.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcs12q8j9mx30i8h1k6ih.webp" alt="The Brief step of the AI story on the home page: a card with the goal, the limits and what done looks like, next to the agent's reply " width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first try at this shot landed on step 3: the story plays by itself. Shot again with reduced motion, it holds still on the brief.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rest is knowing how the camera lies. Headless Chromium draws WebGL in software, so shaders look broken until it runs with &lt;code&gt;--use-angle=d3d11 --enable-gpu&lt;/code&gt;. Phones are emulated with touch on, or hover-only parts show their mouse version. And a page that animates needs reduced motion or a fixed clock, or the shot shows a random frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  When do subagents pay off?
&lt;/h2&gt;

&lt;p&gt;When the work is reading, not writing. A subagent runs in its own context and hands back only its answer, so a hundred pages of docs never touch the main session.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research in parallel.&lt;/strong&gt; While planning this site I sent out 8 research agents at once, one topic each. One lesson came out of it: parallel agents share one web search budget, so prefer fetching URLs you already know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact-checks.&lt;/strong&gt; For this post, a subagent checked 12 claims against the docs in under a minute. All held, and it found one detail worth adding: auto mode is the starting mode only from version 2.1.283.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh eyes.&lt;/strong&gt; A new agent that never saw the draft reviews the rendered post: what the headings alone say, which facts lack a source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where do skills fit?
&lt;/h2&gt;

&lt;p&gt;A skill turns a process I'd otherwise explain every time into a command. This post came out of &lt;code&gt;/new-blog&lt;/code&gt;: an 11-step pipeline from research to cross-posting, with its own scripts. &lt;code&gt;post-check.mjs&lt;/code&gt; lints the reading load and the SEO, &lt;code&gt;capture.mjs&lt;/code&gt; takes the screenshots, &lt;code&gt;crosspost.mjs&lt;/code&gt; publishes on dev.to and checks the published copy. The skill holds the judgement ("headings are questions a reader asks"), the scripts hold everything a script can prove.&lt;/p&gt;

&lt;p&gt;Skills grow like the lessons file. When I correct a skill's output, the correction goes into the skill, with the date and my words. The &lt;code&gt;/new-blog&lt;/code&gt; rule on numbers came from a round where I said a post "talks too many times about lighthouse and numbers". Another came from Medium: its URL import dropped every picture, because it refuses WebP. Now the skill builds a paste page with JPEG copies and checks that each picture loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you run several sessions on one repo?
&lt;/h2&gt;

&lt;p&gt;I often have two or three chats open on this repo, on two computers. Every trap here happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One session committed another's half-done work.&lt;/strong&gt; Now each session stages only its own paths, never &lt;code&gt;git add -A&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An audit ran on another session's build&lt;/strong&gt;, because a running preview server served the other one's output folder. Now each build gets its own folder and port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;taskkill /IM python.exe&lt;/code&gt; ended every Python on the machine&lt;/strong&gt;, other sessions' servers too. Stop a server by its port's PID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A push got rejected today&lt;/strong&gt;, because another session had pushed in the meantime. &lt;code&gt;git pull --rebase&lt;/code&gt;, then push.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Long builds and servers run in the background, and in my sessions Claude gets a note when one finishes, so it keeps writing meanwhile. For truly separate work, the docs suggest one git worktree per session (&lt;code&gt;claude --worktree &amp;lt;name&amp;gt;&lt;/code&gt;). On Windows, keep it at a short path: under the temp folder, the build hit "Filename too long" and produced pages without CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is it never allowed to do?
&lt;/h2&gt;

&lt;p&gt;Auto mode keeps the prompts away: a separate classifier checks each action in the background. Hooks are the other side of that trust: they run as shell commands with your full user permissions, so read any hook before you add it. On top of it sit the hard rules: never read real &lt;code&gt;.env&lt;/code&gt; files, never commit the private &lt;code&gt;context/&lt;/code&gt; folder, commit only to &lt;code&gt;main&lt;/code&gt;, push only when I say so.&lt;/p&gt;

&lt;p&gt;Be honest about where those rules live, though. Mine are in CLAUDE.md, and the docs are clear that CLAUDE.md guides what Claude tries but blocks nothing. A &lt;code&gt;deny&lt;/code&gt; rule in settings is what actually stops a read. That's the next thing I'm adding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One person, one project.&lt;/strong&gt; This is a portfolio built by me and Claude Code in about two weeks, not a team of twenty. Hooks and gates pay off sooner the more people touch the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows.&lt;/strong&gt; Several traps above (Git Bash paths, &lt;code&gt;taskkill&lt;/code&gt;, long paths) are Windows-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The card is over its budget.&lt;/strong&gt; It prints the lessons up to 8,000 characters, and the file has grown past that, so the rules at the end of the file get cut until I merge and prune them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cost numbers.&lt;/strong&gt; I'm on a subscription and didn't track tokens per feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It takes setup.&lt;/strong&gt; The wiki and hooks came from a planning phase before any product code. On a two-day script I'd write a short CLAUDE.md and stop there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where would I start?
&lt;/h2&gt;

&lt;p&gt;Not with the hooks. Start a &lt;code&gt;lessons.md&lt;/code&gt; today and write one line every time you correct Claude. Add one gate that Claude has to run before it says done. When a rule slips back twice, turn it into a test. The hooks come when you notice you're repeating yourself after every compaction.&lt;/p&gt;

&lt;p&gt;Which piece to reach for when is in &lt;a href="https://muszarski.com/en/blog/claude-code-skills-hooks-mcp-subagents/" rel="noopener noreferrer"&gt;Claude Code skills, hooks, MCP servers and subagents&lt;/a&gt;. The whole build is in the &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;case study&lt;/a&gt;, and the same process plays step by step in &lt;a href="https://muszarski.com/en/#ai" rel="noopener noreferrer"&gt;the AI section on my home page&lt;/a&gt;. If your team works like this, or wants to, here's my &lt;a href="https://muszarski.com/en/cv/" rel="noopener noreferrer"&gt;CV&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;How Claude remembers your project (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Best practices for Claude Code (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/sub-agents" rel="noopener noreferrer"&gt;Create custom subagents (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/common-workflows" rel="noopener noreferrer"&gt;Common workflows (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/permissions" rel="noopener noreferrer"&gt;Configure permissions (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/permission-modes" rel="noopener noreferrer"&gt;Choose a permission mode (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/quickstart" rel="noopener noreferrer"&gt;Quickstart (Claude Code docs)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How this post was made: I wrote it with Claude Code. It drafted the text from my project's wiki, code and notes. Just a quick disclaimer for y'all out there :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Cookieless analytics in the EU: Umami, GDPR and Poland's art. 399</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Sat, 17 Oct 2026 09:33:51 +0000</pubDate>
      <link>https://dev.to/muchassak/cookieless-analytics-in-the-eu-umami-gdpr-and-polands-art-399-45d1</link>
      <guid>https://dev.to/muchassak/cookieless-analytics-in-the-eu-umami-gdpr-and-polands-art-399-45d1</guid>
      <description>&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://muszarski.com/en/blog/cookieless-analytics-in-the-eu/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cookieless analytics such as Umami avoid cookies, but not the EU rule on reading information from a visitor's device. In Poland that rule is art. 399 of the Prawo komunikacji elektronicznej (the electronic communications law), and it has no exemption for analytics. Three positions are defensible: ask for consent, skip the banner but disclose and honour opt-outs, or run no analytics in the browser. This site takes the second.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two rules:&lt;/strong&gt; GDPR covers personal data; art. 399, Poland's version of the ePrivacy device rule, covers storing or reading anything on the device, cookie or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still access:&lt;/strong&gt; the EDPB treats a script that makes the browser send data as access, and art. 399 has no exemption for audience measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This site:&lt;/strong&gt; no banner, Umami without cookies, a privacy policy that says what the script reads, an opt-out, and no script at all under Do Not Track or Global Privacy Control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not legal advice: I'm a developer who read the primary sources for my own site. If the decision matters to your business, ask a lawyer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Which two rules apply?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GDPR&lt;/strong&gt; covers personal data. Analytics needs a legal basis. For a small site that is usually art. 6(1)(f), legitimate interests: processing "necessary for the purposes of the legitimate interests pursued by the controller", unless the visitor's rights override them. Art. 21 lets people object to that processing at any time, on grounds relating to their particular situation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ePrivacy Directive&lt;/strong&gt; (2002/58/EC), art. 5(3), covers storing information on the user's device or reading it from there, personal data or not. Poland implements it in the Prawo komunikacji elektronicznej (Dz.U. 2024 poz. 1221), in force since 10 November 2024. The device rule is its art. 399.&lt;/p&gt;

&lt;p&gt;"Cookieless" answers a question about cookies. The device rule asks a broader one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does art. 399 actually say?
&lt;/h2&gt;

&lt;p&gt;In my own abridged translation of ust. 1, storing information in a user's terminal equipment, or reading what is already stored there, is allowed only if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the user is first told, clearly and simply, the purpose and how to set conditions in their software or service settings;&lt;/li&gt;
&lt;li&gt;the user then consents;&lt;/li&gt;
&lt;li&gt;the storage or access doesn't change the configuration of the device or its software.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ust. 3 lifts these conditions when the storage or access is necessary to transmit a message over a public network. It also lifts them for a service "żądanej przez abonenta lub użytkownika końcowego", one the user asked for. Art. 400 adds that consent follows the data protection rules.&lt;/p&gt;

&lt;p&gt;Three things follow from the text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It binds whoever stores or reads. There is no "business" condition.&lt;/li&gt;
&lt;li&gt;It covers access, not only storage.&lt;/li&gt;
&lt;li&gt;The exemptions are transmission and a service the user asked for. Audience measurement isn't one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does a cookieless script "access" the device?
&lt;/h2&gt;

&lt;p&gt;The European Data Protection Board says yes, in the common case. In its Guidelines 2/2023 (§ 33), JavaScript code "where the accessing entity instructs the browser of the user to send asynchronous requests with the targeted information" clearly falls within art. 5(3).&lt;/p&gt;

&lt;p&gt;Umami's tracker is open source, so you can check what it does. In the current code it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads the screen's width and height and the browser language, and sends them with each event;&lt;/li&gt;
&lt;li&gt;reads one &lt;code&gt;localStorage&lt;/code&gt; key, &lt;code&gt;umami.disabled&lt;/code&gt;, to know if you opted out;&lt;/li&gt;
&lt;li&gt;checks the browser's Do Not Track setting when you turn that option on;&lt;/li&gt;
&lt;li&gt;sends requests without credentials by default and sets no cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Umami FAQ agrees: "Umami does not use any cookies in the tracking code". No cookies, then, but on the EDPB reading, still access. That is why the tracker's code matters more than its landing page: a vendor's promise is not a legal source.&lt;/p&gt;

&lt;p&gt;The same guidelines (§ 56) add that being in scope "does not systematically mean that consent needs to be collected". Whether an exemption applies is judged case by case, and in Poland the exemptions are art. 399 ust. 3. Analytics fits them badly.&lt;/p&gt;

&lt;p&gt;The Article 29 Working Party said so in 2012 (WP194): first-party analytics "are not strictly necessary to provide a functionality explicitly requested by the user". It also found them "not likely to create a privacy risk" when limited to first-party aggregate statistics, with clear information, "a user friendly mechanism to opt-out" and anonymisation. It even suggested lawmakers "might appropriately add a third exemption criterion".&lt;/p&gt;

&lt;p&gt;The Polish act has no such criterion. WP194 predates both GDPR and the act: useful as a risk profile, not binding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do Polish practice and the fines look like?
&lt;/h2&gt;

&lt;p&gt;UODO, Poland's data protection authority, uses Plausible Analytics on its own site. Its privacy policy says Plausible "nie wykorzystuje plików cookie i nie zbiera danych osobowych" (uses no cookies and collects no personal data) and describes no consent step. UODO doesn't enforce art. 399, though: fines for breaking it come from the President of UKE, the electronic communications regulator (art. 444 ust. 1 pkt 82 with art. 446).&lt;/p&gt;

&lt;p&gt;The ceilings in the statute: up to 3% of the previous year's revenue (art. 446 ust. 1). With no revenue in the three preceding calendar years, or no more than 500 000 zł in that period, the cap is 15 000 zł (art. 446 ust. 3). These are maximums, not a price list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which positions can you defend?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Position&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(a) Consent prompt&lt;/td&gt;
&lt;td&gt;Ask before loading analytics; load only after a yes&lt;/td&gt;
&lt;td&gt;A prompt on the first visit; statistics only from people who agree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(b) No banner, full disclosure&lt;/td&gt;
&lt;td&gt;Explain it in the privacy policy, offer an opt-out, and don't track when the browser sends Do Not Track or Global Privacy Control&lt;/td&gt;
&lt;td&gt;A residual risk under a literal reading of art. 399&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(c) No client-side analytics&lt;/td&gt;
&lt;td&gt;No tracker script; at most aggregate counts from the server&lt;/td&gt;
&lt;td&gt;You lose screen sizes, languages and click events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(a) is the safest on the letter of art. 399, and (b) matches the WP194 risk profile and the practice above. (c) narrows the question without removing it. Your server still receives IP addresses, and the EDPB guidelines (§ 55) treat an IP address sent from the user's device as in scope too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this site do?
&lt;/h2&gt;

&lt;p&gt;Position (b), decided on every visit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Page loads.&lt;/strong&gt; Only a small inline loader runs; the Umami script isn't requested yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signals.&lt;/strong&gt; Do Not Track or Global Privacy Control on? Stop: no script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opt-out.&lt;/strong&gt; umami.disabled stored in this browser? Stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Umami.&lt;/strong&gt; Added after the load event, counting this site's own domain only. No cookies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events.&lt;/strong&gt; Values from fixed lists, such as which button or which language. Nothing typed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Section 7 of the privacy policy, in all three of the site's languages, says what the script reads and how to switch it off. It names itself as the information art. 399 ust. 1 asks for. The switch sits on the &lt;a href="https://muszarski.com/en/privacy/" rel="noopener noreferrer"&gt;privacy page&lt;/a&gt;. Storing its flag is something the visitor asked for, which is what ust. 3 pkt 2 describes. The pause button follows the same rule: it remembers your choice only after you press it (&lt;a href="https://muszarski.com/en/blog/reduced-motion-and-a-pause-button/" rel="noopener noreferrer"&gt;motion that respects people&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you wire it with Umami?
&lt;/h2&gt;

&lt;p&gt;Do Not Track is one attribute: Umami's &lt;code&gt;data-do-not-track&lt;/code&gt; will "Respect user's Do Not Track browser setting". Global Privacy Control has no Umami setting; neither the docs nor the tracker code mention it. You could cancel each send in Umami's &lt;code&gt;data-before-send&lt;/code&gt; hook (return a falsy value), but it's cleaner not to load the script at all. A simplified version of this site's loader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optedOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;umami.disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// storage blocked&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalPrivacyControl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doNotTrack&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;optedOut&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cloud.umami.is/script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;websiteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR-WEBSITE-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-site.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doNotTrack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The opt-out is the key from Umami's "Exclude my own visits" page, behind a switch that sets or clears it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;optOutButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;umami.disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;umami.disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;umami.disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// storage blocked: nothing is saved, and the loader can't read it either&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx06edp1501joyluw96jj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx06edp1501joyluw96jj.webp" alt="A box titled Visit statistics in this browser, one line on what the statistics are for, and a button: Statistics are on. Turn them off" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3z43ffos6vk1lq0z0ksw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3z43ffos6vk1lq0z0ksw.webp" alt="The same box when the browser sends a privacy signal: one sentence saying statistics are already off, and no button" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The switch on this site's privacy page. With Global Privacy Control on (emulated here in a test browser), there is nothing to switch: statistics are already off.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Umami's server matters too. In its open-source code, a session ID is a hash of the website ID, the IP address, the user agent and a salt that rotates monthly by default. No cookie is involved, but IP addresses are, so treat it as personal data under GDPR: name the basis, explain it in the policy and honour objections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is a developer's reading of primary sources, not a lawyer's opinion. Position (b) keeps a residual risk under a literal reading of art. 399; if that isn't acceptable to you, (a) is a legitimate answer.&lt;/li&gt;
&lt;li&gt;Do Not Track and Global Privacy Control reach only some visitors. MDN marks &lt;code&gt;navigator.doNotTrack&lt;/code&gt; as deprecated, with a discontinued specification, and &lt;code&gt;navigator.globalPrivacyControl&lt;/code&gt; as experimental and missing from some widely used browsers.&lt;/li&gt;
&lt;li&gt;The opt-out lives in one browser. Clearing site data or switching devices forgets it.&lt;/li&gt;
&lt;li&gt;Your context changes the answer: a shop, an app with accounts or a company site is not a personal portfolio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other place where visitor data meets this site is &lt;a href="https://muszarski.com/en/blog/contact-form-on-a-static-site/" rel="noopener noreferrer"&gt;the contact form, which sends mail without a key in the browser&lt;/a&gt;. The whole build is in &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;the case study&lt;/a&gt;, and if you want to talk about any of this, &lt;a href="https://muszarski.com/en/#contact" rel="noopener noreferrer"&gt;here's how to reach me&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.sejm.gov.pl/eli/acts/DU/2024/1221/text.html" rel="noopener noreferrer"&gt;Ustawa z dnia 12 lipca 2024 r. Prawo komunikacji elektronicznej, Dz.U. 2024 poz. 1221 (Sejm ELI API)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.edpb.europa.eu/system/files/documents/2024-10/edpb_guidelines_202302_technical_scope_art_53_eprivacydirective_v2_en_0.pdf" rel="noopener noreferrer"&gt;EDPB Guidelines 2/2023 on Technical Scope of Art. 5(3) of ePrivacy Directive, version 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679" rel="noopener noreferrer"&gt;Regulation (EU) 2016/679 (GDPR), EUR-Lex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ec.europa.eu/justice/article-29/documentation/opinion-recommendation/files/2012/wp194_en.pdf" rel="noopener noreferrer"&gt;Article 29 Working Party, Opinion 04/2012 on Cookie Consent Exemption (WP194)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.umami.is/docs/tracker-configuration" rel="noopener noreferrer"&gt;Umami docs: Tracker configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.umami.is/docs/exclude-my-own-visits" rel="noopener noreferrer"&gt;Umami docs: Exclude my own visits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.umami.is/docs/faq" rel="noopener noreferrer"&gt;Umami docs: FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/umami-software/umami/master/src/tracker/index.ts" rel="noopener noreferrer"&gt;Umami tracker source (src/tracker/index.ts)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/umami-software/umami/master/src/app/api/send/route.ts" rel="noopener noreferrer"&gt;Umami collect endpoint source (src/app/api/send/route.ts)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/globalPrivacyControl" rel="noopener noreferrer"&gt;Navigator: globalPrivacyControl property (MDN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack" rel="noopener noreferrer"&gt;Navigator: doNotTrack property (MDN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://uodo.gov.pl/pl/1/255" rel="noopener noreferrer"&gt;UODO, Polityka prywatności&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How this post was made: I wrote it with Claude Code. It drafted the text from my project's wiki, code and notes. Just a quick disclaimer for y'all out there :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Expo in 2026: what's new, and how it compares to Xcode and Android Studio</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:23:13 +0000</pubDate>
      <link>https://dev.to/muchassak/expo-in-2026-whats-new-and-how-it-compares-to-xcode-and-android-studio-8be</link>
      <guid>https://dev.to/muchassak/expo-in-2026-whats-new-and-how-it-compares-to-xcode-and-android-studio-8be</guid>
      <description>&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://muszarski.com/en/blog/expo-react-native-vs-native/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In 2026, Expo is the usual way to start a React Native app, and React Native's own docs recommend it. In the last year it dropped the old architecture, made Hermes V1 its default engine and made Expo UI stable: real SwiftUI and Jetpack Compose controls, written in React. I shipped my app Linkoglot to both app stores with it from a Windows PC, and never opened Xcode or Android Studio.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo is the default way to build with React Native:&lt;/strong&gt; the React Native docs recommend a framework and name Expo, and 86% of State of React Native 2025 respondents used Expo CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed since SDK 54:&lt;/strong&gt; precompiled iOS builds, React Compiler in the template, the old architecture removed, Hermes V1 by default, smaller over-the-air updates and a stable Expo UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native still wins&lt;/strong&gt; when one platform is the product, when the app lives in OS features like widgets, or when your team already writes Swift or Kotlin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My pick:&lt;/strong&gt; an app for both stores, built by someone who knows React and TypeScript, starts in Expo.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Expo, and why does React Native recommend it?
&lt;/h2&gt;

&lt;p&gt;React Native gives you native iOS and Android apps written in React. Expo is a framework on top of it. It brings a CLI, an SDK of native modules (camera, notifications, audio and more) and file-based routing with Expo Router. EAS, its set of cloud services, has a free tier. The framework itself costs nothing: "Expo and the Expo SDK are free and open source (MIT license)."&lt;/p&gt;

&lt;p&gt;React Native's setup page is direct: "if you're building a new app with React Native, we recommend using a Framework". It names one: "Expo is a production-grade React Native Framework." You can still go without one, and the same page says so.&lt;/p&gt;

&lt;p&gt;In the State of React Native 2025 survey, 86.18% of the 926 people who answered had used Expo CLI in the last year, against 47.73% for the React Native Community CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did I ship an iPhone app without opening Xcode?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/shipping-a-mobile-app-alone/" rel="noopener noreferrer"&gt;Linkoglot&lt;/a&gt; is my AI language-learning app, built alone and live on Google Play and the App Store since July 2026. I built it on a Windows PC. Xcode needs a Mac (Xcode 27 "will only install and run on Apple silicon Macs"), and I never opened it, or Android Studio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fck6tpbuujnuivt0fqdr2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fck6tpbuujnuivt0fqdr2.webp" alt="Linkoglot's camera pointed at a living room, with word labels in three languages on a chair, a plant pot and a book" width="480" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg9uurc3nbu9ad9jk6jd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg9uurc3nbu9ad9jk6jd.webp" alt="A French flashcard reading la plaquette, with an example sentence and score counters around it" width="480" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoe1d8rs2vzbxrc3bj70.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoe1d8rs2vzbxrc3bj70.webp" alt="A voice conversation about Japanese literary terms, the tutor's reply in Japanese under three glowing lights" width="480" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three of Linkoglot's App Store screenshots: the camera, a flashcard and a voice conversation. The Android app runs the same TypeScript screens.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The whole path ran through Expo's tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write.&lt;/strong&gt; TypeScript and React on Windows, with Fast Refresh on both phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate.&lt;/strong&gt; Prebuild writes the ios and android folders from the app config and plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; EAS Build compiles in the cloud; iOS builds run on Expo's Macs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Development builds on real phones, not Expo Go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship.&lt;/strong&gt; Google Play and the App Store, each through its own review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On EAS Build, "iOS builds run on macOS runners hosted in Expo's macOS cloud". In September 2026 Expo wrote a whole post about this path, "You don't need a Mac to develop iOS apps", and added a cloud iOS Simulator, still a "limited-access preview".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing was the part a PC couldn't do alone, and Expo Go wasn't enough&lt;/strong&gt;. Expo Go is the store app that runs your project with only the native code it already ships: "There is no way to get the native code into the Expo Go app". Payments, push notifications on Android and Linkoglot's iOS notification extension all need a development build, your own app with Expo's dev tools inside.&lt;/p&gt;

&lt;p&gt;So I bought an iPhone to test on, next to my Android phone. Expo Go isn't even in the stores for every SDK now: "Expo Go for SDK 56 is not available on the Apple App Store or Google Play Store."&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Expo since SDK 54?
&lt;/h2&gt;

&lt;p&gt;Expo ships a new SDK every few months, each one tied to a React Native version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SDK 54&lt;/strong&gt;, 10 Sep 2025, React Native 0.81&lt;/td&gt;
&lt;td&gt;Precompiled React Native for iOS, React Compiler on in the template, native tabs with Liquid Glass (beta), the last SDK with the old architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SDK 55&lt;/strong&gt;, 25 Feb 2026, React Native 0.83&lt;/td&gt;
&lt;td&gt;Old architecture removed, native tabs in the default template, Expo UI for Jetpack Compose in beta, diffed updates as an option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SDK 56&lt;/strong&gt;, 21 May 2026, React Native 0.85&lt;/td&gt;
&lt;td&gt;Hermes V1 by default, Expo UI stable, diffed updates on by default, Expo Router without React Navigation, agent files in new projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SDK 57&lt;/strong&gt;, 30 Jun 2026, React Native 0.86&lt;/td&gt;
&lt;td&gt;A small release on React Native 0.86, which "is intended to have no breaking changes"; a later patch fixes a Hermes V1 memory regression from SDK 56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SDK 58 beta&lt;/strong&gt;, 15 Sep 2026, React Native 0.88 RC&lt;/td&gt;
&lt;td&gt;Built for iOS 27, Expo Modules 2.0 in beta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few terms. The "old architecture" is React Native's original bridge between JavaScript and native code. Native tabs use each platform's own tab bar, and diffed updates send only what changed in the JavaScript. React Native 0.82 was "the first React Native that runs entirely on the New Architecture", and in the 2025 survey 733 of 932 respondents had already migrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What got faster?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS builds.&lt;/strong&gt; Since SDK 54, React Native for iOS comes precompiled. Expo saw clean builds of React Native's test app drop "from about 120 seconds to 10 seconds" on an M4 Max. It also warned: "you're unlikely to see a ~10x build time improvement in your app". SDK 56 did the same for Expo's own modules: "around 1 minute (~16%)" off the median clean iOS build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup.&lt;/strong&gt; Hermes V1 became React Native's default engine in 0.84 and Expo's in SDK 56, "bringing faster startup times". The public numbers come from one real app, Expensify, in the 0.82 post: time to interactive 2.5% faster on iOS and 7.6% faster on a low-end Android phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates.&lt;/strong&gt; EAS Update now sends only what changed between two JavaScript bundles. In the 24 hours before the SDK 56 post, diffed bundles were "on average 58% smaller than the full bundle they replaced".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-renders.&lt;/strong&gt; React Compiler memoizes components and values at build time, so you write fewer &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; calls by hand. Every new project has it on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Expo UI?
&lt;/h2&gt;

&lt;p&gt;Expo UI is "a set of components that allow you to build UIs directly with Jetpack Compose and SwiftUI from React". Since SDK 56 the SwiftUI and Compose parts are stable; the web part is still experimental. Expo's own summary: "It just exposes the primitives iOS and Android already ship, as React components."&lt;/p&gt;

&lt;p&gt;The official example, a SwiftUI button inside a React component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@expo/ui/swift-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SaveButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Host&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Save changes"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Host&lt;/code&gt; is the boundary: everything inside it is laid out by SwiftUI or Compose, not by React Native's flexbox. Modifiers come from &lt;code&gt;@expo/ui/swift-ui/modifiers&lt;/code&gt; as an array, like SwiftUI's own modifier chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters now is Liquid Glass.&lt;/strong&gt; Apple's docs say standard SwiftUI and UIKit components "pick up the appearance and behavior of this material automatically". Apps built with the iOS 27 SDK can no longer opt out of the new look. A control from Expo UI is the real SwiftUI control, so it changes with the OS.&lt;/p&gt;

&lt;p&gt;To add glass yourself, give an Expo UI view the &lt;code&gt;glassEffect({ glass: { variant: 'clear' } })&lt;/code&gt; modifier (Xcode 26 and iOS 26 or later). Your own React Native views can sit in a &lt;code&gt;GlassView&lt;/code&gt; from &lt;code&gt;expo-glass-effect&lt;/code&gt;, also on iOS 26 and up.&lt;/p&gt;

&lt;p&gt;Expo's guidance on when to use it: Expo UI where users expect the screen to look like the OS (settings, modals, pickers, sheets), and regular React Native views for your own design. Linkoglot is the second kind: its screens are NativeWind with React Native Reusables.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else comes with Expo now?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router:&lt;/strong&gt; file-based routes for the app, much like a web framework's. Since SDK 56 it no longer depends on React Navigation, and its native tabs get Liquid Glass on iOS 26.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native code when you need it:&lt;/strong&gt; the Expo Modules API "allows you to write Swift and Kotlin to add new capabilities to your app", and config plugins edit the generated native projects at prebuild. Linkoglot's iOS notification extension is generated that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Submit and EAS Update:&lt;/strong&gt; Submit uploads builds to both stores and "works from any OS (including Windows and Linux for iOS)". Update ships JavaScript, styles and images over the air; native changes still go through store review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Observe:&lt;/strong&gt; generally available since 20 August 2026. It measures launch times, time to first render and time to interactive in production, on real devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI tooling:&lt;/strong&gt; Expo runs an MCP server for coding agents, on the free plan (with a monthly usage cap) since May 2026, and new SDK 56 projects start with &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;. It's the same kind of setup I use &lt;a href="https://muszarski.com/en/blog/claude-code-skills-hooks-mcp-subagents/" rel="noopener noreferrer"&gt;with Claude Code&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free EAS plan includes 15 Android and 15 iOS builds and 1,000 monthly active users for updates; the Starter plan is $19 a month plus usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expo or Xcode and Android Studio: what do you trade?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Expo (React Native)&lt;/th&gt;
&lt;th&gt;Xcode (Swift, SwiftUI)&lt;/th&gt;
&lt;th&gt;Android Studio (Kotlin, Compose)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One codebase reaches&lt;/td&gt;
&lt;td&gt;iOS, Android and web&lt;/td&gt;
&lt;td&gt;Apple platforms&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your computer&lt;/td&gt;
&lt;td&gt;Any OS; iOS builds in EAS's cloud&lt;/td&gt;
&lt;td&gt;A Mac; Xcode 27 needs Apple silicon&lt;/td&gt;
&lt;td&gt;Windows, macOS, Linux or ChromeOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript and React&lt;/td&gt;
&lt;td&gt;Swift&lt;/td&gt;
&lt;td&gt;Kotlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React Native views, or SwiftUI and Compose through Expo UI&lt;/td&gt;
&lt;td&gt;SwiftUI or UIKit&lt;/td&gt;
&lt;td&gt;Jetpack Compose or Views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A new OS feature&lt;/td&gt;
&lt;td&gt;After an Expo SDK or library update, or in your own module&lt;/td&gt;
&lt;td&gt;On release day&lt;/td&gt;
&lt;td&gt;On release day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shipping a fix&lt;/td&gt;
&lt;td&gt;JavaScript over the air, within store rules; native code through review&lt;/td&gt;
&lt;td&gt;Through review&lt;/td&gt;
&lt;td&gt;Through review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI in the tools&lt;/td&gt;
&lt;td&gt;Expo's MCP server, agent files in new projects&lt;/td&gt;
&lt;td&gt;Claude, ChatGPT and Gemini in the coding assistant&lt;/td&gt;
&lt;td&gt;Gemini agent mode, other models through a remote endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing the table hides: React Native developers still open the native tools. In the State of React Native 2025 survey, 68.2% built their apps with EAS Build, and 60.3% built by hand in Xcode and 55.9% in Android Studio. Expo moves those IDEs from your daily tools to the tools you open when a native build breaks. For Linkoglot, that day never came.&lt;/p&gt;

&lt;h2&gt;
  
  
  When are Xcode or Android Studio the better call?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One platform is the product.&lt;/strong&gt; An iOS-only app gets every new API in Xcode on release day. Expo's SDK built for iOS 27 was still a beta on 14 October 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app lives in OS features.&lt;/strong&gt; Widgets, watch apps and extensions are native targets written in Swift or Kotlin, Expo or not. Expo can generate them from config, but if they are most of the app, React Native is overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The team already writes Swift and Kotlin.&lt;/strong&gt; A Kotlin team that needs both platforms has its own route: JetBrains declared "Compose Multiplatform for iOS is Stable" in May 2025.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a new app on both stores, built by someone who knows React and TypeScript, I'd pick Expo again. My reasons for Linkoglot still hold: one codebase for two stores, fast tooling, libraries ready for the hard parts, and a language I already knew. The web skills carry over too: Linkoglot uses TanStack Query, Supabase's JavaScript client and Lingui, the same libraries I use on websites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I've shipped one app with Expo and none in Swift or Kotlin, so the native side comes from Apple's and Google's docs, not from my own hands.&lt;/li&gt;
&lt;li&gt;The speed numbers are Expo's and the React Native team's benchmarks (the Hermes figures are Expensify's app). I didn't measure them on Linkoglot.&lt;/li&gt;
&lt;li&gt;What I write about Expo UI, EAS Observe and SDK 58 comes from Expo's docs and posts. SDK 58 is a beta, and prices are as read on 14 October 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How Linkoglot was built, from the database to both stores' rules, is in &lt;a href="https://muszarski.com/en/blog/shipping-a-mobile-app-alone/" rel="noopener noreferrer"&gt;shipping a mobile app alone&lt;/a&gt;, and the app has its own chapter &lt;a href="https://muszarski.com/en/#linkoglot" rel="noopener noreferrer"&gt;on my home page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://reactnative.dev/docs/environment-setup" rel="noopener noreferrer"&gt;Get Started with React Native (React Native docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://results.stateofreactnative.com/en-US/react-native-architecture/" rel="noopener noreferrer"&gt;State of React Native 2025: architecture and frameworks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://results.stateofreactnative.com/en-US/build-and-publish/" rel="noopener noreferrer"&gt;State of React Native 2025: build and publish&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/pricing" rel="noopener noreferrer"&gt;Expo pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS Build (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/blog/build-ios-apps-on-windows-with-cloud-simulators" rel="noopener noreferrer"&gt;You don't need a Mac to develop iOS apps (Expo blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/develop/development-builds/faq/" rel="noopener noreferrer"&gt;Development builds FAQ (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/notifications/" rel="noopener noreferrer"&gt;Notifications (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.revenuecat.com/docs/getting-started/installation/expo" rel="noopener noreferrer"&gt;Using RevenueCat with Expo (RevenueCat docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/sdk-54" rel="noopener noreferrer"&gt;Expo SDK 54 (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/sdk-55" rel="noopener noreferrer"&gt;Expo SDK 55 (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/sdk-56" rel="noopener noreferrer"&gt;Expo SDK 56 (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/sdk-57" rel="noopener noreferrer"&gt;Expo SDK 57 (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/sdk-58-beta" rel="noopener noreferrer"&gt;Expo SDK 58 beta (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://reactnative.dev/blog/2025/10/08/react-native-0.82" rel="noopener noreferrer"&gt;React Native 0.82: A New Era (React Native blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://reactnative.dev/blog/2026/02/11/react-native-0.84" rel="noopener noreferrer"&gt;React Native 0.84 (React Native blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/learn/react-compiler" rel="noopener noreferrer"&gt;React Compiler (React docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/ui/" rel="noopener noreferrer"&gt;Expo UI (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/" rel="noopener noreferrer"&gt;Expo UI: SwiftUI (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/glass-effect/" rel="noopener noreferrer"&gt;GlassEffect (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/blog/expo-ui-stable-sdk-56" rel="noopener noreferrer"&gt;Expo UI is stable (Expo blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/technologyoverviews/liquid-glass" rel="noopener noreferrer"&gt;Liquid Glass (Apple Developer Documentation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/bundleresources/information-property-list/uidesignrequirescompatibility" rel="noopener noreferrer"&gt;UIDesignRequiresCompatibility (Apple Developer Documentation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/modules/overview/" rel="noopener noreferrer"&gt;Expo Modules API: overview (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/config-plugins/introduction/" rel="noopener noreferrer"&gt;Config plugins: introduction (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/router/advanced/native-tabs/" rel="noopener noreferrer"&gt;Native tabs (Expo Router docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/submit/introduction/" rel="noopener noreferrer"&gt;EAS Submit (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;EAS Update (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/eas-observe-is-now-generally-available" rel="noopener noreferrer"&gt;EAS Observe is now generally available (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/eas/observe/introduction/" rel="noopener noreferrer"&gt;EAS Observe: introduction (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/eas/ai/mcp/" rel="noopener noreferrer"&gt;Expo MCP Server (Expo docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expo.dev/changelog/the-expo-mcp-server-is-now-available-on-the-free-plan" rel="noopener noreferrer"&gt;The Expo MCP server is now available on the free plan (Expo changelog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes" rel="noopener noreferrer"&gt;Xcode 26 Release Notes (Apple Developer)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-27-release-notes" rel="noopener noreferrer"&gt;Xcode 27 Release Notes (Apple Developer)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio" rel="noopener noreferrer"&gt;Download Android Studio (Android Developers)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/gemini/features" rel="noopener noreferrer"&gt;Gemini in Android Studio features (Android Developers)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/gemini/use-a-remote-model" rel="noopener noreferrer"&gt;Use a remote model in Android Studio (Android Developers)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jetbrains.com/kotlin/2025/05/compose-multiplatform-1-8-0-released-compose-multiplatform-for-ios-is-stable-and-production-ready/" rel="noopener noreferrer"&gt;Compose Multiplatform 1.8.0: iOS is stable (JetBrains blog)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How this post was made: I wrote it with Claude Code. It drafted the text from my project's wiki, code and notes. Just a quick disclaimer for y'all out there :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>react</category>
      <category>performance</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Astro vs Next.js for a portfolio or a landing page</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:47:58 +0000</pubDate>
      <link>https://dev.to/muchassak/astro-vs-nextjs-for-a-portfolio-or-a-landing-page-4jpf</link>
      <guid>https://dev.to/muchassak/astro-vs-nextjs-for-a-portfolio-or-a-landing-page-4jpf</guid>
      <description>&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://muszarski.com/en/blog/astro-vs-nextjs-for-a-portfolio/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;, where the clips play and the version tabs work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a portfolio or a landing page, Astro is the better default: it pre-renders every page to HTML and ships no JavaScript unless a component asks for it. Next.js is the better call when the site is really an application, with logins, per-user data and server-side writes. This site is Astro; Clockful, the time tracker I built during my internship at Neoteric, is Next.js.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Astro for content:&lt;/strong&gt; every page pre-rendered to HTML, zero JavaScript by default, interactivity opted into per island.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js for apps:&lt;/strong&gt; logins, roles and per-user data; its static export gives up cookies, redirects, Server Actions and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public data:&lt;/strong&gt; in HTTP Archive's August 2026 mobile data, 71.0% of Astro origins pass Core Web Vitals against 35.1% of Next.js origins, partly because people build different things with each.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What does each framework say it is for?
&lt;/h2&gt;

&lt;p&gt;Astro's docs are direct: "Astro is the web framework for building content-driven websites like blogs, marketing, and e-commerce." It calls itself server-first, with zero JavaScript by default. The same page says other frameworks "excel at building more complex, application-like experiences in the browser: logged-in admin dashboards, inboxes, social networks, todo lists".&lt;/p&gt;

&lt;p&gt;Next.js calls itself "a React framework for building full-stack web applications". In its App Router, layouts and pages are React Server Components by default, and you opt into client code with the &lt;code&gt;"use client"&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;Those two lines answer most of the question. A portfolio is a content site with a contact form. A dashboard is an application.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much JavaScript reaches the browser?
&lt;/h2&gt;

&lt;p&gt;In Astro, every UI component, React included, renders to HTML and CSS, and its client JavaScript is stripped out. Interactivity is opt-in per component, through islands: &lt;code&gt;client:load&lt;/code&gt; hydrates at once, &lt;code&gt;client:idle&lt;/code&gt; when the browser is idle, &lt;code&gt;client:visible&lt;/code&gt; when the component scrolls into view. A page without islands ships no framework runtime.&lt;/p&gt;

&lt;p&gt;Next.js renders on the server too, and Server Components keep their code out of the bundle. The client still runs React, though, to hydrate Client Components and handle client-side navigation, and everything a &lt;code&gt;"use client"&lt;/code&gt; file imports joins the bundle. That pays off in an app with many screens and shared state; on a five-page portfolio it is overhead to manage.&lt;/p&gt;

&lt;p&gt;Here is the Astro path on this site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Every page becomes HTML, with its CSS inlined in the head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN.&lt;/strong&gt; Vercel serves the files; the contact form is the one function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text first.&lt;/strong&gt; The H1 is the largest paint and waits for no script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small scripts.&lt;/strong&gt; Component scripts load as small modules, with no framework runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effects last.&lt;/strong&gt; Loops and clips wait for the first interaction; shaders draw only in view.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are no framework islands here at all. Every component is an &lt;code&gt;.astro&lt;/code&gt; file, and the interactive parts (the pause button, the credentials tour, the writing rail) are plain scripts that Astro bundles into small modules. On the home page they come to under 10 KB, gzipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can both produce a static site?
&lt;/h2&gt;

&lt;p&gt;Yes, at a different cost. Astro's default is static: "By default, your entire Astro site will be prerendered". If one route needs a server, such as a contact endpoint, you add an adapter and mark that page with &lt;code&gt;export const prerender = false&lt;/code&gt;. Everything else stays static.&lt;/p&gt;

&lt;p&gt;This site skipped even that: no adapter, and the contact form posts to a plain Vercel Function next to the static files (&lt;a href="https://muszarski.com/en/blog/contact-form-on-a-static-site/" rel="noopener noreferrer"&gt;how the contact form works&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Next.js can produce a static site too: with &lt;code&gt;output: 'export'&lt;/code&gt;, &lt;code&gt;next build&lt;/code&gt; writes one HTML file per route into an &lt;code&gt;out&lt;/code&gt; folder. The docs list what you give up, including cookies, redirects, Proxy, Incremental Static Regeneration, Server Actions and dynamic routes without &lt;code&gt;generateStaticParams()&lt;/code&gt;. Many of those are the reason to pick Next.js in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about content, translations and page transitions?
&lt;/h2&gt;

&lt;p&gt;A portfolio is mostly writing. Astro's content collections load Markdown and MDX from folders, validate frontmatter with a Zod schema and generate TypeScript types from it. This post is one of those files: its title, description and sources are checked before the page builds. The site's three languages run on Lingui, all of it static (&lt;a href="https://muszarski.com/en/blog/multilingual-astro-site-with-lingui/" rel="noopener noreferrer"&gt;how the multilingual setup works&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For page transitions, Astro supports the browser's native cross-document view transitions (the CSS &lt;code&gt;@view-transition&lt;/code&gt; rule), and its &lt;code&gt;&amp;lt;ClientRouter /&amp;gt;&lt;/code&gt; adds SPA-style ones when you want more control. This site uses only the CSS rule: pages crossfade with no JavaScript, and readers who ask for reduced motion navigate instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does public data say?
&lt;/h2&gt;

&lt;p&gt;The HTTP Archive Tech Report takes real-user Core Web Vitals from the Chrome UX Report per origin, and detects technologies on each site's home page and one inner page. Astro and Next.js in the August 2026 dataset, mobile, all origins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mobile, August 2026&lt;/th&gt;
&lt;th&gt;Astro&lt;/th&gt;
&lt;th&gt;Next.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Origins assessed&lt;/td&gt;
&lt;td&gt;43,850&lt;/td&gt;
&lt;td&gt;374,612&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good Core Web Vitals (LCP, INP, CLS)&lt;/td&gt;
&lt;td&gt;71.0%&lt;/td&gt;
&lt;td&gt;35.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good LCP&lt;/td&gt;
&lt;td&gt;80.6%&lt;/td&gt;
&lt;td&gt;53.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median JavaScript per page&lt;/td&gt;
&lt;td&gt;405 KB&lt;/td&gt;
&lt;td&gt;853 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read it as a picture of two populations, not a verdict on two frameworks. By their own docs, Next.js targets applications and Astro targets content sites, so part of the gap is what people build with each. The report can't separate a framework from the site built on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did this site measure?
&lt;/h2&gt;

&lt;p&gt;This site is Astro 7, Tailwind 4 and one variable font, served as static files from Vercel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokdfev9ts7fcrh63n8oz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokdfev9ts7fcrh63n8oz.webp" alt="A Lighthouse report for the home page on mobile: four green gauges for Performance, Accessibility, Best Practices and SEO, next to a phone preview of the hero"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The home page in Lighthouse on mobile, from a local production build on 7 October 2026. The CV, blog and work pages score 96 to 100 on the same setup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two honest notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I did not build the same site in Next.js,&lt;/strong&gt; so I can't tell you what it would have scored. That is why the comparison above uses public data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The framework didn't earn the score alone.&lt;/strong&gt; On the afternoon of 7 October the home page fell to 67-74 with no framework code involved: animation loops running during load, a class that restyled every element in the offscreen sections, and scripts forcing layouts. Fixing those brought it back. What Astro gave me is the starting point: zero JavaScript, so there was nothing to remove.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same pattern shows up in &lt;a href="https://muszarski.com/en/blog/lighthouse-scores-of-16-developer-portfolios/" rel="noopener noreferrer"&gt;Lighthouse runs on 16 developer portfolios&lt;/a&gt;: the immersive 3D sites scored far below the minimal ones. The whole build story is in &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;how this site was built&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since then I have measured one real move: yws.lu, a client site I built in Next.js in 2025. It scored 23-45 on Lighthouse performance, and 94-100 after the rebuild in Astro (Lighthouse 13.5, local, the median of 3 runs). The details are in &lt;a href="https://muszarski.com/en/blog/yws-lu-case-study/" rel="noopener noreferrer"&gt;the yws.lu case study&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Next.js the right call?
&lt;/h2&gt;

&lt;p&gt;Pick Next.js when most screens depend on who is looking at them. Clockful, the role-based time tracker from my internship at Neoteric, is that kind of project: logins, roles, per-user data, and Postgres row-level security on every table.&lt;/p&gt;

&lt;p&gt;The Next.js docs cover that ground in one guide: authentication, sessions in cookies, authorization in a data access layer, and Server Actions for writes. Astro can render on demand too, but its own docs point to other frameworks for logged-in dashboards. Next.js also wins when the landing page belongs to an existing Next.js product: one codebase beats two stacks for one marketing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Astro the right call?
&lt;/h2&gt;

&lt;p&gt;When the site is content first: a portfolio, a landing page, docs, a blog, a small business site. You can still write React, Preact, Svelte, Vue or SolidJS where you need interactivity, and only those islands ship code. Hosting is a CDN plus a build, with a server function only where you need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one fits your kind of site?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your site&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio, CV, blog&lt;/td&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;Static HTML, zero JS by default, content collections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing landing page&lt;/td&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;Same, plus an island for the one interactive widget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Landing page inside a Next.js product&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;One codebase, one deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard with logins and roles&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;Server Components, auth guide, Server Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App with per-user data on every screen&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;Built for on-demand rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content site with one or two dynamic routes&lt;/td&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;Static by default, &lt;code&gt;prerender = false&lt;/code&gt; per route&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How do you decide for your project?
&lt;/h2&gt;

&lt;p&gt;Write down what the site must do in its first year. If the list says "log in", "dashboard" or "per-user data", start with Next.js. If it says "pages, posts, a contact form", start with Astro, then measure on a mid-range phone.&lt;/p&gt;

&lt;p&gt;The rest of what I've built is on my &lt;a href="https://muszarski.com/en/work/" rel="noopener noreferrer"&gt;work page&lt;/a&gt;; Clockful, the role-based app, is on my &lt;a href="https://muszarski.com/en/cv/" rel="noopener noreferrer"&gt;CV&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/concepts/why-astro/" rel="noopener noreferrer"&gt;Astro docs: Why Astro?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/concepts/islands/" rel="noopener noreferrer"&gt;Astro docs: Islands architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/on-demand-rendering/" rel="noopener noreferrer"&gt;Astro docs: On-demand rendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/content-collections/" rel="noopener noreferrer"&gt;Astro docs: Content collections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/view-transitions/" rel="noopener noreferrer"&gt;Astro docs: View transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs" rel="noopener noreferrer"&gt;Next.js docs: Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/getting-started/server-and-client-components" rel="noopener noreferrer"&gt;Next.js docs: Server and Client Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/static-exports" rel="noopener noreferrer"&gt;Next.js docs: Static exports&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/authentication" rel="noopener noreferrer"&gt;Next.js docs: Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://httparchive.org/reports/techreport/landing" rel="noopener noreferrer"&gt;HTTP Archive Tech Report: about the data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://httparchive.org/reports/techreport/tech?tech=Astro,Next.js" rel="noopener noreferrer"&gt;HTTP Archive Tech Report: Astro vs Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How this post was made: I wrote it with Claude Code. It drafted the text from my project's wiki, code and notes. Just a quick disclaimer for y'all out there :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>performance</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hello Wor-- I mean, Hello dev.to :)</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:38:00 +0000</pubDate>
      <link>https://dev.to/muchassak/hello-wor-i-mean-hello-devto--fdn</link>
      <guid>https://dev.to/muchassak/hello-wor-i-mean-hello-devto--fdn</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg" class="crayons-story__hidden-navigation-link"&gt;Trying to build an Awwwards-winning brutalist portfolio: the process, the mindset and the tools&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/muchassak" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177825%2F9516f0c3-66c5-4ba9-9fd0-d47ef79cee4b.jpg" alt="muchassak profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/muchassak" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mateusz Muszarski
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mateusz Muszarski
                
                
              
              &lt;div id="story-author-preview-content-4834606" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/muchassak" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177825%2F9516f0c3-66c5-4ba9-9fd0-d47ef79cee4b.jpg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mateusz Muszarski&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg" id="article-link-4834606"&gt;
          Trying to build an Awwwards-winning brutalist portfolio: the process, the mindset and the tools
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/astro"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;astro&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/performance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;performance&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            16 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>design</category>
      <category>frontend</category>
      <category>portfolio</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Trying to build an Awwwards-winning brutalist portfolio: the process, the mindset and the tools</title>
      <dc:creator>Mateusz Muszarski</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:08:59 +0000</pubDate>
      <link>https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg</link>
      <guid>https://dev.to/muchassak/trying-to-build-an-awwwards-winning-brutalist-portfolio-the-process-the-mindset-and-the-tools-4njg</guid>
      <description>&lt;p&gt;&lt;em&gt;This post first appeared on &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;, where the clips play and the version tabs work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My old portfolio was a WebGL flight from Earth to Neptune. It looked great on my desktop. On a phone it showed an “Oops!” screen. This is the story of swapping it for a poster: the versions, the research, the sites I learned from and the reasons behind each call, mostly in pictures.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/bUbHtUvuzpI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How this site got built, in 47 seconds. Made in code with Claude Code: the real site filmed frame by frame, and music written in code.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; a 3D site that scored 34 on mobile Lighthouse and didn't work on phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After:&lt;/strong&gt; Astro, three languages, one font, a few shaders that never get in the way, and 95 to 100 on mobile Lighthouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How:&lt;/strong&gt; a project wiki before any code, research on award winners and real products, four live designs to pick from, and checks on every change.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What rules did I work by?
&lt;/h2&gt;

&lt;p&gt;Six rules: most written down before the first line of code, the rest learned on the way. Everything below follows from them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan before code.&lt;/strong&gt; The project lives in a wiki that every AI session reads first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical first, then wow.&lt;/strong&gt; Impressive within 3 seconds, usable in one tap on a phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research, then pick.&lt;/strong&gt; Real sites and real screens first; I choose between live options, not sketches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show, don't tell.&lt;/strong&gt; A picture or a moving demo wherever a paragraph would go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep every version.&lt;/strong&gt; So I can show how it changed, and go back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing ships unchecked.&lt;/strong&gt; Tests, screenshots I look at myself, speed on a slow phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How did it go, version by version?
&lt;/h2&gt;

&lt;p&gt;About four days, and every version is kept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before: The 3D space portfolio&lt;/strong&gt; (Replaced)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2e65n58dbovd3mczavrr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2e65n58dbovd3mczavrr.webp" alt="The old site: a glowing planet and the word Projects in space" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A WebGL flight to Neptune. Beautiful on my desktop, an “Oops!” screen on phones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 Oct 2026: Phase 0: a wiki, no code&lt;/strong&gt; (Kept)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Plan first, code second.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Research, requirements, brand, legal and the rules of the project, before a single component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 Oct 2026: Drafting Table&lt;/strong&gt; (Rejected)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gek711p8jmoeo114yni.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gek711p8jmoeo114yni.webp" alt="The first design: a pale paper page with a blueprint grid and a small heading" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A clean system is not a good design.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Paper, cyanotype and a title-block header. It passed every check and looked forgettable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 Oct 2026: Four live directions&lt;/strong&gt; (Poster picked)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbsyykkjj6vqbe6eqlx3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbsyykkjj6vqbe6eqlx3.webp" alt="The Poster direction: the name in huge white capitals on cobalt with a pink round sticker" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The type is the picture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Orbit, Poster, Toybox and Exhibit as working pages. Claude recommended Orbit. I picked Poster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 Oct 2026: Round 2: Poster everywhere&lt;/strong&gt; (Notes)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbtfbttxd4eujadba1vna.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbtfbttxd4eujadba1vna.webp" alt="Round 2 of Poster: the contact section, Let's talk in huge capitals on pink" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole home page in Poster, three colour themes and two fonts tried side by side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 Oct 2026: Round 3: less text&lt;/strong&gt; (Approved)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx11fk0l4g1leeatuisht.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx11fk0l4g1leeatuisht.webp" alt="Round 3 of Poster: credentials as tall cards with sideways titles, one open" width="799" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Less text, more to see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tool logos with their names, a reel per project, a shorter everything. The comps stop here and the real code starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6-8 Oct 2026: The site today&lt;/strong&gt; (Live)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt1lxq2n16wpq1kskah5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt1lxq2n16wpq1kskah5.webp" alt="Today's hero: the name fitted edge to edge on cobalt, the role and two buttons" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built in Astro one section at a time, then reshaped by rounds of my notes on the live site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rebuild it?
&lt;/h2&gt;

&lt;p&gt;Here is the old portfolio in motion:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfguqvo09pohcgex7f40.webp" alt="The old space portfolio: the camera flies past Mars to Jupiter" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 3D space portfolio, recorded on a desktop. Planet textures: Solar System Scope, CC BY 4.0.&lt;/em&gt; The clips play on &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Beautiful on my desktop. Then I measured it, on 5 October 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13.8 MB before the start button.&lt;/strong&gt; 7.6 MB of it was an emoji font.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;34 on mobile Lighthouse.&lt;/strong&gt; On a throttled phone the main content took 46.9 seconds to appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phones got an “Oops!” screen.&lt;/strong&gt; The site didn't run there at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only a mouse wheel moved it.&lt;/strong&gt; No touch swipe, no keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing for search engines.&lt;/strong&gt; The raw HTML had no text to index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone opening a portfolio from a LinkedIn message on a phone gives it maybe ten seconds. My old site spent them on a loading bar. It proved I can write shaders; it also proved I hadn't thought about the person on the other side.&lt;/p&gt;

&lt;p&gt;So no preloader, no gate, no scroll-jacking, and the same content on every screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a portfolio for?
&lt;/h2&gt;

&lt;p&gt;Showing your work. It sounds obvious, yet many portfolios bury the visitor under “about me” paragraphs, or pitch their tech stack as if it were sacred, and the projects end up three scrolls down. Here the projects come right after the name: “See the work” is the first button, the tools are one strip of logos, and the about-me has a page of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did the planning work?
&lt;/h2&gt;

&lt;p&gt;Before any code, I wrote the project down in a wiki of markdown pages: the audience, the brand, the budgets, the legal texts. The idea comes from Andrej Karpathy's &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki&lt;/a&gt;: the AI keeps the wiki up to date itself, so what it learns builds up. Three Claude Code hooks put the right pages in front of every session:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session starts.&lt;/strong&gt; The working rules and the lessons load first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each prompt.&lt;/strong&gt; A hook names the wiki pages the task needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The work.&lt;/strong&gt; Claude Code builds and checks with those pages open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn ends.&lt;/strong&gt; If the code changed and the wiki didn't, the turn stops once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;↻ A mistake becomes a one-line lesson, and the next session starts with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More in &lt;a href="https://muszarski.com/en/blog/llm-wiki-project-memory-for-claude-code/" rel="noopener noreferrer"&gt;An LLM wiki: project memory for Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the research show?
&lt;/h2&gt;

&lt;p&gt;Agents opened 20 developer portfolios that won Awwwards awards in 2023-2026, on a desktop and on a throttled phone (1.6 Mbps, a 4× slower CPU). Each got two scores from 1 to 5: &lt;strong&gt;Wow&lt;/strong&gt; for how memorable it is, and &lt;strong&gt;Practicality&lt;/strong&gt; for how fast you can reach the projects and the contact. Practicality was measured as the moment the first real link becomes clickable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;20 award-winning portfolios by Wow and Practicality (one run each, 5 October 2026)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Practicality&lt;/th&gt;
&lt;th&gt;Wow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edoardo Lunardi&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clément Grellier&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elliott Mangham&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valentin Mor&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antoine Wodniack&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jesper Landberg&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stefan Vitasović&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grégory Lallé&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rafa de las Heras&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiroto Sato&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ravi Klaassens&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abhishek Jha&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Francesco Michelini&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lusion (studio)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max Milkin&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corentin Bernadou&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kris Temmerman&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bruno Simon&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samsy&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jordan Delcros&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Both scores 4 or more: only 4 of 20 sites&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What came out of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An award doesn't mean usable.&lt;/strong&gt; On the throttled phone only 8 of the 20 were usable within 3 seconds, and 9 weren't usable within 10. The four least practical (game worlds and gated intros) even had a higher average Site of the Day score than the three most practical ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The jury isn't hiring.&lt;/strong&gt; Awwwards weighs design 40%, usability 30%, creativity 20% and content 10%, and the jurors are designers and developers. A recruiter asks one thing: can I find the projects and the contact?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stunning and practical is possible, but rare.&lt;/strong&gt; Only four sites scored 4 or more on both. What they share: real links on the first screen, and the show beside the content, never in front of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what that looks like on a phone. First, the immersive end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe12zzwuduuw6rpet1iec.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe12zzwuduuw6rpet1iec.webp" alt="Bruno Simon's portfolio on a phone: a small 3D car in a purple world and the words Click to start" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bruno Simon: Click to start&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblg07shnsb3cwhmmt0ub.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblg07shnsb3cwhmmt0ub.webp" alt="Samsy's portfolio on a phone: a dark screen with a glowing red robot and no links" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Samsy: No links in the HTML&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiktawzmbmecxxb7wg81y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiktawzmbmecxxb7wg81y.webp" alt="Jordan Delcros's portfolio on a phone: a black screen with a thin loading bar" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jordan Delcros: About 20 s of loading&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three of the most memorable sites in the study, on the throttled phone (5 October 2026). Quoted for review; the sites belong to their authors: bruno-simon.com, samsy.ninja, jordan-delcros.com.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the four that did both. Each one gave this site an idea:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4zwajgmxppzycq8xhws.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4zwajgmxppzycq8xhws.webp" alt="Edoardo Lunardi's portfolio on a phone: desktop-style windows with an About text, links and a work gallery" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Edoardo Lunardi: Real links on the first screen&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8jfaz7mb0r4jih79i4h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8jfaz7mb0r4jih79i4h.webp" alt="Jesper Landberg's portfolio on a phone: a vertical list of project cards" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jesper Landberg: The show beside the content&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1zklnhfedu0d6mhy4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1zklnhfedu0d6mhy4.webp" alt="Antoine Wodniack's portfolio on a phone: red wavy lines, the words Creative Developer and a Hire me button in the header" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Antoine Wodniack: Hire me in the header&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jysnzyq0jm2t8g97byh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jysnzyq0jm2t8g97byh.webp" alt="Stefan Vitasović's portfolio on a phone: big type cut by grey redaction bars" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stefan Vitasović: One signature effect&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stunning and practical, and what I took from each. Quoted for review; the sites belong to their authors: edoardolunardi.dev, jesperlandberg.com, wodniack.dev, stefanvitasovic.dev.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Section ideas came from &lt;a href="https://mobbin.com/" rel="noopener noreferrer"&gt;Mobbin&lt;/a&gt;, a library of real screens from shipped products. The topic chips above my contact form are one of them: you start with a tap, not a blank field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not another 3D world?
&lt;/h2&gt;

&lt;p&gt;Honestly, I love them. My last portfolio was one. But I had measured both kinds of site by then, and the trade looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;A full 3D world&lt;/th&gt;
&lt;th&gt;This site&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First screen&lt;/td&gt;
&lt;td&gt;a loader or “click to start”&lt;/td&gt;
&lt;td&gt;my name, my role and two buttons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links&lt;/td&gt;
&lt;td&gt;often none in the HTML (Samsy, Jordan Delcros)&lt;/td&gt;
&lt;td&gt;every link is real HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search engines and screen readers&lt;/td&gt;
&lt;td&gt;text drawn in WebGL is invisible to them&lt;/td&gt;
&lt;td&gt;real text, painted first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow phone&lt;/td&gt;
&lt;td&gt;9 of 20 award winners unusable within 10 s&lt;/td&gt;
&lt;td&gt;shaders start after the text, only while visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No WebGL or WebGPU&lt;/td&gt;
&lt;td&gt;an error instead of the site (Kris Temmerman)&lt;/td&gt;
&lt;td&gt;a CSS picture underneath&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the wow stays, as a layer instead of a gate. Four shaders run on this site, all written for it with no library:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fyc4vxy7m6w2pzv7pp5.webp" alt="The All projects head: a dithered cloud of colour gathers behind a wall of project tiles and takes the colour of the tile under the pointer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Dither field&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zp7sprr6x6baxsryvqr.webp" alt="The footer: faint contour lines drift under the links and rise under the pointer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contour lines&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme1o2dkph8v10u1fothc.webp" alt="Hobby prints on My life: picking a hobby wipes a new two-ink riso print in dot by dot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Riso prints&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjupkyq9oajdsfcfg6nm.webp" alt="The My life head: a colourful goo behind the title swells when poked" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Goo&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A dither field on All projects, contour lines in every footer, riso prints and goo on My life. Each starts after the text is on screen, runs only while visible, and holds still for reduced motion or the pause button.&lt;/em&gt; The clips play on &lt;a href="https://muszarski.com/en/blog/case-study/" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I run the AI?
&lt;/h2&gt;

&lt;p&gt;Like a small team. I brief the lead agent, subagents research in parallel, MCP servers bring real data, hooks guard the risky commands, checks gate the result, and I review it. The AI does a lot of the typing; I own the decisions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On the home page: Watch one change, step by step&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same process as a live story: who works on each step and what it looks like. &lt;a href="https://muszarski.com/en/#ai" rel="noopener noreferrer"&gt;See the seven steps&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The building blocks are in &lt;a href="https://muszarski.com/en/blog/claude-code-skills-hooks-mcp-subagents/" rel="noopener noreferrer"&gt;Claude Code skills, hooks, MCP servers and subagents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened to the first design?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gek711p8jmoeo114yni.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gek711p8jmoeo114yni.webp" alt="The rejected first design: a pale paper page with a faint grid, a small heading and a boxed contact block" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafting Table, the first look. It passed every test and nobody would remember it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So Claude built four very different directions as live pages. It recommended Orbit. I picked Poster: brutalism with manners. Flat saturated colour, hard offset shadows, square buttons and type fitted edge to edge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxuj7j1pn1nb2zap4r12.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxuj7j1pn1nb2zap4r12.webp" alt="Orbit: the name over a particle planet" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Orbit: Claude's pick&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbsyykkjj6vqbe6eqlx3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbsyykkjj6vqbe6eqlx3.webp" alt="Poster: the name in huge capitals on cobalt" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Poster: My pick&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvw4nm47u9do8k17v02bt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvw4nm47u9do8k17v02bt.webp" alt="Toybox: soft 3D toy shapes next to the name" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Toybox&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwic51u7o52fl02mhuea5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwic51u7o52fl02mhuea5.webp" alt="Exhibit: the name in a serif over a dark red glow" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Exhibit&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In Poster the type itself is the picture, so the design is already there at first paint.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How did each part change, and why?
&lt;/h2&gt;

&lt;p&gt;Six parts of the site through their versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI section
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Round 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb9wy6w6ie9p81qexa34.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdb9wy6w6ie9p81qexa34.webp" alt="Round 2: the heading Delegate. Verify. next to a checklist of gates" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A checklist of gates. All true, and all the floor. It sold the checks, not the way I work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round 3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx5jtm7rxejbplrt3ehm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx5jtm7rxejbplrt3ehm.webp" alt="Round 3: the heading Delegate. Automate. above a row of counters for hooks, wiki pages, subagents, MCP servers, skills and lessons" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Counters for hooks, pages and agents. Numbers that change every week and say nothing about the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwi1veju3c91fimonhxtw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwi1veju3c91fimonhxtw.webp" alt="Now: I run AI like a team. The team of me and six agents above seven numbered steps, with step 5, Build, open: a terminal where a force push is stamped Blocked" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Show how the work gets done, not how much of it there is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One change in seven steps, from my brief to my review, each with a small drawn scene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Competences (once Credentials)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Round 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyrnteckmrrqoaxo6r5d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyrnteckmrrqoaxo6r5d.webp" alt="Round 2: a grid of seven credential cards, each with a paragraph" width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seven cards, each a paragraph. Everything visible, nothing readable at a glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round 3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx11fk0l4g1leeatuisht.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx11fk0l4g1leeatuisht.webp" alt="Round 3: tall collapsed cards with vertical titles and one card open with a picture" width="799" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sideways titles that open on hover. Compact, and hard to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgbzaqjz7ia2di1g07rt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgbzaqjz7ia2di1g07rt.webp" alt="Now: a readable list on the left and the Experience card open on the right, with school and three internships as tickets" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Experience first, and every certificate checkable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A list on the left, one open card on the right, a link to check every certificate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contact
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Round 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbtfbttxd4eujadba1vna.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbtfbttxd4eujadba1vna.webp" alt="Round 2: Let's talk, with an email field, a message field and a row of buttons" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A normal form, and every field is a reason to stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round 3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5falsk1e5oyo2girmfw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5falsk1e5oyo2girmfw.webp" alt="Round 3: Let's talk, with one message box and three topic chips" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As few fields as possible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Message first. The reply field appears once you start typing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjlatjmdtrajx5jqsuo4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjlatjmdtrajx5jqsuo4.webp" alt="Now: Let's talk, with topic chips, a message box, a Send button and three direct links" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste an email, a phone number or a LinkedIn link and the reply field fills itself. It works without JavaScript too.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I work
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Round 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fggpuqiteacpkxemvwrgt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fggpuqiteacpkxemvwrgt.webp" alt="Round 2: How I work as four numbered principles with small receipts" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four principles as numbered lines. Easy to write, easy to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn06zzilzcnqh1in4kizk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn06zzilzcnqh1in4kizk.webp" alt="Now: Product engineer, a ring of seven stops from client to review, and an open stop with a drawn scene of a shop and a goal, budget and deadline checklist" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tell it as a story you can see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A ring of seven stops, each with a drawn scene and a real example.&lt;/p&gt;

&lt;h3&gt;
  
  
  All projects
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;TV&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6n1405tedgnvd3n2yxb5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6n1405tedgnvd3n2yxb5.webp" alt="The TV version: the heading All my projects, ON AIR, next to a yellow retro TV showing Linkoglot, above project cards in bright colours" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A retro TV flipping through the projects, and a drawn scene wherever there was nothing to film. Fun, but too much of it: it looked vibe coded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Night&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6hw0yu5kst40w4w1qka.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6hw0yu5kst40w4w1qka.webp" alt="The night version: the heading Things I've built over contour lines, a numbered list of projects on the right, and project plates staggered left and right below" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One dark field, a contour map under the title, plates staggered left and right and an X-ray lens over every capture. Calmer, and hard to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4tma5q1806rck8nzx64.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4tma5q1806rck8nzx64.webp" alt="Now: the heading Things I've built next to a tilted wall of project tiles over a dithered blue and pink glow, above a grid of project cards on a light background" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Easy to scan first, then the show.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A wall of every project to jump from, then a plain grid of cards on a light field. The contour lines moved to the footer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blog covers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbefjn1jv820jgpj1ay6c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbefjn1jv820jgpj1ay6c.webp" alt="Six old covers: a stack of papers, two bar charts, a row of boxes, a shield, the letters A A a, and EN and PL bubbles" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Colours came from the card's place in the list, so a post changed colour between pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9c3i90h4rjavwu5vi2tl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9c3i90h4rjavwu5vi2tl.webp" alt="Six new covers: linked wiki pages, a level scale, a terminal wired to helpers, a crossed-out cookie next to bars, one wide letter between two walls, and two swapped speech bubbles" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One palette per cover, and the topic drawn literally.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A scale for a comparison, a crossed-out cookie for cookieless analytics. No words.&lt;/p&gt;

&lt;p&gt;The rule behind all six: show the thing, cut the paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the other pages look like?
&lt;/h2&gt;

&lt;p&gt;Every page gets the same care as the home page, on a laptop and on a phone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j801c2vh0oghwkb3562.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j801c2vh0oghwkb3562.webp" alt="All projects: the heading Things I've built next to a tilted wall of project tiles over a dithered blue and pink glow" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All projects&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F465vm3yv9gmvdnmy2sjq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F465vm3yv9gmvdnmy2sjq.webp" alt="My life: the words My life in huge outlined capitals over a colourful blob, next to a stack of holiday photos" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My life&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhy5p23jnh4p40u4wczv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhy5p23jnh4p40u4wczv.webp" alt="The CV page: the name in huge capitals on cobalt, a photo, the role and a Download PDF button next to the printed CV" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CV&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flu599iu8fk7xockar1vl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flu599iu8fk7xockar1vl.webp" alt="The blog: the word Blog in huge capitals on cobalt above search, kind, topic and sort filters" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Blog&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All projects, My life, the CV and the blog at 1440 px.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvk2grzpyt2snxjx1lq5v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvk2grzpyt2snxjx1lq5v.webp" alt="Four phone screens side by side: the home page, All projects, My life and the CV" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same site on a phone: home, All projects, My life and the CV. Same content, one column.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the small details?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The name fills the screen without JavaScript.&lt;/strong&gt; At build time a script measures the font and works out the exact width for each line, so the name fits edge to edge before any script runs (&lt;a href="https://muszarski.com/en/blog/fit-text-to-width-at-build-time/" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r2o37fshyd6ebgls1ym.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r2o37fshyd6ebgls1ym.webp" alt="The home page at three widths side by side: a phone, a tablet and a wide desktop, each with the name filling the width" width="800" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The home page at 390, 1024 and 1920 pixels.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phones get the same site.&lt;/strong&gt; Every hover effect has a twin for touch (&lt;a href="https://muszarski.com/en/blog/hover-effects-on-phones/" rel="noopener noreferrer"&gt;how&lt;/a&gt;), and the contact form has its own write-up, bug included (&lt;a href="https://muszarski.com/en/blog/contact-form-on-a-static-site/" rel="noopener noreferrer"&gt;a contact form on a static site&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Who else is it built for?
&lt;/h2&gt;

&lt;p&gt;Everyone, including people who use the web differently. A site that only works with a mouse and perfect eyesight shuts people out, and I'd rather it didn't. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Motion you can stop.&lt;/strong&gt; One “Pause motion” button stops everything, and with reduced motion turned on, everything shows its final state (&lt;a href="https://muszarski.com/en/blog/reduced-motion-and-a-pause-button/" rel="noopener noreferrer"&gt;how that works&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A keyboard gets everywhere.&lt;/strong&gt; A skip link jumps past the header, every hover has a focus twin, and focus never hides under the sticky header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen readers hear the full story.&lt;/strong&gt; Hidden text adds what the eye gets from context, like “opens in a new tab”, and every button's name starts with the words you see on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast is tested, not eyeballed.&lt;/strong&gt; A unit test checks every colour pair, and axe runs on every page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do search engines know who I am?
&lt;/h2&gt;

&lt;p&gt;Sitemaps and meta tags are the floor. To find me by name, and one day show a knowledge panel, a search engine has to be sure who the site is about. So every page tells it the same story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One person, one id.&lt;/strong&gt; The home page and the CV describe me as one &lt;code&gt;Person&lt;/code&gt; in structured data: my name, my role, my real portrait and my profiles. Every post and My life point back to it, and the links to my profiles carry &lt;code&gt;rel="me"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real photos with real facts.&lt;/strong&gt; My life shows my own photos, each one marked up with where and when it was taken, and listed in the image sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A named author.&lt;/strong&gt; Every post is a &lt;code&gt;BlogPosting&lt;/code&gt; by that same person, with an author box under it, and every outside fact has a dated source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A footer that does the work.&lt;/strong&gt; Every page links to the projects, the CV, the blog, its RSS feed and my profiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One address per page.&lt;/strong&gt; One canonical URL, the language versions linked to each other, and a share card for every page in every language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The build fails if a page loses its title, description or canonical, or if the structured data stops parsing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzalc54n5i9agahzkqe8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzalc54n5i9agahzkqe8.webp" alt="The author box under a post: my avatar, my name, a one-line bio and links to More about me, the CV and Contact" width="673" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Author box&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhbo36ix1tz63b98j3yu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhbo36ix1tz63b98j3yu.webp" alt="The site footer: columns for Work, Site with Blog, CV, My life and RSS, Elsewhere with my profiles, the legal pages and a Pause motion button" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Footer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The author box under every post and the footer on every page: the same person, the same links, everywhere.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How is every change checked?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static.&lt;/strong&gt; Types, lint and unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers.&lt;/strong&gt; Playwright on every page, with axe for accessibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eyes.&lt;/strong&gt; Screenshots at phone, tablet and desktop widths that I look at myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; Lighthouse on a slow phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;↻ Anything that fails becomes a fix, and a repeated mistake becomes a lesson in the wiki.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nothing is done until the checks pass and I've looked at the screenshots myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is it built with?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Astro 7 (&lt;a href="https://muszarski.com/en/blog/astro-vs-nextjs-for-a-portfolio/" rel="noopener noreferrer"&gt;why not Next.js&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language and styling&lt;/td&gt;
&lt;td&gt;TypeScript, Tailwind CSS 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translations&lt;/td&gt;
&lt;td&gt;Lingui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;Turborepo and bun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Font&lt;/td&gt;
&lt;td&gt;Anybody, a variable font (OFL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icons&lt;/td&gt;
&lt;td&gt;Tabler (MIT) and Simple Icons (CC0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact form&lt;/td&gt;
&lt;td&gt;EmailJS behind a Vercel function, Cloudflare Turnstile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Umami Cloud, cookieless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;Playwright, axe-core, Vitest, Lighthouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where did it end up?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokdfev9ts7fcrh63n8oz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokdfev9ts7fcrh63n8oz.webp" alt="A Lighthouse report for the home page on mobile: four green gauges for Performance, Accessibility, Best Practices and SEO" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The home page in Lighthouse on mobile, from a local production build on 7 October 2026. Lab numbers: real-user data comes after launch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you'd like to talk about building something like this, or about a role, &lt;a href="https://muszarski.com/en/#contact" rel="noopener noreferrer"&gt;my contact form is one message long&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How this post was made: I wrote it with Claude Code. It drafted the text from my project's wiki, code and notes; I set the direction, and reviewed and approved it before it went out.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;llm-wiki.md, Andrej Karpathy's idea file (GitHub Gist, 4 April 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring" rel="noopener noreferrer"&gt;Lighthouse performance scoring (Chrome for Developers)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html" rel="noopener noreferrer"&gt;Understanding Success Criterion 2.2.2: Pause, Stop, Hide (W3C)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.awwwards.com/websites/" rel="noopener noreferrer"&gt;Awwwards, Sites of the Day&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mobbin.com/" rel="noopener noreferrer"&gt;Mobbin, a library of real app and web screens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dequelabs/axe-core" rel="noopener noreferrer"&gt;axe-core, accessibility rules engine (Deque)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.solarsystemscope.com/textures/" rel="noopener noreferrer"&gt;Solar System Scope, planet textures (CC BY 4.0), used in the old space portfolio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fonts.google.com/specimen/Anybody" rel="noopener noreferrer"&gt;Anybody, a variable font (Google Fonts)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>astro</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
