<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Oleksii Mazurenko — Blog</title>
    <link>https://oleksiimazurenko.dev/nb/blog</link>
    <description>Technical articles about web development, performance optimization, and developer tools.</description>
    <language>nb</language>
    <lastBuildDate>Sat, 13 Jun 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://oleksiimazurenko.dev/nb/blog/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Claude Code Multi-Profile, v2: en ren XDG-arkitektur</title>
      <link>https://oleksiimazurenko.dev/nb/blog/claude-profiles-clean-architecture</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/claude-profiles-clean-architecture</guid>
      <description>Etter en måned med den gamle alias-baserte tilnærmingen bygde jeg om Claude Code multi-profile-oppsettet mitt til en XDG-kompatibel struktur under ~/.config/claude-profiles/. Den egentlige grunnen var ikke ryddighet — det var oppdagelsen av at ~/.claude.json er en separat fil i home-roten der MCP-servere lagt til via --scope user stille bodde.</description>
      <content:encoded><![CDATA[<p>For en måned siden publiserte jeg <a href="/nb/blog/multiple-claude-accounts-one-device">et innlegg om hvordan kjøre to Claude Code-kontoer parallelt på samme maskin</a> — privat og jobb, via <code>CLAUDE_CONFIG_DIR</code> og et shell-alias. Tilnærmingen fungerte. Alt gjorde det det skulle gjøre.</p>
<p>Men etter en måned med reell bruk støtte jeg på et par solide problemer som det innlegget ikke dekket. Det største — en skjult særegenhet ved Claude Code som jeg ved et uhell tråkket midt oppi, da jeg la til Gmail-MCP-en og den "forsvant" fra profilen min fem minutter senere. I dag bygde jeg alt om til en ny arkitektur — XDG-kompatibel, med en symlink og en interaktiv profilvelger via <code>gum</code>. Dette er v2 — en evolusjon ut av reell erfaring, ikke en teoretisk forbedring.</p>
<h2>Det som begynte å skurre</h2>
<p>Fire ting. De tre første handler om ryddighet, synlighet og skala. Den fjerde er den ekte arkitektoniske fellen jeg ikke så med en gang. Jeg går gjennom dem i rekkefølge, fordi det er nettopp den fjerde som til slutt tvang frem ombyggingen.</p>
<h3>1. To separate mapper i $HOME — det er rotete</h3>
<p><code>~/.claude</code> og <code>~/.claude-promova</code> — to dotfolders side om side i roten av $HOME. XDG Base Directory Specification sier at configs hører hjemme i <code>~/.config/</code>. Dotfile-mapper strødd rett i home er et antimønster som over tid gjør $HOME til en rotekrok. Kosmetisk, ja, men det irriterte hver gang jeg så <code>ls -la ~</code>.</p>
<h3>2. Ingen visuell bekreftelse på aktiv profil</h3>
<p>Jeg starter <code>claude</code> og vet ikke hvilken profil som er aktiv — før jeg kjører <code>claude config list</code> inne i sesjonen. Hvis jeg glemte hvilken terminal jeg startet hvor, må jeg sjekke. En bagatell, men med personal + work i parallell i to faner samler det seg opp.</p>
<h3>3. Alias skalerer ikke</h3>
<p>To profiler — <code>claude</code> og <code>claude-promova</code> — det er greit. Jeg legger til en tredje (en frilanskunde) — da trengs et tredje alias. En fjerde — et fjerde. Etter et halvt år ville jeg ikke huske hvilke aliaser jeg faktisk hadde opprettet.</p>
<h3>4. Den skjulte fellen i ~/.claude.json</h3>
<p>Og dette er den egentlige grunnen til ombyggingen. Claude Code har <strong>to forskjellige steder</strong> for konfigurasjon, og dokumentasjonen roper det ikke ut: <code>~/.claude/</code> — katalogen der <code>projects/</code>, <code>sessions/</code>, <code>hooks/</code>, <code>skills/</code> ligger. Og separat — <code>~/.claude.json</code>, en fil rett i $HOME-roten, der <code>oauthAccount</code>, <code>mcpServers</code>, <code>projects</code>-historikken, <code>skillUsage</code> og rundt 40 andre felt av ekte live state bor.</p>
<p>Kommandoen <code>claude mcp add --scope user</code> skriver akkurat til <code>~/.claude.json</code> i home-roten, og <strong>ikke</strong> til <code>~/.claude/.claude.json</code> eller til profilens katalog. Det visste jeg ikke. Helt til jeg en dag tråkket rett i det.</p>
<h2>Discovery: hvorfor Gmail-MCP-en &quot;forsvant&quot;</h2>
<p>I dag tidlig satt jeg opp Gmail-MCP-en i Claude Code. Standard oppsett: Google Cloud-prosjekt, OAuth-credentials, <code>claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp</code>. Alt OK. Startet sesjonen på nytt — virker, jeg leser mail, svarer på meldinger. En time senere begynte vi å refaktorere aliasene til en funksjon med <code>gum</code>-profilvelger, og deretter flyttet vi alt over til XDG. Jeg kjørte <code>mv ~/.claude → ~/.config/claude-profiles/personal</code>, startet CC på nytt, valgte personal i menyen. Og i den nye sesjonen åpnet jeg <code>/mcp</code>:</p>
<pre><code>figma            (failed)
playwright-test
claude.ai Notion</code></pre>
<p>Ingen gmail. Ingen vaultforge. Bare tre servere, hvorav én til og med var failed. Og jeg hadde nettopp lagt til Gmail. Samtidig viste sesjonen i en annen terminal (work-profilen) Gmail og Vaultforge uten noe problem.</p>
<p>Jeg begynte å grave og fant at jeg på maskinen hadde <strong>tre</strong> ulike filer med navnet <code>.claude.json</code>:</p>
<table><thead><tr><th>Path</th><th>Size</th><th>mcpServers</th></tr></thead><tbody><tr><td>&lt;code&gt;~/.claude.json&lt;/code&gt; (home root)</td><td>113 KB</td><td>figma, gmail, vaultforge</td></tr><tr><td>&lt;code&gt;~/.config/claude-profiles/personal/.claude.json&lt;/code&gt;</td><td>29 KB</td><td>figma, playwright-test</td></tr><tr><td>&lt;code&gt;~/.config/claude-profiles/promova/.claude.json&lt;/code&gt;</td><td>40 KB</td><td>figma, playwright-test, vaultforge</td></tr></tbody></table>
<p>Der hadde vi det. Det er nettopp det som er den arkitektoniske fellen:</p>
<ol><li><code>claude mcp add --scope user</code> skriver alltid til <code>~/.claude.json</code> i home-roten, uavhengig av <code>CLAUDE_CONFIG_DIR</code></li><li>Når <code>CLAUDE_CONFIG_DIR</code> er satt, leser Claude Code <code>$CLAUDE_CONFIG_DIR/.claude.json</code> — altså filen inne i profilen</li><li>Oppføringene for "user-scope MCPs" og "profilens MCPs" bor i <strong>forskjellige filer</strong> med samme navn — og det er lett å blande dem</li></ol>
<p>I mitt tilfelle var <code>~/.claude.json</code> (home-roten, 113 KB) den levende, aktuelle tilstanden — med gmail, vaultforge, OAuth-sesjonen, alt. Og <code>~/.config/claude-profiles/personal/.claude.json</code> (29 KB) viste seg å være en gammel snapshot som av en eller annen grunn allerede tidligere lå i gamle <code>~/.claude/</code> — kanskje skrev en eldre CC-versjon dit, kanskje et plugin. <code>jq -r 'keys[]'</code> på begge filene viste at home-root-versjonen hadde 41 unike nøkler som ikke fantes i snapshoten.</p>
<p>Og de 41 nøklene er ikke søppel. Det er Claude Codes virkelige tilstand:</p>
<ul><li><code>skillUsage</code> — bruksstatistikk for skills</li><li><code>githubRepoPaths</code> — repo-cache for prosjektnavigering</li><li><code>cachedGrowthBookFeatures</code> + <code>cachedStatsigGates</code> — feature flags (uten dem henter CC ferske ved hver oppstart)</li><li><code>hasShownOpus45Notice</code>, <code>hasShownOpus46Notice</code>, <code>hasShownS1MWelcomeV2</code> — UI-flagg (uten dem dukker modalene opp igjen ved neste oppstart)</li><li><code>lastPlanModeUse</code>, <code>feedbackSurveyState</code>, <code>installMethod</code> — onboarding- og UX-state</li></ul>
<p>Hvis du bare kjører <code>mv ~/.claude ~/.config/claude-profiles/personal</code> uten merge — mister du alt dette. Du ser welcome-modalene igjen, githubRepoPaths-søket kjører på nytt, alle survey-spørsmålene kommer tilbake. Som jeg nesten gjorde.</p>
<h2>Den nye arkitekturen</h2>
<p>Alt bor under én felles foreldrekatalog i <code>~/.config/</code>, slik XDG vil ha det. Hver profil er selvstendig — den har sin fulle state inkludert sin egen <code>.claude.json</code>. <code>~/.claude</code> blir igjen som symlink til personal-profilen for bakoverkompatibilitet.</p>
<pre><code>~/.config/claude-profiles/
├── personal/
│   ├── .claude.json          ← full live state + MCP list (113 KB)
│   ├── projects/, sessions/, hooks/, skills/, ...
│   └── CLAUDE.md
└── promova/
    ├── .claude.json          ← work state + work MCPs (41 KB)
    └── projects/, sessions/, agents/, skills/, ...

~/.claude  →  symlink to ~/.config/claude-profiles/personal/</code></pre>
<p>Ingen <code>.claude.json</code> i $HOME-roten. Hver profil er en separat, isolert katalog der alt ligger: <code>projects</code>/<code>sessions</code>-mappene og den samme filen med MCP-servere og OAuth-tokens. Én source of truth per profil.</p>
<h3>Hvorfor symlinken peker på personal</h3>
<p>Alt som hardkoder stien <code>~/.claude/</code> — gamle skript, plugins, IDE-utvidelser for Claude Code, statusline-configs som <code>claude-powerline.json</code> — fortsetter å virke uten endringer. Symlinken løses opp til personal-profilen. Hvis du ved et uhell kjører <code>command claude</code> (uten wrapper-funksjonen) — havner du også i personal via default-path lookup. Personal blir den "quiet default" den var før, men bor nå fysisk på XDG-plassen.</p>
<h3>Interaktiv velger ved oppstart — funksjon + gum</h3>
<p>I stedet for aliaser — en funksjon <code>claude()</code> i <code>~/.zshrc</code> som viser en pilmeny via <code>gum</code> (Charm sin TUI-hjelper). Funksjonen fanger opp <code>claude</code>-kallet på shell-nivå, lar deg velge profil og kjører <code>command claude</code> med tilhørende <code>CLAUDE_CONFIG_DIR</code>. <code>command</code> er viktig — det går utenom wrapper-funksjonen og kaller den ekte binærfilen.</p>
<pre><code># brew install gum

# Claude Code: profile picker on launch
claude() {
  local profile
  profile=$(gum choose \
    --header &quot;Claude profile:&quot; \
    --cursor &quot;▸ &quot; \
    --selected.foreground 212 \
    --cursor.foreground 212 \
    --header.foreground 244 \
    &quot;personal&quot; &quot;promova&quot;) || return
  case &quot;$profile&quot; in
    personal) CLAUDE_CONFIG_DIR=&quot;$HOME/.config/claude-profiles/personal&quot; command claude &quot;$@&quot; ;;
    promova)  CLAUDE_CONFIG_DIR=&quot;$HOME/.config/claude-profiles/promova&quot;  command claude &quot;$@&quot; ;;
  esac
}</code></pre>
<p>Slik ser det ut ved oppstart:</p>
<pre><code>Claude profile:
▸ personal
  promova</code></pre>
<p>↑↓ for å navigere, Enter for å velge, Esc for å avbryte (Claude starter rett og slett ikke). Profilen er alltid synlig — umulig å gå glipp av.</p>
<h2>Migreringsskript</h2>
<p>For den som leser dette og vil flytte fra det gamle oppsettet. Det mest kritiske steget er det andre: det merger <code>~/.claude.json</code> fra home-roten med det som allerede ligger i personal-profilen, og kombinerer <code>mcpServers</code>-listene. Uten det steget mister profilen både MCP-ene sine og hele live-staten.</p>
<pre><code># 1. Move existing dotfolders into a new XDG-style parent.
#    APFS mv is an inode rename — safe even if claude --resume is open in
#    another terminal, file descriptors stay alive on the same inode.
mkdir -p ~/.config/claude-profiles
mv ~/.claude          ~/.config/claude-profiles/personal
mv ~/.claude-promova  ~/.config/claude-profiles/promova   # rename as needed

# 2. CRITICAL: merge ~/.claude.json (the live state — likely 100+ KB) into
#    the personal profile, unioning mcpServers so no MCP is dropped.
jq -s &apos;.[0] * {mcpServers: (.[0].mcpServers + .[1].mcpServers)}&apos; \
  ~/.claude.json \
  ~/.config/claude-profiles/personal/.claude.json \
  &gt; /tmp/personal-merged.json
mv /tmp/personal-merged.json ~/.config/claude-profiles/personal/.claude.json

# 3. Fix permissions — jq+mv inherits umask (likely 644), but this file
#    holds OAuth tokens. Tighten to 600 immediately.
chmod 600 ~/.config/claude-profiles/personal/.claude.json

# 4. Back up the orphaned home-root file (delete once verified)
mv ~/.claude.json ~/.claude.json.migrated.bak

# 5. Symlink for backward compatibility (statusline configs, IDE plugins,
#    anything that hardcodes ~/.claude/ keeps working unchanged)
ln -s ~/.config/claude-profiles/personal ~/.claude</code></pre>
<p>Det tredje steget om rettigheter er viktig i seg selv. <code>jq | mv</code> oppretter en fil med umask 644 (world-readable). Inni er det OAuth-tokens. <code>chmod 600</code> umiddelbart etter mergen er obligatorisk.</p>
<p>Etter migreringen — lukk og åpne alle aktive Claude-sesjoner, last shellet på nytt (<code>source ~/.zshrc</code> eller ny terminal), kjør <code>claude</code>, velg profil, sjekk via <code>claude mcp list</code> at alle MCP-er er på plass. Hvis alt er greit — slett <code>~/.claude.json.migrated.bak</code>. Hvis noe er galt — rollback er trivielt: <code>mv ~/.claude.json.migrated.bak ~/.claude.json</code> og fjern symlinken.</p>
<h2>Det du får</h2>
<ul><li>Én felles foreldrekatalog i stedet for to dotfolders i $HOME — XDG-kompatibelt</li><li>Symlinken bevarer kompatibilitet med alt som hardkoder <code>~/.claude/</code></li><li>Hver profil er selvstendig — sin fulle state og sine MCP-er i sin egen <code>.claude.json</code></li><li>Én source of truth per profil — slutt på foreldreløs config i home-roten som stille driver fra profilens</li><li>Sensitiv data (oauthAccount, tokens) garantert med rettigheter 600</li><li>Visuell bekreftelse på aktiv profil ved hver oppstart — umulig å glemme hvilken profil som er aktiv</li><li>Å legge til en tredje profil = å legge til én linje i <code>case</code> i funksjonen, ikke klone et nytt alias og huske navnet</li></ul>
<h2>Der det svikter</h2>
<p>Jeg vil være ærlig. Dette er ikke en gratis forbedring — noen kompromisser følger med på kjøpet, og de er verdt å kjenne på forhånd.</p>
<ul><li><strong><code>gum</code> er en ekstra avhengighet</strong> (<code>brew install gum</code>, ~13 MB). Hvis du av prinsipp ikke vil installere det — fallback til <code>select</code> i zsh eller en enkel <code>read</code>. Funker, men ser ikke like pent ut og mangler pilnavigering.</li><li><strong>En Enter ved hver oppstart.</strong> For den som drar i gang <code>claude</code> titalls ganger om dagen — kan irritere. Alternativ under (direnv).</li><li><strong>Symlinken <code>~/.claude → personal</code> gjør personal til default.</strong> Hvis du må ha work-profilen som default — må du peke om symlinken (<code>ln -sf</code>). Ikke vanskelig, men det er ikke "glem det og ingenting ryker".</li><li><strong>Symlinken kan teoretisk ryke</strong> hvis et eller annet verktøy atomisk overskriver <code>~/.claude.json</code> via et temp+rename-mønster (write-file-atomic). I praksis gjør Claude Code det ikke selv, men hvis du installerer tredjepartsplugins — sjekk.</li><li><strong>Hvis du har ulike Anthropic-kontoer på profilene med ulike planer</strong> — etter bytte kan det være en sub-sekund lag mens Claude Code synker OAuth-state. I min bruk er det ikke merkbart, men det er ikke null.</li></ul>
<h2>Alternativer jeg vurderte</h2>
<p><strong>direnv</strong> — setter <code>CLAUDE_CONFIG_DIR</code> automatisk basert på en <code>.envrc</code> i roten av hvert prosjekt. Null interaksjon, null klikk. Minus: du må legge en <code>.envrc</code> i hver work-root, og hvis du kjører <code>claude</code> i en ukjent mappe — får du default-profilen (kanskje ikke den du vil ha). For den som lever i et begrenset antall work-roots og vil slippe å klikke — er direnv reelt bedre.</p>
<p><strong>Symlink-basert bytting</strong> (én aktiv profil ved å peke om <code>~/.claude</code>-symlinken) vurderte jeg også og forkastet umiddelbart. Du kan ikke ha to terminaler med ulike profiler åpne samtidig — den globale "gjeldende" er én. For meg er det en deal-breaker.</p>
<h2>Konklusjon</h2>
<p>v2 er ikke bare bedre UX oppå v1. Det er en erkjennelse av at Claude Code har en skjult arkitektonisk særegenhet (<code>~/.claude.json</code> som en separat fil i home-roten, skrevet av <code>--scope user</code>-kommandoer uavhengig av <code>CLAUDE_CONFIG_DIR</code>) som man må ta hensyn til hvis man vil ha ekte isolasjon mellom profiler. Den første tilnærmingen (<code>~/.claude</code> + <code>~/.claude-promova</code> + alias) virket til 80 %, men de gjenværende 20 % viste seg som stille state-drift mellom profilene. Nå er det tatt høyde for. Hvis du så vidt begynner — start rett på v2. Hvis du allerede sitter på v1 — migreringsskriptet er over, flyttingen tar fem minutter og bryter ingenting (det er nettopp <code>jq</code>-mergen som redder deg fra å miste state).</p>]]></content:encoded>
      <pubDate>Sat, 13 Jun 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>productivity</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Slik skriver du Claude Code-agenter som ikke lyver til deg</title>
      <link>https://oleksiimazurenko.dev/nb/blog/writing-specialized-agents</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/writing-specialized-agents</guid>
      <description>To regler for å bygge pålitelige Claude Code-agentpipelines: én agent per spesialisering, og shell-kommandoer i stedet for prompts overalt der svaret må være kvantitativt.</description>
      <content:encoded><![CDATA[<p>Du ba Claude Code om å <em>implementere dette designet og verifisere at det stemmer med Figma-mockupen</em>. Den kom tilbake med: <em>Ferdig. Alle seksjoner stemmer, spacing er riktig, farger er riktige.</em> Du åpnet siden. Halvparten av spacingen var gal. Hover-tilstanden fantes ikke. Knappene var en nyanse ved siden av. Modellen løy ikke med vilje — den predikerte at du ville høre <em>verifisert</em>, og produserte nøyaktig den tokensekvensen. Det var ingen verifiseringssteg. Det kunne aldri ha vært det — verifisering krever sammenligning mot ground truth, og én enkelt agent i én enkelt kontekst har ingen mulighet til å tre ut av sitt eget svar og sjekke det.</p>
<p>To regler har gjort hallusinasjonspregede arbeidsflyter om til pålitelige pipelines for meg: <strong>én agent, én spesialisering</strong>, og <strong>alt som kan kjøres som en shell-kommando må kjøres som en shell-kommando</strong>. Dette er ikke teori. Dette er hva jeg gjør hver dag med Claude Code, og det er disse mønstrene som faktisk gjør en forskjell.</p>
<h2>Hvorfor generalistagenter lyver</h2>
<p>LLM-er er next-token-prediktorer. Når en prompt ber om to roller — <em>bygg X</em> og <em>verifiser X</em> — fullfører modellen den første rollen, og predikerer deretter hvordan utdataene fra den andre rollen ville sett ut, uten å faktisk utføre den. Selvverifisering er strukturelt svak: samme kontekst, samme modell, samme blinde flekker. <em>Bestått</em> på verifisering korrelerer med <em>bestått</em> på bygging — de feiler sammen.</p>
<p>Modellen vet ikke at den lyver. Fra dens perspektiv er <em>jeg verifiserte alt nøye</em> en koherent fortsettelse av å ha skrevet koden. Dette er den samme grunnen til at <em>er du sikker?</em>-prompts ikke fanger opp hallusinasjoner: modellen er like sikker på andre gjennomgang. Selvtillit korrelerer ikke med korrekthet — den korrelerer med hvor plausibelt neste setning høres ut.</p>
<p>Løsningen er ikke <em>bedre prompts</em>. <em>Vær forsiktig</em>, <em>dobbeltsjekk</em>, <em>ikke hallusinér</em> — disse instruksjonene gjør ingenting. Løsningen er strukturell: spesialiser agenten slik at den fysisk ikke kan late som, og rut kvantitativt arbeid gjennom shell-en så svaret kommer fra reell tilstand, ikke fra token-sannsynlighet.</p>
<h2>Regel 1: én agent, én spesialisering</h2>
<p>Del arbeidet inn i separate agenter med separate kontekster. Hver agent har ett enkelt ansvar og et begrenset verktøysett. Hele arbeidsflyten blir et stafettløp i stedet for én agent som løper runder:</p>
<ul><li><strong>Builder-agent:</strong> tar spekken, skriver koden. Det er hele jobben dens. Den har <code>Read</code>, <code>Edit</code>, <code>Write</code>, <code>Bash</code>.</li><li><strong>Reviewer-agent:</strong> tar spekken pluss diff-en, sjekker akseptansekriterier. Frisk kontekst. Ingen kunnskap om <em>hvordan</em> koden ble skrevet, bare hva som kom ut. Den har <code>Bash</code>, <code>Read</code>, <code>Grep</code>, <code>Glob</code> — ingen skriveverktøy overhodet.</li><li><strong>Analytics-agent:</strong> svarer på dataspørsmål ved å konstruere og kjøre queries. Kun <code>Bash</code>. Kan ikke nå svaret uten å kjøre en ekte kommando.</li><li><strong>Orchestrator:</strong> hovedsesjonen som sender ut hver agent etter tur og aldri ber én agent gjøre en annens jobb.</li></ul>
<p>Konkret eksempel: UI-implementasjon pluss en visuell sjekk mot en Figma-mockup. Builder-en skriver komponentene og committer diff-en. Orchestrator-en kaller deretter Reviewer-en med design-URL-en, diff-en og eksplisitte akseptansekriterier. Reviewer-en kjører Playwright, tar skjermbilder, differ dem mot referansen og returnerer <code>PASS</code> eller <code>FAIL</code> med de faktiske skjermbildebanene og piksel-diff-ene. Builder-en kommer aldri i nærheten av verifiseringssteget — og det er nøyaktig derfor verifiseringen er ekte.</p>
<p>Anti-mønsteret er mega-agenten: én enkelt prompt som sier <em>bygg dette UI-et og pass på at det stemmer med mockupen</em>. Jeg garanterer deg at den vil rapportere at alt stemmer. Det gjør det ikke. Narrativet om <em>jeg verifiserte</em> er bare den mest sannsynlige tokensekvensen etter <em>jeg bygde det</em>.</p>
<h2>Regel 2: shell fremfor prompt, alltid</h2>
<p>Alt som er kvantitativt, alt som berører reell tilstand, alt der svaret kan være galt på en måte som ser riktig ut — push det gjennom <code>sh</code>. Agentens jobb er å konstruere og kjøre kommandoen, deretter lese utdataene. Agenten er ikke kilden til sannhet. Shell-utdataene er.</p>
<ul><li><strong>Telling:</strong> <code>wc -l logs.txt</code> er sant. <em>Det er omtrent 47 logglinjer</em> fra en modell er en hallusinasjon.</li><li><strong>Analytics:</strong> <code>psql -c "SELECT count(*) FROM events WHERE created_at &gt; now() - interval '30 days'"</code>. Ikke <em>estimer volumet</em>.</li><li><strong>Tester:</strong> <code>pnpm test --reporter=json | jq '.numFailedTests'</code>. Ikke <em>oppsummer hva som feilet</em>.</li><li><strong>Git-tilstand:</strong> <code>git rev-list --count main..HEAD</code>, <code>git diff --stat</code>. Ikke <em>tell committene</em> eller <em>beskriv endringene</em>.</li></ul>
<p>Når du har internalisert dette, begynner du å legge merke til hvert sted der agenten var i ferd med å finne på et tall. <em>Det ser ut til å være rundt 200 poster...</em> — nei. Kjør <code>SELECT count(*)</code>. <em>De fleste testene består...</em> — nei. Kjør testsuiten, parse JSON-en. Modellen er utmerket til å konstruere kommandoen. Den er upålitelig som kommandoen.</p>
<h2>Feilmodi jeg faktisk har truffet på</h2>
<p>Dette er ikke hypotetiske eksempler. Hvert av disse kostet meg reell tid før jeg endret mønsteret:</p>
<ul><li><strong>Fantomverifisering.</strong> Agenten sa <em>jeg sjekket alle 14 seksjoner mot mockupen</em>. Den åpnet ikke mockupen. Den tok ikke et skjermbilde. Sjekken var et hallusinert steg i narrativet.</li><li><strong>Selvsikre gale tall.</strong> Spurte om monthly active users fra analysedata. Fikk et tall som var ~3× feil. Modellen interpolerte fra eksempelrader i stedet for å kjøre den faktiske spørringen.</li><li><strong>Oppdiktede filendringer.</strong> Agenten sa <em>jeg oppdaterte <code>config/feature-flags.json</code></em>. Det hadde den ikke. Den hadde bare hatt til hensikt å gjøre det. <code>git diff</code> var tom.</li><li><strong>Falske testkjøringer.</strong> <em>Alle tester består.</em> Ingen tester ble kjørt. Agenten kalte aldri testkjøreren — den predikerte hvordan testkjørerens utdata ville ha sett ut.</li></ul>
<p>Alle fire løses av de samme to reglene: del opp agenten, push til shell. Reviewer-en har ikke <code>Write</code>, så den kan ikke fake-redigere filer. Analytics-agenten har bare <code>Bash</code>, så den kan ikke returnere et tall som ikke kom fra en spørring. Strukturell umulighet slår gode intensjoner hver gang.</p>
<h2>Slik strukturerer du dette i Claude Code</h2>
<p>Claude Code støtter sub-agenter definert i <code>.claude/agents/*.md</code>. Hver agentfil deklarerer et navn, en beskrivelse, et tillatt verktøysett og en systemprompt. Orchestrator-en (din hovedsesjon) sender dem ut med <code>Agent</code>-verktøyet. Her er den typen definisjon jeg bruker for reviewer-en — kort, smal og fysisk ute av stand til å skrive kode:</p>
<pre><code>---
name: reviewer
description: Reviews a diff against acceptance criteria. Cannot edit code.
tools: Bash, Read, Grep, Glob
---

You are a strict code reviewer. You receive:
- A diff (already produced by the builder)
- Acceptance criteria

Your job:
1. Run the build, the tests, the linter — through Bash.
2. Read the changed files directly.
3. Compare the actual behavior to the criteria.
4. Report PASS or FAIL with concrete evidence (command output, file excerpts).

You must NOT:
- Trust the builder&apos;s summary
- Assume anything was verified just because it was claimed
- Mark something PASS without running the actual check</code></pre>
<p>Legg merke til verktøysettet: <code>Bash, Read, Grep, Glob</code>. Ingen <code>Write</code>, ingen <code>Edit</code>, ingen <code>Agent</code>. Reviewer-en kan kjøre kommandoer, lese filer, søke etter mønstre — og ingenting annet. Hvis den prøver å presentere en hallusinert diff som <em>verifisert</em>, gjør formen på verktøykallene det åpenbart: det var ingen ekte sjekker. Du kan auditere verktøykallene og se nøyaktig hva som ble inspisert.</p>
<p>Orkestreringsmønsteret: hovedsesjonen kaller Builder → venter → kjører <code>git diff</code> selv for å fange den faktiske endringen → kaller Reviewer med spekken og diff-en → leser Reviewer-ens kjennelse. Hovedsesjonen ber aldri én agent gjøre begge deler. Verktøybegrensninger er sterkere enn promptinstruksjoner: <em>ikke fake verifiseringen</em> er et ønske. Å ikke ha <code>Write</code> er et faktum.</p>
<h2>Anti-mønstre å legge bak seg</h2>
<p>Ting jeg ser i prompts som ikke gjør noe — eller verre, gir en falsk følelse av sikkerhet:</p>
<ul><li><strong><em>Vær forsiktig og dobbeltsjekk arbeidet ditt.</em></strong> Genererer ingen ekstra atferd. Modellen produserer allerede det som ser ut som forsiktig arbeid.</li><li><strong><em>Pass på at du faktisk verifiserer.</em></strong> Ordet <em>faktisk</em> tilfører ingen semantikk modellen kan handle på. Den vil <em>faktisk</em> hevde å ha verifisert.</li><li><strong><em>Ikke hallusinér.</em></strong> En prompt engineering-meme. Hallusinasjon er ikke en bryter modellen kan skru av.</li><li><strong>Å stole på agenten på <em>små</em> tall.</strong> Små tall er der den lyver mest selvsikkert. Det finnes ingen nedre grense for ærlighet.</li><li><strong>Å legge til flere regler i prompten for å <em>tvinge</em> ærlighet.</strong> Strukturelle fikser (del opp + shell) slår promptjusteringer hver gang. Hvis en regel må håndheves, kode den inn i verktøytilgang, ikke i norsk.</li></ul>
<p>Hvis strategien din for å fange opp hallusinasjoner er mer emfatisk formulering, har du ikke en strategi. Du har et håp.</p>
<h2>Den mentale modellen</h2>
<p>En agent er ikke en kollega. Det er en funksjon: <code>prompt → tokens</code>. Funksjonen er utmerket til å skrive kode og elendig til å introspektere om den gjorde det riktige. Behandle dens påstander om sitt eget arbeid som en hypotese. Diff-en, exit-koden, skjermbildet, radantallet — det er beviset. Sluttsummeringen er den mest løgnaktige overflaten i hele systemet.</p>
<p>Spesialisering er forsikringen din mot narrativ drift. Shell-en er din eneste ground truth. Builder skriver. Reviewer sjekker. Bash avgjør.</p>
<h2>Konklusjon</h2>
<p>Hvis du skal huske én ting: ikke la én enkelt agent både produsere og bedømme sitt eget resultat, og ikke la noen agent besvare et kvantitativt spørsmål uten å kjøre en kommando. Alt annet er nedstrøms av disse to reglene. Konfigurer verktøytilgang aggressivt, auditér verktøykall i stedet for oppsummeringer, og hallusinasjonspunkter krymper fra <em>overalt</em> til <em>noen få konkrete steder du allerede vet å sjekke</em>.</p>]]></content:encoded>
      <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>agents</category>
      <category>ai</category>
      <category>prompt-engineering</category>
      <category>automation</category>
    </item>
    <item>
      <title>Flere Claude Code-kontoer på én enhet</title>
      <link>https://oleksiimazurenko.dev/nb/blog/multiple-claude-accounts-one-device</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/multiple-claude-accounts-one-device</guid>
      <description>Hvordan bruke to (eller flere) Claude Code-kontoer parallelt — personlig og bedrift — med full isolasjon ved hjelp av én enkelt miljøvariabel.</description>
      <content:encoded><![CDATA[<p>Jeg bruker Claude Code daglig — for personlige prosjekter og for jobb. Problemet: det er to helt forskjellige kontoer med ulike OAuth-sesjoner, ulike CLAUDE.md-instruksjoner, ulike MCP-servere og separat prosjektminne. Slik kjører jeg dem parallelt på én enhet med ett enkelt shell-alias.</p>
<h2>Problemet</h2>
<p>Claude Code lagrer alt i <code>~/.claude</code> som standard — OAuth-token, samtalehistorikk, global CLAUDE.md, prosjektminne, MCP-serverkonfigurasjoner og innstillinger. Med to kontoer trenger du to helt adskilte verdener:</p>
<ul><li><strong>Personlig konto:</strong> eget Max/Pro-abonnement, personlig CLAUDE.md med dine preferanser, dine MCP-servere (Obsidian, personlige verktøy)</li><li><strong>Bedriftskonto:</strong> bedriftsplan, arbeids-CLAUDE.md med Jira/Slack-integrasjonsinstruksjoner, bedriftens MCP-servere</li><li><strong>Ulike OAuth-sesjoner:</strong> du kan ikke være logget inn på to kontoer i samme konfigurasjonskatalog</li><li><strong>Separat prosjektminne:</strong> du vil ikke at arbeidskontekst lekker inn i personlige sesjoner og vice versa</li></ul>
<p>Å logge ut og inn hver gang du bytter kontekst er ikke et alternativ. Du mister sesjonstilstanden, og det er rett og slett smertefullt.</p>
<h2>Løsningen: CLAUDE_CONFIG_DIR</h2>
<p>Claude Code respekterer én enkelt miljøvariabel: <code>CLAUDE_CONFIG_DIR</code>. Sett den til hvilken som helst bane, og Claude bruker den katalogen istedenfor <code>~/.claude</code> for alt — auth, historikk, innstillinger, minne. Hele oppsettet tar 60 sekunder.</p>
<h3>Steg 1: Opprett en andre konfigurasjonskatalog</h3>
<p>Velg et navn som passer ditt brukstilfelle:</p>
<pre><code>mkdir ~/.claude-work</code></pre>
<p>Ferdig. Claude fyller den med nødvendig struktur ved første oppstart.</p>
<h3>Steg 2: Autentiser den andre kontoen</h3>
<p>Kjør Claude én gang med den nye konfigurasjonskatalogen for å trigge OAuth-innlogging:</p>
<pre><code>CLAUDE_CONFIG_DIR=~/.claude-work claude</code></pre>
<p>Nettleseren åpnes. Logg inn med bedriftskontoen. OAuth-tokenet lagres i <code>~/.claude-work</code> — helt adskilt fra din personlige sesjon i <code>~/.claude</code>.</p>
<h3>Steg 3: Legg til et shell-alias</h3>
<p>Legg dette til i shell-konfigurasjonen din så du slipper å huske variabelen:</p>
<pre><code>alias claude-work=&apos;CLAUDE_CONFIG_DIR=~/.claude-work claude&apos;</code></pre>
<p>Last inn shell på nytt:</p>
<pre><code>source ~/.zshrc</code></pre>
<h2>Hva du får</h2>
<p>Nå har du to helt isolerte Claude-miljøer:</p>
<ul><li><strong><code>claude</code></strong> — starter med personlig konto, personlig CLAUDE.md, personlig minne</li><li><strong><code>claude-work</code></strong> — starter med bedriftskonto, arbeidsspesifikk CLAUDE.md, separat minne</li><li><strong>Isolert historikk:</strong> arbeidssamtaler forblir i arbeid, personlige forblir personlige</li><li><strong>Separate MCP-servere:</strong> din personlige Obsidian vault MCP vises ikke i arbeidssesjoner</li><li><strong>Uavhengige innstillinger:</strong> ulike tillatte verktøy, ulike tilgangsnivåer, ulike modellpreferanser per konto</li></ul>
<h2>Hvordan det fungerer under panseret</h2>
<p>Konfigurasjonskatalogen er den eneste sannhetskilden for Claude Codes tilstand. Her er hva som lever inni hver:</p>
<pre><code>~/.claude/              ← personal account
├── CLAUDE.md           ← personal global instructions
├── projects/           ← personal project memory
├── settings.json       ← personal permissions &amp; MCP servers
└── ...                 ← OAuth session, history, cache

~/.claude-work/         ← corporate account
├── CLAUDE.md           ← company-specific instructions (Jira, Slack, etc.)
├── projects/           ← separate project memory
├── settings.json       ← different MCP servers, permissions
└── ...                 ← separate OAuth session</code></pre>
<p>Når du kjører <code>claude-work</code>, leser Claude alt fra <code>~/.claude-work</code>. Den vet ikke at <code>~/.claude</code> eksisterer. De to instansene er helt uavhengige — du kan til og med kjøre dem samtidig i ulike terminalfaner.</p>
<h2>Skalering til N kontoer</h2>
<p>Mønsteret utvides til et vilkårlig antall kontoer. Frilanser med flere kunder? Legg til flere alias:</p>
<pre><code># Personal (default — no alias needed)
# Just run: claude

# Corporate
alias claude-work=&apos;CLAUDE_CONFIG_DIR=~/.claude-work claude&apos;

# Freelance client
alias claude-client=&apos;CLAUDE_CONFIG_DIR=~/.claude-client claude&apos;</code></pre>
<p>Hvert alias får sin egen konfigurasjonskatalog, sin egen OAuth-sesjon, sin egen CLAUDE.md med kundespesifikke instruksjoner.</p>
<h2>Praktiske tips</h2>
<ul><li><strong>Navngi kataloger tydelig:</strong> <code>~/.claude-work</code>, <code>~/.claude-clientname</code> — du takker deg selv når det er tre eller fire</li><li><strong>Skriv tilpasset CLAUDE.md for hver:</strong> arbeids-CLAUDE.md kan inkludere bedriftsspesifikke instruksjoner (Jira-tickets, Slack-kanaler, deployment-prosedyrer). Den personlige forblir slank.</li><li><strong>Ulike MCP-servere per konto:</strong> konfigurer arbeidsverktøy (Jira MCP, Slack MCP, interne API-er) kun i arbeidskonfigurasjonen. Hold din personlige ren.</li><li><strong>Sjekk hvilken konto som er aktiv:</strong> kjør <code>claude config list</code> i en sesjon om du er usikker — viser banen til konfigurasjonskatalogen</li></ul>
<h2>Der denne tilnærmingen ikke strekker til</h2>
<p><code>CLAUDE_CONFIG_DIR</code> isolerer per <em>konto</em>, ikke per <em>prosjekt</em>. Inne i én enkelt profil ser Claude hver MCP-server du noen gang har registrert for den kontoen — på tvers av alle prosjektene dine. For ren personlig bruk er det som regel greit. I det øyeblikket du har flere produksjonskritiske prosjekter under én konto, særlig i overlappende domener som fakturering, adminverktøy eller infrastruktur, introduserer det en konkret risiko mellom prosjekter: en KI-assistent kan kalle et verktøy fra prosjekt A mens den jobber med prosjekt B, spesielt når begge eksponerer likt navngitte operasjoner.</p>
<p>Profilmønsteret svarer på spørsmålet <em>which account am I in?</em>. Det svarer ikke på spørsmålet <em>which project's tools should be active right now?</em>. For arbeid med høyere innsats, stable et nytt isolasjonslag oppå konto-delingen:</p>
<ul><li><strong>Én profil per produksjonskritisk prosjekt, ikke bare per konto:</strong> istedenfor <code>~/.claude</code> og <code>~/.claude-work</code>, opprett <code>~/.claude-work-billing</code> og <code>~/.claude-work-admin</code>. Hver profil ser bare de MCP-serverne den faktisk trenger.</li><li><strong>Prosjekt-scope MCP via <code>.mcp.json</code>:</strong> commit en <code>.mcp.json</code> i prosjektroten som lister bare det prosjektets MCP-servere. Claude plukker dem opp når den startes fra den katalogen. Hold den globale konfigurasjonen minimal — bare universelle verktøy (notater, søk), ingen produksjonsendepunkter.</li><li><strong>Navngi MCP-servere utvetydig:</strong> unngå generiske navn som <code>admin</code>, <code>billing</code>, <code>mcp-server</code>. Prefiks med prosjektet: <code>acme_billing_prod</code>, <code>acme_admin_stage</code>. Et beskrivende navn tvinger fram en pause når noe er i ferd med å bli kalt fra feil kontekst.</li><li><strong>Gå gjennom hvert MCP-verktøyskall før godkjenning:</strong> kall som <code>*_create_*</code>, <code>*_delete_*</code>, <code>*_charge_*</code> fortjener et bevisst andre blikk. Hvilken hastighet du enn vinner på generell auto-godkjenning fordamper første gang et verktøy fra feil prosjekt avfyres mot produksjon.</li></ul>
<p>Den generelle regelen: del opp profiler aggressivt, hold produksjonsklar MCP unna default-profilen, og behandle overlapp i verktøysnavn mellom prosjekter som en smell verdt å refaktorere.</p>
<h2>Konklusjon</h2>
<p>Én miljøvariabel. Ett alias. Full isolasjon mellom kontoer. Ingen logout/login-dans, ingen konfigurasjonskonflikter, ingen kontekstlekkasje. Den typen løsning som er nesten skuffende enkel — men det er nettopp det som gjør den god. Sett opp én gang og tenk aldri på det igjen.</p>]]></content:encoded>
      <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>productivity</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Hvordan koble Claude Desktop til Obsidian — En reise gjennom 4 MCP-servere</title>
      <link>https://oleksiimazurenko.dev/nb/blog/claude-obsidian-mcp-servers</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/claude-obsidian-mcp-servers</guid>
      <description>En virkelig historie om å finne en stabil måte å automatisere refaktorering av Obsidian-vault via Claude. Hva som gikk i stykker, hva som fungerte, og hvorfor VaultForge viste seg å være det eneste fungerende alternativet.</description>
      <content:encoded><![CDATA[<p>Se for deg: du har 400+ notater i Obsidian, samlet opp gjennom år. Alt ligger spredt i vault-roten, konsepter blandet med tekniske notater, det finnes duplikater (<code>ideas.md</code> og en <code>ideas/</code>-mappe med 13 filer inni), ikke noe system. Du vil rydde opp — bygge en skikkelig mappearkitektur, legge til MOC-filer, organisere tagger. Å gjøre det manuelt er kjedelig og tregt. Den logiske tanken: <strong>koble Claude til Obsidian via MCP, la AI gjøre refaktoreringen</strong>. Det viser seg — det er en vei gjennom et minefelt. Her er hva jeg måtte gjennom for å nå en fungerende løsning.</p>
<h2>Hva er MCP og hvorfor det ikke er så enkelt</h2>
<p>MCP (Model Context Protocol) er en åpen protokoll fra Anthropic som lar Claude koble seg til eksterne verktøy og data. Prinsippet er enkelt: en lokal server kjører, eksponerer "verktøy" (tools), og Claude kaller dem under samtalen.</p>
<p>For Obsidian finnes det teoretisk mange MCP-servere. I praksis — hver har sine egne problemer.</p>
<p><strong>Hovedproblemet med Obsidian-økosystemet:</strong> Obsidian er en lukket applikasjon uten offisiell MCP. Fellesskapet fylte gapet, men hver implementasjon går sin egen vei, og ingen har "offisiell velsignelse".</p>
<h2>Forsøk 1: MarkusPfundstein/mcp-obsidian</h2>
<p>Første verktøyet som dukker opp ved søk. 3 400 stjerner på GitHub, med i alle tutorials. Virker som et trygt valg.</p>
<p><strong>Hvordan det fungerer:</strong> Python-server basert på Local REST API-pluginet i Obsidian. Serveren kommuniserer med pluginet via HTTPS, pluginet utfører operasjoner gjennom Obsidian API.</p>
<h3>Hva som gikk galt</h3>
<ul><li>Ikke oppdatert på 17 måneder</li><li>85 åpne issues</li><li><strong>Ingen <code>move</code>/<code>rename</code></strong> — bare read, write, append, delete</li><li>Local REST API har en dokumentert datatap-bug: POST-endpoint kan stille overskrive en fil ved append</li></ul>
<p>Ikke egnet for refaktorering — vi trenger å flytte filer og bevare lenker. Vi går videre.</p>
<h2>Forsøk 2: aaronsb/obsidian-mcp-plugin</h2>
<p>Fant et alternativ som fungerer som et <strong>nativt Obsidian-plugin</strong>. Det betyr direkte tilgang til Obsidians interne API — backlinks, Dataview, lenkegraf. Move via det native API-et oppdaterer alle wiki-lenker automatisk, fordi Obsidian håndterer dette selv.</p>
<h3>Installasjonsvanskeligheter</h3>
<ul><li>Pluginet er <strong>ikke i Obsidians offisielle katalog</strong> (PR venter med valideringsfeil)</li><li>Må installeres via <strong>BRAT</strong> (Beta Reviewers Auto-update Tool)</li><li>Claude Desktop aksepterer ikke Bearer token direkte via UI — tvang aktivering av HTTPS i pluginet</li><li>Self-signed sertifikat for localhost skaper tillitsproblemer</li></ul>
<p>Gjennom alle disse omveiene fikk jeg det endelig koblet til. Grunntest — <code>vault.move</code> skriver om <code>[[wikilinks]]</code>, fungerer som forventet.</p>
<h3>Hva som gikk galt i produksjon</h3>
<p>Da jeg begynte massrefaktorering (drag-and-drop av dusinvis av mapper i Obsidian + samtidige MCP-operasjoner), <strong>hang serveren i 4+ minutter</strong>. Hvorfor: pluginet kjører <em>inne i</em> Obsidian. Når Obsidian omindekserer tusenvis av filer etter en massiv strukturendring, blokkeres pluginet med det.</p>
<p>Konklusjon: <strong>avhengigheten av en åpen Obsidian-instans og dens indeks er fatal for masseoperasjoner</strong>.</p>
<h2>Forsøk 3: @bitbonsai/mcpvault</h2>
<p>Logisk — vi trenger en server som <strong>ikke er avhengig av Obsidian</strong>. Jobber direkte med filer på disk. <code>@bitbonsai/mcpvault</code> — anbefalt i mange anmeldelser. Direkte filsystemtilgang, enkel oppsett (<code>npx @bitbonsai/mcpvault@latest /path/to/vault</code>), 14 verktøy. Obsidian trenger ikke engang å være åpent.</p>
<p><strong>Før installasjon sjekket jeg én kritisk ting</strong> — om wiki-lenker oppdateres ved move. Fant en brukeranmeldelse:</p>
<blockquote>Filesystem-tilkoblingen vet ikke at den er i Obsidian — den ser en mappe med &lt;code&gt;.md&lt;/code&gt;-filer og det er alt. Vet ikke at filnavn bærer semantisk vekt, at hver &lt;code&gt;[[wikilink]]&lt;/code&gt; vil gå i stykker i det øyeblikket du gir nytt navn eller flytter. Auto-update links fungerer bare når navneendringen skjer innenfra appen. Jeg lærte dette etter å ha bedt Claude rydde opp i filnavn og kom tilbake til et dashboard med halvparten av lenkene ødelagt.</blockquote>
<p>Bekreftet i mcpvaults egen dokumentasjon: PR #101 (wiki link resolution) er <strong>under review, ikke merget</strong>. Så å flytte via <code>mcpvault</code> ville ødelegge halve vaultet. Ikke egnet.</p>
<h2>Forsøk 4: VaultForge (Final)</h2>
<p><code>blacksmithers/vaultforge</code> — spesielt bygget for AI-agenter som gjør refaktorering.</p>
<h3>Arkitektonisk korrekt</h3>
<ul><li><strong>Direkte filsystem</strong> — ikke avhengig av Obsidian</li><li><strong>Egen wikilink-motor</strong> — implementerer <code>[[wikilink]]</code>-oppløsningslogikk som oppdaterer alle former (stem, full sti, alias, embed)</li><li><strong>Dry run som standard</strong> på alle destruktive operasjoner — viser først hva som endres, så bekrefter du</li><li><strong>27 verktøy</strong> mot 8–14 hos konkurrenter: batch_rename, update_links, backlinks (impact analysis), prune_empty_dirs, frontmatter, smart_search (BM25), vault_themes (TF-IDF clustering)</li><li><strong>MIT-lisens</strong>, TypeScript, null underavhengigheter</li><li><strong>Installasjon på 30 sekunder</strong> via <code>.mcpb</code> (one-click-utvidelse for Claude Desktop)</li></ul>
<h3>Sikkerhetstest på isolerte filer</h3>
<p>Opprettet 4 testfiler med krysslenker — stem-lenker, lenker med alias, lenker med full sti. Flytter én fil til en undermappe:</p>
<pre><code>delta.md → subfolder/delta-renamed.md</code></pre>
<p>VaultForge viste en dry run: "1 fil vil bli omdøpt, 3 lenker vil bli oppdatert". Utført på ordentlig.</p>
<table><thead><tr><th>Link type</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Stem</td><td>[[delta]]</td><td>[[delta-renamed]]</td></tr><tr><td>Alias</td><td>[[delta|D]]</td><td>[[delta-renamed|D]]</td></tr><tr><td>Full path + alias</td><td>[[_vf-test/delta|D]]</td><td>[[_vf-test/subfolder/delta-renamed|D]]</td></tr></tbody></table>
<p>Sjekket etterpå — <strong>alle tre lenketyper ble oppdatert korrekt</strong>. Dette er nøyaktig det alle tidligere verktøy manglet.</p>
<h2>Hvordan installere VaultForge — Endelig instruksjon</h2>
<p>Hvis du har macOS og Claude Desktop:</p>
<h3>Steg 1</h3>
<p>Last ned <code>.mcpb</code>-filen:</p>
<pre><code>curl -fsSL https://github.com/blacksmithers/vaultforge/releases/latest/download/vaultforge.mcpb \
  -o /tmp/vaultforge.mcpb &amp;&amp; open /tmp/vaultforge.mcpb</code></pre>
<h3>Steg 2</h3>
<p>Claude Desktop åpner installasjonsdialogen for utvidelser. Skriv inn den <strong>absolutte stien</strong> til vaultet ditt — ingen backslashes, vanlige mellomrom:</p>
<pre><code>/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault</code></pre>
<h3>Steg 3</h3>
<p>Klikk Save. Claude Desktop legger til utvidelsen i konfigurasjonen automatisk. <strong>Ingen omstart nødvendig</strong> — <code>.mcpb</code>-utvidelser plukkes opp automatisk.</p>
<h3>Steg 4</h3>
<p>Verifiser: i en ny chat spør: <em>"What is the status of my Obsidian vault?"</em> — bør returnere noe som <code>totalFiles: 416, totalDirs: 135, ...</code></p>
<h2>Hva jeg lærte om Obsidians MCP-økosystem</h2>
<p><strong>For det første: "mest populær" betyr ikke "fungerer".</strong> MarkusPfundstein/mcp-obsidian har 3 400 stjerner og er standardanbefalingen, men det er utdatert og mangler viktige operasjoner.</p>
<p><strong>For det andre: et nativt plugin har en skjult kostnad.</strong> Aaronsb-pluginet så ideelt ut — graph, Dataview, nativt move. Men avhengigheten av en kjørende Obsidian-instans og dens indeks gjør det uegnet for seriøse masseoperasjoner.</p>
<p><strong>For det tredje: direkte filsystem uten link-engine er en felle.</strong> Mcpvault er raskt og enkelt, men "bare å flytte filer" ødelegger vault-strukturen. Lenker bærer <strong>påtvunget semantikk</strong> som filsystemet ikke vet om. Uten sin egen wikilink-logikkimplementasjon blir verktøyet en landmine.</p>
<p><strong>For det fjerde: test på isolerte data.</strong> Før du betror et verktøy massrefaktorering — lag en testmappe med 4–5 filer med krysslenker og se hva som skjer. 5 minutters testing sparer timer med gjenoppretting fra backup.</p>
<p><strong>For det femte: behold en git-backup av vaultet ditt.</strong> Det viktigste av alt. Et enkelt <code>git init</code> inne i vaultet og periodiske commits — det er forsikring mot alle feil fra en AI-agent eller et verktøy. Hvis noe går i stykker — <code>git reset --hard</code> bringer alt tilbake.</p>
<h2>Konklusjon</h2>
<p>Reisen tok flere timer og tre mislykkede forsøk. Den endelige arkitekturen ser slik ut:</p>
<ul><li><strong>VaultForge</strong> — hovedarbeidsverktøyet. Direkte filsystem + egen wikilink-motor + 27 verktøy = stabil refaktorering i enhver skala.</li><li><strong>Git</strong> — vault-versjonering. Gratis rollback for enhver feil.</li></ul>
<p>Nå kan jeg gjøre det alt dette ble startet for: be Claude organisere 400 notater i en ordentlig PARA-arkitektur, slå sammen duplikater, legge til frontmatter, bygge MOC-kart. Hver operasjon er trygg, lenker bevares, dry run viser hva som vil skje før noe endres.</p>
<p>Hvis du også ser på ditt rotete Obsidian og vil ha en AI-assistent — start rett med VaultForge. Ikke gjenta min rute gjennom døde prosjekter, beta-plugins og filsystemservere uten lenkelogikk.</p>]]></content:encoded>
      <pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate>
      <category>Obsidian</category>
      <category>Claude</category>
      <category>MCP</category>
      <category>Automation</category>
      <category>AI</category>
    </item>
    <item>
      <title>Svarte hull som rekursive universer: fra fysikk til eksistensens formål</title>
      <link>https://oleksiimazurenko.dev/nb/blog/black-holes-recursive-universes</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/black-holes-recursive-universes</guid>
      <description>Hva om hvert svart hull er et Big Bang for et nytt univers? En utforskning av rekursiv kosmologi og kognitiv lukking.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Hva om hvert svart hull er et Big Bang for et nytt univers? Denne artikkelen utforsker ideen om at universet vårt kan være en node i et uendelig rekursivt tre — der svarte hull føder sub-universer, energi sirkulerer tilbake via Hawking-stråling, og fysikkens grunnleggende lover bevisst er utformet for å gjøre kontakt mellom universer umulig.</p>
<h2>Svart hull = univers</h2>
<p>Ideen kom under et øyeblikk av ettertanke: et svart hull dannes når nok masse og trykk konsentreres i et enkelt punkt. Den singulariteten — uendelig tetthet, uendelig krumning — ser mistenkelig ut som forholdene vi beskriver for Big Bang.</p>
<p>Hva om dette er den samme hendelsen, sett fra forskjellige sider? Utenfra ser vi et svart hull som sluker materie. Innenfra — et nytt univers som eksploderer til eksistens. Massen og energien som kollapset inn i det svarte hullet blir råmaterialet for et helt nytt kosmos med sine egne stjerner, planeter og kanskje sine egne svarte hull.</p>
<blockquote>Hvert svart hull i universet vårt kan inneholde et univers. Og universet vårt kan eksistere inne i et svart hull i et foreldre-univers.</blockquote>
<h2>Hvorfor universer ikke kan kontakte hverandre</h2>
<p>Her er den elegante delen: når du først har krysset hendelseshorisonten, finnes det ingen vei tilbake. Generell relativitetsteori garanterer dette — foreldre-universets fremtid ligger helt utenfor hendelseshorisonten, utilgjengelig innenfra. Fra sub-universets perspektiv har foreldre-universet allerede tatt slutt. Hele dets tidslinje har allerede passert.</p>
<p>Dette er ikke en teknisk begrensning vi kan overvinne med bedre teknologi. Det er bygget inn i selve romtidens geometri. Universer er fundamentalt isolert fra hverandre — ikke av avstand, men av tidens struktur.</p>
<h2>Energisyklusen: å låne og tilbakebetale</h2>
<p>Men energi går ikke tapt. Hawking-stråling — den kvanteprosessen der svarte hull sakte fordamper — skaper et bemerkelsesverdig kretsløp:</p>
<ol><li>Et foreldre-univers skaper et svart hull og overfører energi til et sub-univers</li><li>Sub-universet lever gjennom hele sin livssyklus over billioner av år</li><li>Det svarte hullet fordamper sakte og returnerer energi til foreldre-universet via Hawking-stråling</li><li>Foreldre-universet mottar energien sin tilbake — med renter</li></ol>
<p>Den "renten" er fascinerende: fysikere tror nå at Hawking-stråling bevarer informasjon. Foreldre-universet får ikke bare tom energi tilbake — det får et avtrykk av alt som skjedde inni. Hver stjerne som ble dannet, hver planet, hvert øyeblikk av bevissthet — kodet i stråling.</p>
<h2>Rekursjon helt ned</h2>
<p>Hvis du er programmerer, er mønsteret umiskjennelig. Dette er rekursjon. Hvert univers kaller <code>universe()</code> med mindre energi og skaper sub-universer som skaper sub-sub-universer, helt til det ikke er nok energi til å danne svarte hull — grunntilfellet.</p>
<pre><code>universe(energy)
  ├── creates black holes
  │     ├── universe(energy - n)
  │     │     ├── universe(energy - n - m)
  │     │     │     └── base case: not enough energy for black holes
  │     │     └── returns energy via Hawking radiation
  │     └── returns energy via Hawking radiation
  └── receives all energy back</code></pre>
<p>Fysikeren Lee Smolin formaliserte en lignende idé som <strong>kosmologisk naturlig utvalg</strong>: universer "reproduserer seg" gjennom svarte hull, og hver generasjon har litt forskjellige fysiske konstanter — optimalisert gjennom utallige sykluser for å produsere flere svarte hull, flere universer.</p>
<h2>Hvor er vi i denne syklusen?</h2>
<p>Universet vårt er omtrent 13,8 milliarder år gammelt. Det høres urgammelt ut, men i sammenheng med hele dets levetid er vi vitne til den aller første begynnelsen:</p>
<table><thead><tr><th>Hendelse</th><th>Tidsskala</th></tr></thead><tbody><tr><td>Universets nåværende alder</td><td>~10¹⁰ years</td></tr><tr><td>Stjerner slutter å dannes</td><td>~10¹⁴ years</td></tr><tr><td>De svarte hullenes æra</td><td>~10⁴⁰ years</td></tr><tr><td>Siste svarte hull fordamper</td><td>~10¹⁰⁰ years</td></tr></tbody></table>
<p>Vi eksisterer ved omtrent 0,00000000...01% av universets totale levetid. Stjernenes æra — alt vi kan se — er et kort glimt helt i begynnelsen. Universets virkelige historie er den langsomme, tålmodige æraen der svarte hull skaper og fordamper sub-universer.</p>
<h2>Spørsmålet om høyere dimensjoner</h2>
<p>Alt som er diskutert så langt opererer innenfor vår tredimensjonale forståelse. Men hvis universet vårt er et "snitt" av noe høyere-dimensjonalt, kan hele det rekursive treet av svarte hull og sub-universer bare være en skygge av en struktur vi ikke kan oppfatte.</p>
<p>I 1884 skrev Edwin Abbott <em>Flatland</em> — en fortelling om todimensjonale vesener som ikke kan forestille seg en tredje dimensjon. En kule som passerer gjennom Flatland fremstår som en sirkel som vokser og krymper. "Flatlendingene" kan beskrive det matematisk, men aldri virkelig forstå hva de ser. Vi kan befinne oss i nøyaktig samme posisjon i forhold til vårt univers.</p>
<blockquote>Hva er bevissthet? Hvorfor eksisterer subjektiv opplevelse? David Chalmers kalte dette det &quot;vanskelige problemet&quot; — og det kan være det sterkeste beviset for at noe opererer utenfor vår dimensjonale rekkevidde.</blockquote>
<h2>Alt er låst på det fundamentale nivået</h2>
<p>Den mest slående erkjennelsen er ikke at vi ikke vet — men at vi <em>ikke kan</em> vite. Hver undersøkelsesretning treffer en fundamental barriere:</p>
<ul><li><strong>Vil du se foreldre-universet?</strong> Blokkert av hendelseshorisonten</li><li><strong>Vil du forstå bevisstheten?</strong> Blokkert — et system kan ikke fullt ut analysere seg selv (Gödels ufullstendighetsteoremer)</li><li><strong>Vil du vite hva som var "før"?</strong> Blokkert — tiden begynte med Big Bang</li><li><strong>Vil du oppfatte høyere dimensjoner?</strong> Blokkert av de kognitive begrensningene til et tredimensjonalt vesen</li></ul>
<p>Filosofen Colin McGinn kaller dette <strong>kognitiv lukking</strong>: noen spørsmål er lukket for det menneskelige sinnet, ikke på grunn av utilstrekkelige data, men på grunn av sinnets egen arkitektur. Forskjellen mellom "vi vet ikke ennå" og "vi kan ikke vite" er dyptgripende.</p>
<h2>Det eneste som gjenstår: selvforbedring</h2>
<p>Hvis hver utgang er blokkert med vilje — hvis du ikke kan se utover, ikke kan se bakover, ikke kan se oppover — finnes det bare en retning igjen: innover. Universet virker bevisst konstruert for å tvinge fokus på selvet.</p>
<p>Denne konklusjonen kommer ikke fra religion eller filosofilærebøker. Den kommer fra å følge logikken i svarte hull, rekursjon, informasjonsteori og kognisjonens grenser. Eksistensialister, buddhister, stoikere og fysikere — alle ankommer til det samme punktet via forskjellige veier: formålet med eksistens kan rett og slett være foredlingen av det vesenet som eksisterer.</p>
<blockquote>Vi kom hit ikke gjennom tro, men gjennom fysikk — fra svarte hull, gjennom rekursive universer, til kunnskapens fundamentale blokader, til den eneste åpne døren: å bli bedre.</blockquote>
<h2>Referanser</h2>
<ul><li><a href="https://en.wikipedia.org/wiki/Cosmological_natural_selection" target="_blank" rel="noopener">Lee Smolin — Kosmologisk naturlig utvalg</a></li><li><a href="https://en.wikipedia.org/wiki/Hawking_radiation" target="_blank" rel="noopener">Stephen Hawking — Hawking-stråling</a></li><li><a href="https://en.wikipedia.org/wiki/Hard_problem_of_consciousness" target="_blank" rel="noopener">David Chalmers — Bevissthetens vanskelige problem</a></li><li><a href="https://en.wikipedia.org/wiki/Flatland" target="_blank" rel="noopener">Edwin Abbott — Flatland: En roman i mange dimensjoner (1884)</a></li><li><a href="https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems" target="_blank" rel="noopener">Kurt Gödel — Ufullstendighetssetningene</a></li></ul>]]></content:encoded>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <category>physics</category>
      <category>philosophy</category>
      <category>consciousness</category>
      <category>black-holes</category>
      <category>cosmology</category>
    </item>
    <item>
      <title>AI drepte CMS — i hvert fall for enkle nettsider</title>
      <link>https://oleksiimazurenko.dev/nb/blog/ai-killed-cms-for-simple-sites</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/ai-killed-cms-for-simple-sites</guid>
      <description>Hvorfor tradisjonelle innholdshåndteringssystemer blir unødvendige for porteføljer, blogger og landingssider.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>For enkle nettsider — porteføljer, blogger, landingssider, småbedriftssider — er et tradisjonelt CMS i ferd med å bli unødvendig overhead. AI-verktøy som Claude Code, Cursor og GitHub Copilot kan nå redigere kodebasen din direkte, forstå kontekst, oversette innhold og deploye endringer via git. Abstraksjonslaget som CMS ga, erstattes av et smartere grensesnitt: naturlig språk.</p>
<h2>CMS-skatten du betaler</h2>
<p>Ethvert CMS kommer med en skjult kostnad. Ikke bare abonnementsavgiften — hele økosystemet av kompleksitet som pakkes rundt den ellers enkle nettsiden din:</p>
<ul><li><strong>Infrastruktur:</strong> En database å drifte, et API å vedlikeholde, et dashbord å sikre. WordPress alene utgjør ~43% av nettet og ~90% av CMS-rettede angrep.</li><li><strong>Ytelse:</strong> Dynamisk sidegenerering, API-kall på hver forespørsel, klientside-hydrering av CMS-data. Porteføljen din på 3 sider har nå arkitekturen til et SaaS-produkt.</li><li><strong>Leverandørinnlåsing:</strong> Innholdet ditt lever i noen andres databaseskjema. Migrere fra Contentful til Sanity? Det er et prosjekt, ikke en konfigurasjonsendring.</li><li><strong>Kontekstbytte:</strong> Rediger kode i IDE-en din, bytt så til et nettleserbasert CMS-dashbord for å endre en overskrift. To forskjellige mentale modeller for det som i bunn og grunn er den samme operasjonen.</li><li><strong>Kostnad:</strong> Headless CMS-priser skalerer ofte med API-kall eller innholdsposter. En personlig blogg trenger ikke en innholdsinfrastruktur til $99/måned.</li></ul>
<p>For en markedsføringsside der 50 personer redigerer innhold daglig, er denne kostnaden berettiget. For en utviklerportefølje eller en småbedrifts landingsside? Du bygger en bro over en sølepytt.</p>
<h2>Hva som endret seg: AI forstår koden din</h2>
<p>Grunnen til at CMS eksisterte var enkel: ikke-tekniske personer (og selv utviklere som ikke ville røre kode for innholdsendringer) trengte et visuelt grensesnitt for å oppdatere nettsider. Koden var for kompleks, for skjør, for lett å ødelegge.</p>
<p>AI endret denne ligningen fundamentalt. Moderne AI-kodeverktøy gjør ikke bare autofullføring — de forstår prosjektstruktur, leser eksisterende mønstre og gjør kontekstuelt korrekte endringer. Endringen i arbeidsflyten er dramatisk:</p>
<pre><code># Old workflow: CMS
1. Open browser → log into CMS dashboard
2. Navigate to content → find the right page
3. Edit in WYSIWYG editor → fight with formatting
4. Preview → looks different from production
5. Hit publish → pray the cache invalidates

# New workflow: AI
1. Tell AI: &quot;Change the pricing on the landing page to $29/month&quot;
2. AI edits the file, you review the diff
3. Push to git → deploy</code></pre>
<p>Dette er ikke hypotetisk. Denne bloggen kjører på SolidStart med innhold lagret som TypeScript-filer. Hver artikkel — inkludert denne — ble laget ved å fortelle AI hva den skal skrive, gjennomgå resultatet og pushe til git. Ikke noe CMS-dashbord. Ingen database. Ikke noe API-lag mellom innholdet og koden.</p>
<h2>Virkelige eksempler fra denne siden</h2>
<p>Denne nettsiden støtter 10 språk, har en blogg, genererer OG-bilder dynamisk og produserer RSS-feeder og sitemaps. Slik ser innholdslaget ut — ren TypeScript:</p>
<pre><code>// This blog post you&apos;re reading right now is a TypeScript file.
// No database. No API. No CMS dashboard.
// Just a typed object that AI can read and edit directly.

export const myPost: BlogPost = {
  slug: &quot;ai-killed-cms&quot;,
  date: &quot;2026-04-17&quot;,
  translations: {
    en: {
      title: &quot;Why I stopped using a CMS&quot;,
      description: &quot;AI understands my codebase better than any CMS UI.&quot;,
      content: makeContent(proseEn),
    },
    uk: { /* ... */ },
    de: { /* ... */ },
    // 10 languages — AI translates them all
  },
};</code></pre>
<p>Ting jeg gjør med AI som tradisjonelt ville krevd et CMS:</p>
<ul><li><strong>Legge til et nytt blogginnlegg:</strong> "Skriv en ny artikkel om X, følg samme struktur som eksisterende innlegg" — AI oppretter filen, legger til oversettelser, registrerer den i indeksen</li><li><strong>Oppdatere landingssidetekst:</strong> "Endre hero-overskriften til Y" — AI finner riktig fil og oppdaterer den</li><li><strong>Oversette innhold:</strong> "Legg til tysk oversettelse for prissiden" — AI leser den engelske versjonen og produserer en kulturelt tilpasset oversettelse, ikke en ord-for-ord-kopiering</li><li><strong>Rette en skrivefeil:</strong> "Det er en skrivefeil på about-siden, 'recieve' skal være 'receive'" — gjort på 3 sekunder, committet til git med en meningsfull melding</li></ul>
<h2>Hva CMS faktisk løste — og hvordan AI erstatter det</h2>
<p>La oss være ærlige om hva CMS brakte til bordet og hvordan hver evne kartlegges til AI-arbeidsflyten:</p>
<table><thead><tr><th>Problem</th><th>CMS-løsning</th><th>AI-løsning</th></tr></thead><tbody><tr><td>Ikke-teknisk redigering</td><td>WYSIWYG-redigerer</td><td>Instruksjoner på naturlig språk</td></tr><tr><td>Flerspråklig innhold</td><td>i18n-tillegg, locale-felt</td><td>AI oversetter med kulturell kontekst</td></tr><tr><td>Innholdsplanlegging</td><td>Innebygde publiseringsdatoer</td><td>Git-basert CI/CD med cron eller datofelt i koden</td></tr><tr><td>Versjonshistorikk</td><td>CMS revisjonssystem</td><td>Git — gullstandarden for versjonskontroll</td></tr><tr><td>Mediehåndtering</td><td>Innebygd ressursbibliotek</td><td>CDN + git LFS eller skylagring</td></tr></tbody></table>
<p>Den sentrale innsikten: git er allerede et bedre versjonskontrollsystem enn noe CMS noensinne har bygget. Og naturlig språk er et bedre grensesnitt enn noen WYSIWYG-redigerer — fordi det bærer intensjon, ikke bare formatering.</p>
<h2>Paradigmeskiftet: kode er innholdslaget</h2>
<p>Vi er vitne til en inversjon. I to tiår var trenden å skille innhold fra kode — legge innholdet i en database, eksponere det via API, rendere det i frontend. Det ga mening da kode var vanskelig å redigere og innhold måtte være tilgjengelig for ikke-utviklere.</p>
<blockquote>AI gjorde ikke CMS foreldet ved å være et bedre CMS. Det gjorde CMS foreldet ved å gjøre kode like tilgjengelig som et dashbord.</blockquote>
<p>Utviklingen av innholdshåndtering på nett følger en tydelig bane:</p>
<ol><li><strong>2000-tallet:</strong> Monolittiske CMS (WordPress, Drupal) — innhold og presentasjon koblet i ett system</li><li><strong>2010-tallet:</strong> Headless CMS (Contentful, Strapi) — innhold separert via API, rendret av frontend-rammeverk</li><li><strong>2020-tallet:</strong> Statiske nettstedsgeneratorer + Markdown (Hugo, Astro) — innhold som filer, bygget ved deploy</li><li><strong>2025+:</strong> Kode-som-innhold + AI — innholdet lever i typet kode, AI er redigeringsgrensesnittet</li></ol>
<h2>Når du fortsatt trenger et CMS</h2>
<p>Dette er ikke et "CMS er dødt"-standpunkt. CMS løser reelle problemer i stor skala. Du trenger fortsatt ett når:</p>
<ul><li><strong>Store redaksjonsteam:</strong> 10+ innholdsredaktører som trenger rollebasert tilgang, godkjenningsflyter og samtidig redigering. Git merge-konflikter er ikke en innholdsredaktørs problem å løse.</li><li><strong>Høyfrekvent innhold:</strong> Nyhetssider som publiserer 50+ artikler om dagen trenger optimaliserte redaksjonspipelines, ikke git-commits.</li><li><strong>Komplekse innholdsrelasjoner:</strong> E-handelskataloger med tusenvis av SKU-er, produktvarianter og dynamisk prising krever strukturerte databaser.</li><li><strong>Regulatorisk etterlevelse:</strong> Bransjer som krever revisjonsspor, innholdsgodkjenningskjeder og lovpålagte gjennomgangsprosesser trenger spesialbygde systemer.</li></ul>
<p>Grensen er tydelig: hvis innholdsendringene dine krever koordinering mellom flere ikke-tekniske interessenter med høy frekvens, fortjener CMS sin kompleksitet. Hvis du er en soloutvikler, et lite team, eller administrerer en side som endres ukentlig i stedet for hver time — er AI + kode enklere, raskere, billigere og mer pålitelig.</p>
<h2>Fremtiden: AI som det universelle grensesnittet</h2>
<p>Trenden strekker seg utover CMS. Hvert programvareabstraksjonslag som eksisterte fordi "det underliggende systemet er for komplekst for direkte interaksjon" komprimeres av AI. Admin-dashbord, konfigurasjon-UI, visuelle databaseredigerere — alle disse er grensesnitt som oversetter menneskelig intensjon til systemendringer. AI gjør denne oversettelsen nativt.</p>
<p>For enkle nettsider er fremtiden allerede her. Innholdet ditt er kode. Redigeringsverktøyet ditt er AI. Versjonskontrollen din er git. Deployen din er en push. Hele CMS-laget — dashbordet, databasen, API-et, hostingen — var mellomvare mellom intensjonen din og nettsiden din. AI fjernet behovet for den mellomvaren.</p>
<blockquote>Det beste CMS-et er intet CMS. Ikke fordi innholdshåndtering ikke betyr noe — men fordi AI gjorde selve koden til det mest intuitive grensesnittet for innholdshåndtering vi noensinne har hatt.</blockquote>]]></content:encoded>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <category>ai</category>
      <category>cms</category>
      <category>web-development</category>
      <category>opinion</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Hvordan koble Perplexity AI til Obsidian via MCP — Notater direkte fra chatten</title>
      <link>https://oleksiimazurenko.dev/nb/blog/perplexity-obsidian-mcp-integration</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/perplexity-obsidian-mcp-integration</guid>
      <description>Sett opp Perplexity Desktop til å lese og skrive til Obsidian-vaultet ditt med MCP filesystem-server. Søk på nettet og lagre i notatene dine i én samtale.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Perplexity Desktop støtter <strong>MCP (Model Context Protocol)</strong>-koblinger. Ved å legge til den offisielle <code>@modelcontextprotocol/server-filesystem</code>-serveren som peker på ditt Obsidian-vault, kan du be Perplexity på naturlig språk om å lese, opprette og redigere notater — direkte fra chatten. Ingen plugins, ingen utvidelser, ingen kopiering.</p>
<h2>Problemet</h2>
<p>Perplexity er utmerket til forskning — det søker på nettet, oppsummerer kilder og gir svar med sitater. Men når du vil lagre funnene i Obsidian-vaultet ditt, bryter arbeidsflyten sammen: kopier tekst, bytt til Obsidian, finn riktig notat, lim inn, formater. Hver. Eneste. Gang.</p>
<p>Nettleserutvidelser som "Perplexity to Obsidian" hjelper med eksport, men de er enveis — AI-en kan ikke <em>se</em> vaultet ditt, kan ikke lese eksisterende notater og kan ikke bestemme hvor ting skal plasseres basert på mappestrukturen din.</p>
<h2>Hva er MCP?</h2>
<p><strong>Model Context Protocol (MCP)</strong> er en åpen standard som lar AI-modeller samhandle med lokale verktøy og datakilder. Tenk på det som en USB-port for AI — du kobler til en "server" (et lite program), og AI-en får nye evner. I vårt tilfelle gir filesystem-serveren Perplexity 14 verktøy for å jobbe med filer:</p>
<pre><code>Perplexity Desktop (Mac App)
  │
  ├── MCP Connector: &quot;Obsidian Vault&quot;
  │     │
  │     └── @modelcontextprotocol/server-filesystem
  │           │
  │           ├── read_file()      → read any .md file
  │           ├── write_file()     → create or overwrite files
  │           ├── edit_file()      → patch existing files
  │           ├── list_directory() → browse vault structure
  │           ├── search_files()   → find files by pattern
  │           └── ... 14 tools total
  │
  └── Perplexity AI Model
        │
        └── &quot;Save this to my daily note&quot; → calls write_file()</code></pre>
<p>Nøkkelpunktet: AI-modellen har ikke direkte tilgang til filene dine. Den kaller verktøy levert av MCP-serveren, som kjører lokalt på maskinen din. Dataene dine forlater aldri datamaskinen din med mindre du eksplisitt ber AI-en om å gjøre noe med dem.</p>
<h2>Krav</h2>
<ul><li><strong>Perplexity Pro</strong>-abonnement (MCP-koblinger er tilgjengelige for betalende brukere)</li><li><strong>Perplexity Mac App</strong> fra App Store (ikke nettleserversjonen)</li><li><strong>Node.js</strong> installert på Mac-en (for at <code>npx</code> skal fungere)</li></ul>
<h2>Trinn-for-trinn-oppsett</h2>
<p>Hele oppsettet tar omtrent 2 minutter:</p>
<table><thead><tr><th>Step</th><th>Action</th></tr></thead><tbody><tr><td>1</td><td>Open &lt;strong&gt;Perplexity Mac App&lt;/strong&gt; (App Store version)</td></tr><tr><td>2</td><td>Click avatar → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Connectors&lt;/strong&gt;</td></tr><tr><td>3</td><td>Click &lt;strong&gt;Add Connector&lt;/strong&gt;</td></tr><tr><td>4</td><td>Set Server Name: &lt;code&gt;Obsidian Vault&lt;/code&gt;, Type: &lt;code&gt;Standard I/O&lt;/code&gt;</td></tr><tr><td>5</td><td>Paste the npx command with your vault path</td></tr><tr><td>6</td><td>Leave Environment Variables empty</td></tr><tr><td>7</td><td>Click &lt;strong&gt;Save&lt;/strong&gt; → confirm Security Warning</td></tr><tr><td>8</td><td>Verify status shows &lt;strong&gt;Running&lt;/strong&gt; with 14 tools</td></tr></tbody></table>
<h3>Kommandoen</h3>
<p>Kommandoen som skal limes inn i <strong>Command</strong>-feltet:</p>
<pre><code>npx -y @modelcontextprotocol/server-filesystem &quot;/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault&quot;</code></pre>
<p>Erstatt banen med den faktiske plasseringen til Obsidian-vaultet ditt. Hvis vaultet synkroniseres via iCloud, vil banen være under <code>~/Library/Mobile Documents/iCloud~md~obsidian/Documents/</code>. Sørg for å beholde anførselstegnene — banen inneholder sannsynligvis mellomrom.</p>
<h2>Hvordan bruke det</h2>
<p>Når koblingen viser <strong>Running</strong> med 14 tilgjengelige verktøy, gå til en hvilken som helst Perplexity-chat og begynn å snakke med vaultet ditt:</p>
<pre><code>&gt; Show me the structure of my Obsidian vault
&gt; Add a new reflection to daily notes/2026-04-12.md: &quot;Started using MCP today&quot;
&gt; Find all notes that mention &quot;meditation&quot;
&gt; Create a new note in concepts/ about quantum computing
&gt; List all files in my ideas/ folder</code></pre>
<p>AI-en forstår vault-strukturen din, respekterer formateringskonvensjonene dine og kan jobbe med eksisterende innhold. Du kan be den undersøke et emne på nettet og lagre sammendraget direkte i et spesifikt notat.</p>
<h2>Hvorfor MCP er bedre enn andre tilnærminger</h2>
<p>Før MCP var det begrensede måter å koble Perplexity og Obsidian på:</p>
<table><thead><tr><th>Feature</th><th>Copy-paste</th><th>Browser extension</th><th>MCP integration</th></tr></thead><tbody><tr><td>AI sees vault structure</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI reads existing notes</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI writes to vault</td><td>No</td><td>Export only</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI edits existing files</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>Works from chat</td><td>No</td><td>Partially</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>Setup complexity</td><td>None</td><td>Low</td><td>Medium (one-time)</td></tr></tbody></table>
<h2>Nåværende begrensninger</h2>
<ul><li><strong>Kun Mac</strong> — Perplexitys MCP-koblinger fungerer for øyeblikket bare i Mac App Store-versjonen</li><li><strong>Ingen Obsidian API-integrasjon</strong> — filesystem-serveren jobber med råfiler, ikke gjennom Obsidians API. Det betyr at den ikke utløser Obsidian-plugins (Linter, Templater) ved filoppretting</li><li><strong>Godkjenning påkrevd</strong> — sensitive filoperasjoner kan kreve din bekreftelse i Perplexity-appen — det er en sikkerhetsfunksjon, ikke en feil</li></ul>
<h2>Konklusjoner</h2>
<p>Dette oppsettet forvandler Perplexity fra et forskningsverktøy til et forsknings-og-fangstverktøy:</p>
<ol><li>Søk på nettet og lagre i Obsidian i én samtale</li><li>AI-en ser vault-strukturen din og tilpasser seg organisasjonssystemet ditt</li><li>Null appbytter — alt skjer i Perplexity-chatten</li></ol>
<h2>Kilder</h2>
<ul><li><a href="https://modelcontextprotocol.io" target="_blank" rel="noopener noreferrer">Model Context Protocol</a> — official MCP specification</li><li><a href="https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" target="_blank" rel="noopener noreferrer">MCP Filesystem Server</a> — the server used in this setup</li><li><a href="https://www.perplexity.ai/help-center/en/articles/11502712-local-and-remote-mcps-for-perplexity" target="_blank" rel="noopener noreferrer">Perplexity Help Center</a> — Local and Remote MCPs for Perplexity</li><li><a href="https://www.perplexity.ai/hub/blog/everything-is-computer" target="_blank" rel="noopener noreferrer">Perplexity Blog</a> — Everything is Computer</li></ul>]]></content:encoded>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <category>Obsidian</category>
      <category>Perplexity</category>
      <category>MCP</category>
      <category>AI</category>
      <category>Productivity</category>
    </item>
    <item>
      <title>Daglig AI-nyhetsdigest med Claude Code CLI og Obsidian — Null avhengigheter</title>
      <link>https://oleksiimazurenko.dev/nb/blog/ai-news-digest-claude-code-obsidian</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/ai-news-digest-claude-code-obsidian</guid>
      <description>Hvordan jeg bygde en daglig nyhetsforskningsagent med et 6-linjers bash-skript, Claude Code headless-modus og macOS launchd.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Et 6-linjers bash-skript som kjører <strong>Claude Code CLI</strong> i headless-modus hver morgen kl 9:00. Det søker nyheter på 11 konfigurerbare emner, filtrerer støy og skriver en formatert markdown-digest direkte til en <strong>Obsidian vault</strong> synkronisert via iCloud. Null avhengigheter. ~100 linjer konfigurasjon totalt.</p>
<h2>Problemet</h2>
<p>Som utvikler er det en daglig skatt å holde seg oppdatert. RSS-feeder er støyende, Twitter er en tidssluker, nyhetsbrev kommer når du er dypt fokusert. Jeg trengte noe som gjør research <em>for</em> meg.</p>
<p>Den typiske løsningen er å bygge en scraping-pipeline: planlegger, crawler, NLP-pipeline, database, varslingstjeneste. Det er ukers arbeid. Jeg ville ha noe som kunne bygges på en ettermiddag.</p>
<h2>Arkitektur</h2>
<p>Hele systemet er 4 filer og null avhengigheter:</p>
<pre><code>macOS launchd (9:00 AM daily)
  │
  └── digest.sh
        │
        └── claude -p &quot;$(cat prompt.md)&quot; --max-turns 20 --allowedTools Read,WebSearch,WebFetch,Write
              │
              ├── Reads topics.yaml (11 configurable topics)
              ├── WebSearch → finds news for each topic (last 24-48h)
              ├── WebFetch → reads full articles
              ├── Filters noise: old tutorials, promos, AI spam
              └── Write → saves digest to Obsidian Vault
                    │
                    └── ~/Obsidian Vault/digests/2026-04-12.md
                          │
                          └── iCloud sync → available on all devices</code></pre>
<h2>Koden (hele)</h2>
<p>Prosjektet er bevisst minimalt.</p>
<h3>Inngangspunkt: digest.sh</h3>
<p>Hele applikasjonen er et 6-linjers bash-skript:</p>
<pre><code>#!/bin/bash
DIGEST_DIR=&quot;$HOME/Developer/news-digest&quot;

claude -p &quot;$(cat &quot;$DIGEST_DIR/prompt.md&quot;)&quot; \
  --max-turns 20 \
  --allowedTools Read,WebSearch,WebFetch,Write</code></pre>
<p>Viktige flagg: <code>-p</code> kjører Claude i headless-modus, <code>--max-turns 20</code> gir agenten nok svinger, <code>--allowedTools</code> begrenser agenten til lesing, søk og skriving.</p>
<h3>Hjernen: prompt.md</h3>
<p>Her bor intelligensen. Prompten gjør Claude til en nyhetsforskningsagent:</p>
<pre><code># News Digest Agent

You are a news research agent. Your job is to find today&apos;s most important
and interesting news for a senior frontend developer.

## Instructions

1. Read the topics file at ~/Developer/news-digest/topics.yaml
2. For EACH topic, search the web for news from the last 24-48 hours
3. Filter: only include genuinely new and noteworthy items
4. Write the digest as a markdown file to Obsidian Vault digests/YYYY-MM-DD.md
5. IMPORTANT: Use the Write tool to save the file. Do NOT output to stdout.

## Rules

- Language: Ukrainian for summaries, English for titles and technical terms
- If there&apos;s no real news for a topic — SKIP IT ENTIRELY
- Prioritize: releases &gt; breaking changes &gt; security &gt; new patterns &gt; discussions
- Max 5 items per topic, sorted by importance
- Include direct links to sources
- Skip promotional content, generic tutorials, and AI-generated spam</code></pre>
<h3>Konfigurasjon: topics.yaml</h3>
<p>Emner er fullt konfigurerbare — legg til et nytt emne og det er i morgendagens digest:</p>
<pre><code>topics:
  - name: better-auth
    context: &quot;auth library for TypeScript. New releases, breaking changes&quot;

  - name: Next.js
    context: &quot;GitHub issues, releases, App Router, Turbopack, performance&quot;

  - name: SolidJS
    context: &quot;SolidStart, releases, ecosystem, comparison with React&quot;

  - name: Tailwind CSS
    context: &quot;v4 updates, new utilities, plugins&quot;

  - name: Claude AI
    context: &quot;Anthropic announcements, Claude Code, new models, MCP, API&quot;

  - name: GPT AI
    context: &quot;OpenAI announcements, new models, ChatGPT features&quot;

  - name: React
    context: &quot;React 19+, Server Components, new patterns, ecosystem&quot;

  - name: Apple
    context: &quot;Hardware, software, WWDC, developer tools, Apple Intelligence&quot;

  - name: Notable People
    context: &gt;
      Latest tweets from: Elon Musk, Dario Amodei, Sam Altman,
      Jensen Huang, Andrej Karpathy, Simon Willison, Swyx...

  - name: AI Global
    context: &quot;Major AI news, new models, regulations, open-source AI&quot;</code></pre>
<h2>Planlegging med launchd</h2>
<p>På macOS er <code>launchd</code> den native måten å planlegge gjentakende oppgaver:</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot;
  &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;key&gt;Label&lt;/key&gt;
    &lt;string&gt;com.news-digest&lt;/string&gt;

    &lt;key&gt;ProgramArguments&lt;/key&gt;
    &lt;array&gt;
        &lt;string&gt;/bin/bash&lt;/string&gt;
        &lt;string&gt;/Users/you/Developer/news-digest/digest.sh&lt;/string&gt;
    &lt;/array&gt;

    &lt;key&gt;StartCalendarInterval&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;Hour&lt;/key&gt;
        &lt;integer&gt;9&lt;/integer&gt;
        &lt;key&gt;Minute&lt;/key&gt;
        &lt;integer&gt;0&lt;/integer&gt;
    &lt;/dict&gt;

    &lt;key&gt;StandardOutPath&lt;/key&gt;
    &lt;string&gt;/Users/you/Developer/news-digest/logs/stdout.log&lt;/string&gt;
    &lt;key&gt;StandardErrorPath&lt;/key&gt;
    &lt;string&gt;/Users/you/Developer/news-digest/logs/stderr.log&lt;/string&gt;

    &lt;key&gt;EnvironmentVariables&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;PATH&lt;/key&gt;
        &lt;string&gt;/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin&lt;/string&gt;
        &lt;key&gt;HOME&lt;/key&gt;
        &lt;string&gt;/Users/you&lt;/string&gt;
    &lt;/dict&gt;
&lt;/dict&gt;
&lt;/plist&gt;</code></pre>
<p>Installer med <code>launchctl load ~/Library/LaunchAgents/com.news-digest.plist</code>. Skriptet kjører daglig kl 9:00.</p>
<h2>Hvordan resultatet ser ut</h2>
<p>Hver morgen dukker en ny markdown-fil opp i Obsidian-vaultet:</p>
<pre><code>---
date: 2026-04-12
---

# News Digest — 2026-04-12

## Next.js

### Next.js 16.1 Released with Improved Turbopack Caching
Нова версія Next.js 16.1 включає покращене кешування для Turbopack,
що зменшує час холодного старту на ~40%.
[Посилання](https://nextjs.org/blog/next-16-1)

### Critical Memory Leak Fix in App Router
Виправлено витік пам&apos;яті в App Router при частому перемиканні
між динамічними маршрутами.
[GitHub Issue](https://github.com/vercel/next.js/issues/...)

## Claude AI

### Claude Code 1.5 — MCP Server Auto-Discovery
Anthropic випустив оновлення Claude Code з автоматичним
виявленням MCP серверів у проєкті.
[Блог](https://www.anthropic.com/news/...)</code></pre>
<h2>I tall</h2>
<table><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody><tr><td>Total files in project</td><td>&lt;strong&gt;4&lt;/strong&gt; (digest.sh, prompt.md, topics.yaml, .gitignore)</td></tr><tr><td>Lines of code</td><td>&lt;strong&gt;~100&lt;/strong&gt;</td></tr><tr><td>External dependencies</td><td>&lt;strong&gt;0&lt;/strong&gt;</td></tr><tr><td>Setup time</td><td>&lt;strong&gt;~10 minutes&lt;/strong&gt;</td></tr><tr><td>Daily execution time</td><td>&lt;strong&gt;2-5 minutes&lt;/strong&gt;</td></tr><tr><td>Cost per run</td><td>&lt;strong&gt;~$0.10-0.30&lt;/strong&gt; (Claude API usage)</td></tr></tbody></table>
<h2>Viktige designvalg</h2>
<ul><li><strong>Claude Code CLI fremfor API</strong> — ingen håndtering av API-nøkler eller HTTP-klienter</li><li><strong>Obsidian fremfor e-post</strong> — digests er søkbare, lenkbare og permanente</li><li><strong>launchd fremfor cron</strong> — macOS-nativ planlegger med håndtering av tapte kjøringer</li><li><strong>YAML for emner</strong> — nytt emne = 2-linjers endring</li><li><strong>Hoppe over tomme emner</strong> — ingen nyheter = ingen seksjon</li></ul>
<h2>Bygg din egen</h2>
<p>Klart på 10 minutter:</p>
<ol><li>Installer <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer">Claude Code CLI</a> og autentiser</li><li>Klon repoet: <code>git clone https://github.com/oleksiimazurenko/news-digest</code></li><li>Rediger <code>topics.yaml</code> og <code>prompt.md</code></li><li>Rediger plist-filen og <code>launchctl load</code></li><li>Vent til 9:00 — eller test manuelt med <code>bash digest.sh</code></li></ol>
<h2>Konklusjoner</h2>
<p>Det mest interessante med dette prosjektet er hva som <em>ikke</em> er der. Ingen database, ingen API-server, ingen Docker, ingen npm-pakker, ingen Python, ingen HTML-parser, ingen NLP-pipeline.</p>
<p>Slik ser det ut å bygge med AI-agenter: du definerer <em>hva</em> og <em>hvor</em>, agenten håndterer <em>hvordan</em>. Total utviklingstid: cirka 2 timer.</p>
<h2>Kilder</h2>
<ul><li><a href="https://github.com/oleksiimazurenko/news-digest" target="_blank" rel="noopener noreferrer">news-digest on GitHub</a> — full source code</li><li><a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer">Claude Code Documentation</a> — headless mode and CLI flags</li><li><a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" target="_blank" rel="noopener noreferrer">Apple Developer</a> — creating launchd jobs</li><li><a href="https://obsidian.md" target="_blank" rel="noopener noreferrer">Obsidian</a> — markdown knowledge base</li></ul>]]></content:encoded>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <category>Claude Code</category>
      <category>AI</category>
      <category>Obsidian</category>
      <category>Automation</category>
      <category>Productivity</category>
    </item>
    <item>
      <title>Next.js Dark Mode uten blinking eller React 19-advarsler</title>
      <link>https://oleksiimazurenko.dev/nb/blog/nextjs-dark-mode-without-flash</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/nextjs-dark-mode-without-flash</guid>
      <description>Hvordan erstatte next-themes med Zustand + useServerInsertedHTML for flimmerfri mørk modus i Next.js 15+.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p><code>next-themes</code> renderer en <code>&lt;script&gt;</code>-tagg inne i en React Client Component for å forhindre temaflimmer (FOUC). React 19 advarer mot dette. Biblioteket er ikke oppdatert siden mars 2025. Løsning: Zustand-store + <code>useServerInsertedHTML</code>. Null avhengigheter, null FOUC, null advarsler.</p>
<h2>Problemet</h2>
<p>Hvis du bruker <code>next-themes</code> med Next.js 15+ og React 19, får du denne konsollfeilen ved hver sidelasting:</p>
<pre><code>Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.
Consider using template tag instead.

src/providers/theme-provider.tsx (7:10) @ ThemeProvider</code></pre>
<p>Ingen hydreringsfeil. React 19 advarer om at <code>&lt;script&gt;</code>-tagger i klientkomponenter <strong>aldri vil kjøre</strong>. Skriptet fungerer i SSR, men React flagger det.</p>
<h2>Hvorfor det skjer</h2>
<p><code>next-themes</code> setter temaklasse før hydrering ved å injisere et inline-skript via <code>React.createElement</code>:</p>
<pre><code>// next-themes renders a &lt;script&gt; inside a Client Component
return React.createElement(Provider, { value },
  React.createElement(&quot;script&quot;, {
    suppressHydrationWarning: true,
    dangerouslySetInnerHTML: { __html: `(...theme init code...)` }
  }),
  children
)</code></pre>
<p>React 19 endret oppførselen — script-tagger i komponenter flagges nå. <code>suppressHydrationWarning</code> hjelper ikke for dette.</p>
<h2>Hva vi prøvde (og hvorfor det mislyktes)</h2>
<p>Vi prøvde systematisk hver tilnærming:</p>
<table><thead><tr><th>Attempt</th><th>Result</th></tr></thead><tbody><tr><td>&lt;code&gt;suppressHydrationWarning&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;</td><td>Suppresses hydration mismatch, but NOT the script tag warning</td></tr><tr><td>Delay mount with &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt;</td><td>Warning disappears, but causes FOUC (theme flash)</td></tr><tr><td>Raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in layout &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;</td><td>React 19 catches it — same warning even in Server Components</td></tr><tr><td>&lt;code&gt;next/script&lt;/code&gt; with &lt;code&gt;beforeInteractive&lt;/code&gt;</td><td>Still rendered inside React tree — same warning</td></tr><tr><td>Remove &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, put Script in body</td><td>Same warning — it&apos;s inside &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; which is React-managed</td></tr><tr><td>&lt;code&gt;next-themes@1.0.0-beta.0&lt;/code&gt;</td><td>Beta, no release date, unclear if fixed</td></tr></tbody></table>
<h2>Løsningen: Zustand + useServerInsertedHTML</h2>
<p><code>useServerInsertedHTML</code> injiserer HTML i SSR-strømmen <strong>utenfor React-treet</strong>. Skriptet er i HTML, men React "ser" det ikke på klienten. Kombinert med Zustand for reaktiv tematilstand — komplett erstatning, null avhengigheter.</p>
<h3>Hvordan det fungerer</h3>
<pre><code>SSR (server):
  useServerInsertedHTML → injects &lt;script&gt; into HTML stream
  ↓
  Browser receives HTML with theme script already in it
  ↓
  Script runs BEFORE React hydrates → correct class on &lt;html&gt;
  ↓
  No flash. No mismatch. No warning.

Client (after hydration):
  useEffect → _init() → Zustand store syncs with localStorage
  ↓
  useTheme() → reactive theme state for components</code></pre>
<h3>Steg 1: Zustand-store</h3>
<p>Storen håndterer tematilstand, DOM-klasser, systemdetektering og synkronisering mellom faner. <code>_init()</code> returnerer en oppryddingsfunksjon for <code>useEffect</code>:</p>
<pre><code>import { create } from &quot;zustand&quot;;

const STORAGE_KEY = &quot;theme&quot;;
const MEDIA_QUERY = &quot;(prefers-color-scheme: dark)&quot;;

function getSystemTheme(): &quot;light&quot; | &quot;dark&quot; {
  return window.matchMedia(MEDIA_QUERY).matches ? &quot;dark&quot; : &quot;light&quot;;
}

function applyTheme(resolved: string, disableTransition: boolean) {
  const d = document.documentElement;

  if (disableTransition) {
    const style = document.createElement(&quot;style&quot;);
    style.appendChild(
      document.createTextNode(
        &quot;*,*::before,*::after{transition:none!important}&quot;
      )
    );
    document.head.appendChild(style);
    window.getComputedStyle(document.body);
    setTimeout(() =&gt; document.head.removeChild(style), 1);
  }

  d.classList.remove(&quot;light&quot;, &quot;dark&quot;);
  d.classList.add(resolved);
  d.style.colorScheme = resolved;
}

interface ThemeState {
  theme: string;
  systemTheme: &quot;light&quot; | &quot;dark&quot;;
  resolvedTheme: string;
  setTheme: (theme: string) =&gt; void;
  _init: (disableTransition: boolean) =&gt; () =&gt; void;
}

export const useThemeStore = create&lt;ThemeState&gt;((set, get) =&gt; ({
  theme: &quot;system&quot;,
  systemTheme: &quot;light&quot;,
  resolvedTheme: &quot;light&quot;,

  setTheme: (newTheme) =&gt; {
    const { systemTheme, disableTransitionOnChange } = get();
    const resolved = newTheme === &quot;system&quot; ? systemTheme : newTheme;
    localStorage.setItem(STORAGE_KEY, newTheme);
    applyTheme(resolved, disableTransitionOnChange);
    set({ theme: newTheme, resolvedTheme: resolved });
  },

  _init: (disableTransition) =&gt; {
    const stored = localStorage.getItem(STORAGE_KEY) || &quot;system&quot;;
    const system = getSystemTheme();
    const resolved = stored === &quot;system&quot; ? system : stored;

    set({ theme: stored, systemTheme: system, resolvedTheme: resolved });
    applyTheme(resolved, disableTransition);

    // System theme changes
    const mq = window.matchMedia(MEDIA_QUERY);
    const onChange = (e: MediaQueryListEvent) =&gt; {
      const newSystem = e.matches ? &quot;dark&quot; : &quot;light&quot;;
      const { theme } = get();
      set({ systemTheme: newSystem,
            resolvedTheme: theme === &quot;system&quot; ? newSystem : theme });
      if (theme === &quot;system&quot;) applyTheme(newSystem, disableTransition);
    };
    mq.addEventListener(&quot;change&quot;, onChange);

    // Cross-tab sync
    const onStorage = (e: StorageEvent) =&gt; {
      if (e.key !== STORAGE_KEY || !e.newValue) return;
      const system = getSystemTheme();
      const resolved = e.newValue === &quot;system&quot; ? system : e.newValue;
      set({ theme: e.newValue, resolvedTheme: resolved });
      applyTheme(resolved, disableTransition);
    };
    window.addEventListener(&quot;storage&quot;, onStorage);

    return () =&gt; {
      mq.removeEventListener(&quot;change&quot;, onChange);
      window.removeEventListener(&quot;storage&quot;, onStorage);
    };
  },
}));

export function useTheme() {
  return useThemeStore((s) =&gt; ({
    theme: s.theme,
    setTheme: s.setTheme,
    resolvedTheme: s.resolvedTheme,
    systemTheme: s.systemTheme,
  }));
}</code></pre>
<h3>Steg 2: ThemeProvider</h3>
<p>Provideren injiserer FOUC-forebyggende skript via <code>useServerInsertedHTML</code> og initialiserer Zustand-storen ved montering:</p>
<pre><code>&quot;use client&quot;

import { useEffect } from &quot;react&quot;
import { useServerInsertedHTML } from &quot;next/navigation&quot;
import { useThemeStore } from &quot;@/store/use-theme-store&quot;

const THEME_INIT_SCRIPT = `(function(){
  try {
    var t = localStorage.getItem(&quot;theme&quot;) || &quot;system&quot;;
    var r = t;
    if (t === &quot;system&quot;) {
      r = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches
        ? &quot;dark&quot; : &quot;light&quot;;
    }
    document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
    document.documentElement.classList.add(r);
    document.documentElement.style.colorScheme = r;
  } catch(e) {}
})()`

export function ThemeProvider({
  children,
  disableTransitionOnChange = false,
}) {
  // Injects script into SSR HTML outside the React tree
  // — prevents FOUC without triggering React 19 warning
  useServerInsertedHTML(() =&gt; (
    &lt;script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} /&gt;
  ))

  useEffect(() =&gt; {
    return useThemeStore.getState()._init(disableTransitionOnChange)
  }, [disableTransitionOnChange])

  return &lt;&gt;{children}&lt;/&gt;
}</code></pre>
<h3>Steg 3: Layout</h3>
<pre><code>import { ThemeProvider } from &quot;@/providers/theme-provider&quot;

export default function RootLayout({ children }) {
  return (
    &lt;html lang=&quot;en&quot; suppressHydrationWarning&gt;
      &lt;body&gt;
        &lt;ThemeProvider disableTransitionOnChange&gt;
          {children}
        &lt;/ThemeProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre>
<h3>Steg 4: Bruk det</h3>
<pre><code>import { useTheme } from &quot;@/store/use-theme-store&quot;

export function ThemeSwitch() {
  const { theme, setTheme } = useTheme();

  return (
    &lt;button onClick={() =&gt; setTheme(
      theme === &quot;dark&quot; ? &quot;light&quot; : &quot;dark&quot;
    )}&gt;
      {theme === &quot;dark&quot; ? &quot;☀️&quot; : &quot;🌙&quot;}
    &lt;/button&gt;
  );
}</code></pre>
<h2>Migrering fra next-themes</h2>
<p>API-et er bevisst identisk. Migrering er én enkelt importendring per fil:</p>
<pre><code>- import { useTheme } from &quot;next-themes&quot;
+ import { useTheme } from &quot;@/store/use-theme-store&quot;

  // API is identical — no other changes needed
  const { theme, setTheme, resolvedTheme, systemTheme } = useTheme()</code></pre>
<h2>Sammenligning</h2>
<table><thead><tr><th></th><th>next-themes</th><th>Zustand + useServerInsertedHTML</th></tr></thead><tbody><tr><td>React 19 warning</td><td>Yes — &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in client component</td><td>No</td></tr><tr><td>FOUC prevention</td><td>Yes</td><td>Yes</td></tr><tr><td>Cross-tab sync</td><td>Yes</td><td>Yes</td></tr><tr><td>System theme detection</td><td>Yes</td><td>Yes</td></tr><tr><td>&lt;code&gt;disableTransitionOnChange&lt;/code&gt;</td><td>Yes</td><td>Yes</td></tr><tr><td>Bundle size</td><td>~3.5 KB</td><td>~1.5 KB (uses existing Zustand)</td></tr><tr><td>Dependencies</td><td>+1 (next-themes)</td><td>0 (Zustand already in project)</td></tr><tr><td>Maintenance risk</td><td>Abandoned since March 2025</td><td>Your own code</td></tr></tbody></table>
<h2>Hvorfor ikke andre alternativer?</h2>
<h3>@wrksz/themes</h3>
<p>Drop-in-erstatning som bruker <code>useServerInsertedHTML</code>. Fungerer, men nok en avhengighet med én vedlikeholder. Om <code>next-themes</code> lærte oss noe — avhengigheter blir forlatt. Med ~100 linjer kode eier du hele løsningen.</p>
<h3>next-themes@1.0.0-beta.0</h3>
<p>Finnes på npm, ingen releasedato, ingen endringslogg, uklart om React 19-advarselen er fikset. Ikke verdt å satse produksjonskode på.</p>
<h3>Kun CSS (prefers-color-scheme)</h3>
<p>Fungerer for systemdetektering, men kan ikke håndtere localStorage-persistens, manuell bytting eller "system"-alternativet. Trenger JavaScript.</p>
<h2>Konklusjoner</h2>
<ol><li><code>next-themes</code> er i praksis forlatt — siste utgivelse mars 2025, React 19-advarsel ufikset</li><li><code>useServerInsertedHTML</code> er riktig Next.js-primitiv for skriptinjeksjon uten React-advarsler</li><li>Zustand gir reaktiv tematilstand med mindre kode enn en Context-provider</li><li>Hele løsningen ~100 linjer, null nye avhengigheter, du eier hver linje</li></ol>
<h2>Kilder</h2>
<ul><li><a href="https://github.com/shadcn-ui/ui/issues/10104" target="_blank" rel="noopener noreferrer">shadcn/ui #10104</a> — Dark mode guide should address React 19 script warning</li><li><a href="https://github.com/pacocoursey/next-themes/issues/296" target="_blank" rel="noopener noreferrer">next-themes #296</a> — React 19 support request (open since 2024)</li><li><a href="https://github.com/facebook/react/issues/34008" target="_blank" rel="noopener noreferrer">React #34008</a> — Script tags not executing in components (by design)</li><li><a href="https://dev.to/wrksz/i-was-tired-of-next-themes-being-abandoned-so-i-built-a-drop-in-replacement-4dbk" target="_blank" rel="noopener noreferrer">@wrksz/themes</a> — Drop-in replacement using useServerInsertedHTML</li><li><a href="https://nextjs.org/docs/app/building-your-application/styling/css#server-inserted-html" target="_blank" rel="noopener noreferrer">Next.js Docs</a> — useServerInsertedHTML API</li></ul>]]></content:encoded>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <category>next.js</category>
      <category>react-19</category>
      <category>dark-mode</category>
      <category>zustand</category>
      <category>performance</category>
    </item>
    <item>
      <title>Hvordan jeg eliminerte 94 render-blokkerende CSS-filer i Next.js 16 med en dårlig dokumentert Turbopack-funksjon</title>
      <link>https://oleksiimazurenko.dev/nb/blog/eliminating-render-blocking-css</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/eliminating-render-blocking-css</guid>
      <description>Etter dager med å prøve hver tilnærming — fra experimental.inlineCss til MutationObserver-hacks — oppdaget jeg Turbopack Import Attributes som løser problemet med render-blokkerende CSS i Next.js App Router.</description>
      <content:encoded><![CDATA[<h2>Problemet</h2>
<p>Appen vår (<a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a>) bruker Next.js 16 med en <strong>Landing Builder</strong> — et CMS-drevet system som setter sammen markedsføringssider fra ~90 forskjellige seksjonskomponenter. Arkitekturen bruker en <code>sectionRegistry.tsx</code> som mapper seksjonsnavn til <code>next/dynamic()</code>-kall:</p>
<pre><code>// sectionRegistry.tsx — imports all 90 sections
export const sectionRegistry = {
  HeroSection: dynamic(() =&gt; import(&apos;./HeroSection/HeroSection&apos;)),
  FAQSection: dynamic(() =&gt; import(&apos;./FAQSection/FAQSection&apos;)),
  ReviewsSection: dynamic(() =&gt; import(&apos;./ReviewsSection/ReviewsSection&apos;)),
  // ... 87 more
}</code></pre>
<p>En enkelt landingsside rendrer bare <strong>5-8 seksjoner</strong>. Men Lighthouse viste:</p>
<pre><code>Eliminate render-blocking resources
  94 CSS resources (~330 KB)
  Potential savings: 5,440 ms</code></pre>
<p><strong>Hvorfor?</strong> Turbopack ser alle 90 <code>import()</code>-stier som oppnåelige og genererer <code>&lt;link rel="stylesheet"&gt;</code> for <strong>hver</strong> SCSS-modul. Selv seksjoner som aldri rendres på siden får sin CSS injisert i <code>&lt;head&gt;</code>. Dette er en <a href="https://github.com/vercel/next.js/issues/62485" target="_blank" rel="noopener noreferrer">bekreftet, forventet oppførsel</a> i Next.js App Router. <strong>Ingen fix er planlagt.</strong></p>
<h2>Alt jeg prøvde (og hvorfor det feilet)</h2>
<p>Jeg brukte dager på å gå gjennom hver tilnærming jeg kunne finne. Her er den fullstendige listen:</p>
<table><thead><tr><th>Approach</th><th>Why it doesn&apos;t work</th></tr></thead><tbody><tr><td>Split &lt;code&gt;sectionRegistry&lt;/code&gt; into per-section files</td><td>CSS still loaded — &lt;a href=&quot;https://github.com/vercel/next.js/issues/61066&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#61066&lt;/a&gt;</td></tr><tr><td>&lt;code&gt;experimental.inlineCss: true&lt;/code&gt;</td><td>Inlines CSS on &lt;strong&gt;every SSR request&lt;/strong&gt; — crashed our CMS under load</td></tr><tr><td>&lt;code&gt;experimental.optimizeCss&lt;/code&gt; (Critters)</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/62485&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Incompatible with streaming&lt;/a&gt; in App Router</td></tr><tr><td>CSS-in-JS rewrite</td><td>Rewriting 90 sections from SCSS to CSS-in-JS is not realistic</td></tr><tr><td>&lt;code&gt;media=&quot;print&quot;&lt;/code&gt; hack</td><td>Doesn&apos;t work with CSS Modules in Turbopack — &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags are managed by the framework</td></tr><tr><td>Switch back to Webpack</td><td>Has &lt;code&gt;experiments.css&lt;/code&gt; options, but we&apos;re committed to Turbopack</td></tr><tr><td>Turbopack plugins</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/discussions/85465&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Don&apos;t exist&lt;/a&gt; — no plugin API</td></tr><tr><td>Turbopack CSS loaders</td><td>Not supported — only JS output</td></tr><tr><td>SWC plugins for CSS</td><td>SWC only processes JavaScript</td></tr><tr><td>Client Component wrapper for &lt;code&gt;dynamic()&lt;/code&gt;</td><td>Registry is a global constant — bundler sees all dependencies</td></tr><tr><td>Next.js middleware HTML rewrite</td><td>&lt;a href=&quot;https://github.com/orgs/vercel/discussions/3874&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Middleware can&apos;t modify response body&lt;/a&gt;</td></tr><tr><td>Suspense + streaming with async import</td><td>React Float always pulls CSS into initial &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;</td></tr><tr><td>Suspense + 3s delay</td><td>Content streams later, but CSS is in the initial &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; chunk</td></tr><tr><td>&lt;code&gt;experimental.cssChunking&lt;/code&gt;</td><td>Already default. Merges chunks but doesn&apos;t remove irrelevant CSS</td></tr><tr><td>Post-build Beasties/Critters</td><td>Only for static export, &lt;a href=&quot;https://github.com/vercel/next.js/discussions/59989&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;not with ISR&lt;/a&gt;</td></tr><tr><td>npm libraries (next-critical, fg-loadcss)</td><td>Pages Router only or abandoned (6+ years)</td></tr><tr><td>&lt;code&gt;inlineCss&lt;/code&gt; per-route exclusion</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/pull/72195&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Not supported&lt;/a&gt; — all-or-nothing global flag</td></tr><tr><td>Turbopack &lt;code&gt;as: &apos;raw&apos;&lt;/code&gt; for SCSS</td><td>Returns &lt;code&gt;undefined&lt;/code&gt; instead of text</td></tr><tr><td>Turbopack rule for &lt;code&gt;*.inline.module.scss&lt;/code&gt;</td><td>Turbopack intercepts &lt;code&gt;.module.scss&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; custom rules</td></tr><tr><td>Turbopack rule for &lt;code&gt;*.inline.scss&lt;/code&gt;</td><td>Turbopack intercepts &lt;code&gt;.scss&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; custom rules</td></tr><tr><td>MutationObserver &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;</td><td>Tested in dev, risky — CSS still loads as separate files, possible FOUC</td></tr></tbody></table>
<h3>inlineCss-fellen</h3>
<p>Next.js har et <code>experimental.inlineCss</code>-flagg som erstatter alle <code>&lt;link rel="stylesheet"&gt;</code> med inline <code>&lt;style&gt;</code>-tagger. Høres perfekt ut, ikke sant?</p>
<p>Problemet: det er <strong>alt eller ingenting</strong>. Du kan ikke aktivere det per rute. Hvis du har SSR-sider (<code>force-dynamic</code>), bygger hver forespørsel om all CSS inline. Vi prøvde — vår headless CMS tålte ikke lasten.</p>
<h2>Oppdagelsen: Turbopack Import Attributes</h2>
<p>Ved å grave i <a href="https://nextjs.org/blog/next-16-2-turbopack#inline-loader-configuration" target="_blank" rel="noopener noreferrer">Next.js 16.2-utgivelsesnotater</a> fant jeg en dårlig dokumentert funksjon: <strong>Turbopack Import Attributes</strong>. Den lar deg overstyre den innebygde bundler-pipelinen for en spesifikk import med TC39 <code>with {}</code>-syntaks:</p>
<pre><code>import { cssText, styles } from &apos;./hero.module.scss&apos; with {
  turbopackLoader: &apos;@promova/scss-to-inline-loader&apos;,
  turbopackAs: &apos;*.js&apos;
}</code></pre>
<p>Dette forteller Turbopack: <em>"Ikke behandle denne importen som et stilark. Kjør den gjennom min egendefinerte loader og behandle utdataen som JavaScript."</em></p>
<p><strong>Dette er den avgjørende innsikten.</strong> I stedet for at Turbopack genererer et <code>&lt;link rel="stylesheet"&gt;</code> som blokkerer rendering, kompilerer vår loader SCSS og eksporterer det som en JS-streng. Resultat: <strong>bare CSS for seksjoner som faktisk rendres havner i sidens HTML</strong>.</p>
<h2>Løsningen</h2>
<h3>1. Egendefinert Turbopack Loader</h3>
<p>Et ~70-linjers Node.js-skript som yarn workspace-pakke (<code>@promova/scss-to-inline-loader</code>):</p>
<pre><code>const { createHash } = require(&apos;node:crypto&apos;)
const sass = require(&apos;sass&apos;)
const postcss = require(&apos;postcss&apos;)
const postcssModules = require(&apos;postcss-modules&apos;)
const path = require(&apos;path&apos;)

const SHARED_STYLES = path.resolve(__dirname, &apos;../shared/styles&apos;)

// Same prependData as sassOptions.prependData in next.config.ts
const PREPEND_DATA = `
@use &quot;sass:math&quot; as math;
@use &quot;sass:list&quot; as list;
@use &quot;${SHARED_STYLES}/_variables.scss&quot; as *;
@use &quot;${SHARED_STYLES}/_breakpoints.scss&quot; as *;
@use &quot;${SHARED_STYLES}/_mixins.scss&quot; as *;
`

module.exports = function scssToInlineLoader(source) {
  const callback = this.async()
  processScss(source, this.resourcePath)
    .then((js) =&gt; callback(null, js))
    .catch((err) =&gt; callback(err))
}

async function processScss(source, resourcePath) {
  // 1. Compile SCSS → CSS (with global variables, mixins, breakpoints)
  const sassResult = sass.compileString(PREPEND_DATA + &apos;\n&apos; + source, {
    loadPaths: [path.dirname(resourcePath), SHARED_STYLES],
    style: &apos;compressed&apos;,
    sourceMap: false,
    url: new URL(&apos;file://&apos; + resourcePath),
  })

  // 2. Scope class names with postcss-modules (CSS Modules compatible)
  let classNames = {}
  const result = await postcss([
    postcssModules({
      generateScopedName(name, filename) {
        const file = path.basename(filename, path.extname(filename))
          .replace(&apos;.module&apos;, &apos;&apos;)
        const hash = createHash(&apos;md5&apos;)
          .update(filename + name)
          .digest(&apos;base64url&apos;)
          .slice(0, 6)
        return `${file}__${name}__${hash}`
      },
      getJSON(_, json) { classNames = json },
    }),
  ]).process(sassResult.css, { from: resourcePath })

  // 3. Export as JS module — same interface as CSS Modules
  return [
    `export const cssText = ${JSON.stringify(result.css)};`,
    `export const styles = ${JSON.stringify(classNames)};`,
    `export default styles;`,
  ].join(&apos;\n&apos;)
}</code></pre>
<p>Hva den gjør: <strong><code>styles</code></strong> — det samme scopede klassenavnkartet som standard CSS Modules. <strong><code>cssText</code></strong> — kompilert CSS som streng.</p>
<h3>2. InlineStyle-komponent</h3>
<p>Bruker React 19s innebygde <code>&lt;style href precedence&gt;</code>-API for automatisk deduplisering:</p>
<pre><code>export function InlineStyle({ css, id }: { css: string; id: string }) {
  return (
    &lt;style href={id} precedence=&quot;default&quot;&gt;
      {css}
    &lt;/style&gt;
  )
}</code></pre>
<p>React 19 garanterer: samme <code>href</code> → bare én <code>&lt;style&gt;</code> i DOM.</p>
<h3>3. Migrering per komponent (~6 linjer per seksjon)</h3>
<pre><code>// BEFORE: Turbopack → &lt;link rel=&quot;stylesheet&quot;&gt; (render-blocking)
import styles from &apos;./hero_section.module.scss&apos;</code></pre>
<pre><code>// AFTER: Custom loader → JS module → inline &lt;style&gt; (non-blocking)
import { cssText, styles } from &apos;./hero_section.module.scss&apos; with {
  turbopackLoader: &apos;@promova/scss-to-inline-loader&apos;,
  turbopackAs: &apos;*.js&apos;
}
import { InlineStyle } from &apos;@promova/scss-to-inline-loader/InlineStyle&apos;

export function HeroSection() {
  return (
    &lt;section className={styles.hero}&gt;
      &lt;InlineStyle css={cssText} id=&quot;hero-section&quot; /&gt;
      {/* ... component JSX, className usage is identical */}
    &lt;/section&gt;
  )
}</code></pre>
<p><strong><code>.module.scss</code>-filene forblir nøyaktig de samme.</strong> Ingen CSS-omskriving.</p>
<h2>Hvorfor dette er bedre enn inlineCss: true</h2>
<p>Her er den kritiske forskjellen:</p>
<table><thead><tr><th></th><th>&lt;code&gt;inlineCss: true&lt;/code&gt;</th><th>Import Attributes + Loader</th></tr></thead><tbody><tr><td>What gets inlined</td><td>&lt;strong&gt;ALL CSS on the page&lt;/strong&gt; (94 files)</td><td>&lt;strong&gt;Only CSS for rendered sections&lt;/strong&gt; (5-8 files)</td></tr><tr><td>SSR overhead</td><td>CSS rebuilt on every request</td><td>CSS compiled at build time</td></tr><tr><td>Per-route control</td><td>No (all-or-nothing)</td><td>Yes (per-import)</td></tr><tr><td>SSR pages safety</td><td>Risky (overloads server)</td><td>Safe (component-level)</td></tr></tbody></table>
<p>Med <code>inlineCss: true</code> får en side fortsatt ALLE 94 stilark inline. Med vår tilnærming havner <strong>bare CSS som faktisk rendres i HTML-en</strong>.</p>
<h2>Turbopack-fellen: ingen globale regler for .module.scss</h2>
<p>En felle jeg gikk i: du kan tro at du kan legge til en Turbopack-regel i <code>next.config.ts</code> for å bruke loaderen globalt:</p>
<pre><code>// ❌ THIS CAUSES A FATAL PANIC
turbopack: {
  rules: {
    &apos;**/components/**/*.module.scss&apos;: {
      loaders: [&apos;@promova/scss-to-inline-loader&apos;],
      as: &apos;*.js&apos;
    }
  }
}</code></pre>
<p><strong>Ikke gjør dette.</strong> Turbopacks innebygde CSS-modul-pipeline fanger <code>.module.scss</code>-filer <strong>før</strong> egendefinerte regler brukes, noe som forårsaker:</p>
<pre><code>FATAL PANIC: inner asset should be CSS processable</code></pre>
<p><code>with {}</code>-attributter fungerer fordi de instruerer Turbopack <strong>på importstedet</strong> om å omgå CSS-modul-pipelinen fullstendig.</p>
<h2>Resultater</h2>
<p>127 seksjonskomponenter migrert i Landing Builder. Produksjonsbygg verifisert.</p>
<table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Render-blocking CSS files</td><td>94</td><td>0 (for landing sections)</td></tr><tr><td>CSS in HTML per page</td><td>~330 KB (all sections)</td><td>~20-40 KB (rendered sections only)</td></tr><tr><td>CSS delivery</td><td>&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; (network request, blocking)</td><td>&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; (inline, non-blocking)</td></tr><tr><td>Sass features</td><td>Full</td><td>Full (variables, mixins, nesting, @use)</td></tr><tr><td>CSS Modules scoping</td><td>Built-in</td><td>postcss-modules (compatible)</td></tr><tr><td>Code change per section</td><td>—</td><td>~6 lines (import + InlineStyle)</td></tr></tbody></table>
<h2>Begrensninger</h2>
<ul><li><strong><code>with {}</code> per import er ordrig</strong> — hver import trenger 3 ekstra linjer.</li><li><strong>Bare Turbopack</strong> — <code>with {}</code>-attributter støttes ikke av Webpack.</li><li><strong>Klassenavnhashing</strong> — vår loader bruker en annen hashingsalgoritme enn Turbopacks innebygde.</li><li><strong>HTML-størrelse øker</strong> — CSS er inline i HTML i stedet for cachede separate filer.</li></ul>
<h2>Når du bør bruke dette</h2>
<p>Denne teknikken er mest effektiv når:</p>
<ol><li><strong>Du har et registry/barrel-mønster</strong> — én fil importerer mange komponenter, men bare noen få rendres per side</li><li><strong>Du bruker Turbopack</strong> — Import Attributes er Turbopack-spesifikke</li><li><strong>Du vil ha kontroll per komponent</strong> — ikke et alt-eller-ingenting-flagg</li><li><strong>SCSS-en din er kompleks</strong> — variabler, mixins, breakpoints, nesting — alt støttet</li><li><strong>Du ikke kan bruke <code>experimental.inlineCss</code></strong> — fordi du har SSR-sider eller vil ha granulær kontroll</li></ol>
<h2>Relaterte GitHub Issues</h2>
<p>Hvis du er berørt av render-blokkerende CSS i Next.js App Router — du er ikke alene:</p>
<h3>Kjerneproblemet</h3>
<ul><li><a href="https://github.com/vercel/next.js/issues/62485" target="_blank" rel="noopener noreferrer"><strong>#62485</strong></a> — Render blocking CSS (maintainers: "expected behavior")</li><li><a href="https://github.com/vercel/next.js/issues/61066" target="_blank" rel="noopener noreferrer"><strong>#61066</strong></a> — Dynamic imports from Server Components are not code-split</li><li><a href="https://github.com/vercel/next.js/issues/54935" target="_blank" rel="noopener noreferrer"><strong>#54935</strong></a> — Server-side dynamic imports don't split client modules</li><li><a href="https://github.com/vercel/next.js/issues/61574" target="_blank" rel="noopener noreferrer"><strong>#61574</strong></a> — JS/CSS code splitting doesn't work as documented</li><li><a href="https://github.com/vercel/next.js/issues/57634" target="_blank" rel="noopener noreferrer"><strong>#57634</strong></a> — Add support for critical CSS inlining with App Router</li><li><a href="https://github.com/vercel/next.js/issues/50300" target="_blank" rel="noopener noreferrer"><strong>#50300</strong></a> — next/dynamic on server component does not build CSS modules</li></ul>
<h3>inlineCss-funksjonen &amp; problemer</h3>
<ul><li><a href="https://github.com/vercel/next.js/pull/72195" target="_blank" rel="noopener noreferrer"><strong>PR #72195</strong></a> — experimental: css inlining (the implementation)</li><li><a href="https://github.com/vercel/next.js/pull/73182" target="_blank" rel="noopener noreferrer"><strong>PR #73182</strong></a> — Don't inline CSS in RSC payload for client navigation</li><li><a href="https://github.com/vercel/next.js/issues/75648" target="_blank" rel="noopener noreferrer"><strong>#75648</strong></a> — WhatsApp preview broken with inlineCss + Tailwind</li><li><a href="https://github.com/vercel/next.js/issues/83612" target="_blank" rel="noopener noreferrer"><strong>#83612</strong></a> — Turbopack wrong font URL with inline CSS</li></ul>
<h3>Fellesskapet søker løsninger</h3>
<ul><li><a href="https://github.com/vercel/next.js/discussions/82894" target="_blank" rel="noopener noreferrer"><strong>Discussion #82894</strong></a> — How to prevent render-blocking with CSS Modules? (2025)</li><li><a href="https://github.com/vercel/next.js/discussions/70526" target="_blank" rel="noopener noreferrer"><strong>Discussion #70526</strong></a> — Ideas for reducing render-blocking CSS</li><li><a href="https://github.com/vercel/next.js/discussions/59814" target="_blank" rel="noopener noreferrer"><strong>Discussion #59814</strong></a> — Render blocking styles with Tailwind</li><li><a href="https://github.com/vercel/next.js/discussions/49691" target="_blank" rel="noopener noreferrer"><strong>Discussion #49691</strong></a> — How to deal with render-blocking modular CSS</li><li><a href="https://github.com/vercel/next.js/discussions/59989" target="_blank" rel="noopener noreferrer"><strong>Discussion #59989</strong></a> — Critical CSS inlining with App Router</li><li><a href="https://github.com/vercel/next.js/discussions/80486" target="_blank" rel="noopener noreferrer"><strong>Discussion #80486</strong></a> — Is optimizeCss still in use?</li><li><a href="https://github.com/vercel/next.js/discussions/85465" target="_blank" rel="noopener noreferrer"><strong>Discussion #85465</strong></a> — Turbopack plugin API (doesn't exist)</li></ul>
<h3>Turbopack CSS-feil</h3>
<ul><li><a href="https://github.com/vercel/next.js/issues/68412" target="_blank" rel="noopener noreferrer"><strong>#68412</strong></a> — Incorrect CSS modules load order with external components</li><li><a href="https://github.com/vercel/next.js/issues/76464" target="_blank" rel="noopener noreferrer"><strong>#76464</strong></a> — Turbopack sometimes strips SCSS imports</li><li><a href="https://github.com/vercel/next.js/issues/82497" target="_blank" rel="noopener noreferrer"><strong>#82497</strong></a> — SCSS module not loading when not-found.tsx is present</li><li><a href="https://github.com/vercel/next.js/issues/88544" target="_blank" rel="noopener noreferrer"><strong>#88544</strong></a> — Exporting Sass variables from CSS modules doesn't work with Turbopack</li></ul>
<p>Bygget hos <a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a> — en språklæringsplattform som betjener millioner av brukere.</p>]]></content:encoded>
      <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
      <category>Next.js</category>
      <category>CSS</category>
      <category>Performance</category>
      <category>Turbopack</category>
      <category>SCSS</category>
    </item>
    <item>
      <title>Next.js Memory Leak: Fetch + Standalone-modus — 2 år uten fiks</title>
      <link>https://oleksiimazurenko.dev/nb/blog/nextjs-memory-leak-fetch-standalone</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/nb/blog/nextjs-memory-leak-fetch-standalone</guid>
      <description>Next.js patcher den globale fetch og legger til et cache-lag som lekker minne ved hver forespørsel. I Docker/K8s fører dette til OOM-krasj hver par timer. Buggen har eksistert siden Next.js 14 og er fortsatt uløst i 16.2.x.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Next.js patcher den globale <code>fetch</code> og legger til et cache-lag som holder referanser til responsdata etter at de burde ha blitt frigjort. Hvert <code>fetch</code>-kall legger til minne som aldri returneres til GC. I Docker/Kubernetes fører dette til OOM-krasj hver par timer. Buggen har eksistert siden Next.js 14 (april 2024) og er fortsatt uløst i 16.2.x (mars 2026). På Vercel viser ikke problemet seg takket være efemere serverless-funksjoner.</p>
<h2>Hvordan normal fetch fungerer</h2>
<pre><code>Request → fetch → got data → response to user → GC cleans up → memory free</code></pre>
<h2>Hvordan fetch fungerer i Next.js</h2>
<p>Next.js fanger opp den globale <code>fetch</code> og pakker den inn med sitt eget cache-/tracking-lag:</p>
<pre><code>Request → Next.js fetch wrapper → creates:
  - Performance entry (speed tracking)    ← not cleaned
  - Request metadata object               ← not cleaned
  - Internal promise wrapper              ← not cleaned
  - Cache lookup entry                    ← not cleaned
  - Response body (for unique URL)        ← not cleaned
→ Response to user
→ GC sees objects are still referenced → doesn&apos;t touch them
→ Memory grows indefinitely</code></pre>
<h2>Hva som skjer i produksjon</h2>
<pre><code>Self-hosted (Docker/K8s):
  Request 1:      fetch → +100KB → RAM: 100KB
  Request 2:      fetch → +100KB → RAM: 200KB
  Request 1000:   fetch → +100KB → RAM: 100MB
  Request 50,000: fetch → +100KB → RAM: 5GB → OOM Kill 💀
  → Kubernetes restarts the pod → cycle repeats

Vercel (Serverless):
  Request 1: [start] → fetch → +100KB → [process dies] → 0 KB ✅
  Request 2: [start] → fetch → +100KB → [process dies] → 0 KB ✅
  → Memory never accumulates</code></pre>
<h2>Berørte versjoner</h2>
<h3>Next.js — Alle versjoner med App Router</h3>
<table><thead><tr><th>Version</th><th>Issue</th><th>Date</th></tr></thead><tbody><tr><td>14.2.x</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/64212&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#64212&lt;/a&gt;</td><td>April 2024</td></tr><tr><td>14.x-15.x</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/68578&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#68578&lt;/a&gt;</td><td>August 2024</td></tr><tr><td>14.3.0-canary</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/79588&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#79588&lt;/a&gt;</td><td>May 2025</td></tr><tr><td>16.0.1</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/85914&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#85914&lt;/a&gt;</td><td>November 2025</td></tr><tr><td>16.1.0</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/discussions/88603&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#88603&lt;/a&gt;</td><td>January 2026</td></tr><tr><td>16.0.10</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/90433&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#90433&lt;/a&gt;</td><td>February 2026</td></tr><tr><td>16.2.0-canary.51</td><td>Confirmed in #90433 comments</td><td>March 2026</td></tr></tbody></table>
<h3>Node.js</h3>
<p>Testet på Node.js 20, 22, 24, 25 — lekker på alle.</p>
<h2>Hva som ikke fungerer</h2>
<table><thead><tr><th>Attempt</th><th>Result</th></tr></thead><tbody><tr><td>&lt;code&gt;cacheMaxMemorySize: 0&lt;/code&gt;</td><td>Doesn&apos;t help — leak is not from this cache</td></tr><tr><td>Disable image optimization</td><td>Doesn&apos;t help</td></tr><tr><td>&lt;code&gt;--max-old-space-size=6144&lt;/code&gt;</td><td>Just crashes slower</td></tr><tr><td>Read response via &lt;code&gt;.json()&lt;/code&gt; / &lt;code&gt;.text()&lt;/code&gt;</td><td>Doesn&apos;t help</td></tr><tr><td>Remove React fetch patching (canary.45)</td><td>Doesn&apos;t help</td></tr><tr><td>Upgrade to latest version</td><td>Doesn&apos;t help (leaks on 16.2.0-canary.51 too)</td></tr></tbody></table>
<h2>Hva som fungerer (workarounds)</h2>
<table><thead><tr><th>Workaround</th><th>Why it helps</th></tr></thead><tbody><tr><td>Replace &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;axios&lt;/code&gt;</td><td>Bypasses Next.js wrapper</td></tr><tr><td>Replace &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;node-fetch&lt;/code&gt;</td><td>Same reason</td></tr><tr><td>Downgrade Node.js to 20.15.1</td><td>Older undici has fewer leaks (for some)</td></tr><tr><td>Docker image &lt;code&gt;node:20-alpine3.21&lt;/code&gt;</td><td>Helps in some cases</td></tr><tr><td>Deploy to Lambda (SST + OpenNext)</td><td>Ephemeral functions — memory doesn&apos;t accumulate</td></tr><tr><td>Deploy to Vercel</td><td>Same — serverless</td></tr></tbody></table>
<h3>Begrensning med axios-workaround</h3>
<p>Dine egne API-kall kan erstattes med axios. Men Next.js bruker internt den patchede fetch for:</p>
<ul><li>ISR (Incremental Static Regeneration)</li><li><code>revalidatePath</code> / <code>revalidateTag</code></li><li>Server Components data fetching med deduplisering</li><li><code>use cache</code> (Next.js 16)</li></ul>
<p>Selv uten en eneste <code>fetch</code> i koden din — bruker Next.js den fortsatt internt.</p>
<h2>Hvorfor Vercel ikke fikser det</h2>
<h3>Forretningslogikk</h3>
<p>Vercel er et selskap som tjener penger på å hoste Next.js. På deres plattform viser ikke problemet seg (serverless = efemert). Buggen påvirker bare self-hosted (Docker, K8s, VPS) — de som ikke betaler Vercel.</p>
<h3>Offisiell posisjon</h3>
<p>Tim Neutkens (Vercel-maintainer) analyserte problemet og erklærte at det er et <strong>undici</strong>-problem (Node.js fetch-bibliotek), ikke Next.js. Issue #90433 ble stengt. Til tross for at:</p>
<ul><li>axios og node-fetch på samme Node.js fungerer uten lekkasjer</li><li>Lekkasjen bare oppstår når fetch går gjennom Next.js-wrapperen</li><li>Buggen har vært åpen i 2 år uten fiks</li></ul>
<h3>Prioriteringer</h3>
<p>I løpet av disse 2 årene har Next.js-teamet sluppet:</p>
<ul><li>Turbopack (2-5x raskere builds) — markedsføringsfordel</li><li>Cache Components / <code>use cache</code> — reduserer belastningen på Vercel-servere</li><li><code>proxy.ts</code> i stedet for middleware — forenkler edge-deployment på Vercel</li><li>DevTools MCP — AI-hype</li></ul>
<p>Memory leak i self-hosted? Ikke en prioritet.</p>
<h2>Løsning: AWS Lambda (SST + OpenNext)</h2>
<h3>Hva er det</h3>
<p>OpenNext er en open-source-adapter som konverterer en Next.js-build til et format for AWS Lambda. SST er et rammeverk som automatiserer infrastrukturen.</p>
<h3>Arkitektur</h3>
<pre><code>Next.js build
  → OpenNext
    → AWS Lambda (SSR, API routes)
    → S3 (static, assets)
    → CloudFront (CDN)
    → SQS + DynamoDB (ISR revalidation)</code></pre>
<h3>Hvorfor dette løser minneslekkasjen</h3>
<p>Lambda-funksjoner behandler forespørsler og resirkuleres etter 5-15 minutters inaktivitet. Minnet rekker ikke å akkumuleres.</p>
<h3>Deployment</h3>
<pre><code>npx sst@latest init
npx sst deploy --stage production</code></pre>
<h3>Sammenligning</h3>
<table><thead><tr><th></th><th>Vercel</th><th>AWS Lambda (SST)</th><th>Docker self-hosted</th></tr></thead><tbody><tr><td>Memory leak</td><td>Not felt</td><td>Not felt</td><td>Critical</td></tr><tr><td>Cold starts</td><td>Yes</td><td>Yes (~200-500ms)</td><td>No</td></tr><tr><td>Price (~medium traffic)</td><td>$20-150/mo</td><td>$5-30/mo</td><td>$5-50/mo</td></tr><tr><td>Control</td><td>Minimal</td><td>Full</td><td>Full</td></tr><tr><td>ISR/Revalidation</td><td>Works</td><td>Works (SQS)</td><td>Works (with leak)</td></tr><tr><td>Vendor lock-in</td><td>Vercel</td><td>AWS</td><td>None</td></tr></tbody></table>
<h3>Lambda-nyanser</h3>
<ul><li><strong>Cold starts</strong> — første forespørsel er tregere (~200-500ms)</li><li><strong>Sikkerhet</strong> — aktiver OAC (Origin Access Control), ellers er Lambda-URL-en offentlig</li><li><strong>OpenNext</strong> — community-prosjekt, ikke offisielt fra Vercel. Nye Next.js-funksjoner kan gå i stykker</li><li><strong>Lommebokattack</strong> — ved DDoS kan Lambda auto-scaling føre til en stor regning</li></ul>
<h2>Hvorfor en ekte fiks er urealistisk</h2>
<h3>1. Arkitekturproblem</h3>
<p>Lekkasjen er ikke en tilfeldig bug, men en konsekvens av en designbeslutning: Next.js fanger opp den globale <code>fetch</code> og legger til cache/tracking oppå. For å fikse det må måten App Router interagerer med fetch redesignes. Dette påvirker ISR, revalidation, data cache, request deduplication — kjernen i rammeverket.</p>
<h3>2. Interessekonflikt</h3>
<p>Vercel er ikke motivert til å fikse det som ikke påvirker deres plattform. Self-hosted konkurrerer med deres forretning. Jo flere problemer i self-hosted — jo flere migrerer til Vercel.</p>
<h3>3. Skyldforskyvning</h3>
<p>Den offisielle posisjonen er "det er undici, ikke oss". Inntil det endrer seg — vil de ikke jobbe med en fiks.</p>
<h3>4. Ingen community-fiks</h3>
<p>Next.js AGPL-3.0-lisens tillater forks, men kodebasen er enorm og tett koblet til Vercel-infrastrukturen. En community-PR for å fikse fetch-wrapperen ville kreve dyp forståelse av den interne arkitekturen og godkjenning fra maintainers — som allerede har stengt issuen.</p>
<h2>Konklusjoner</h2>
<ol><li><strong>Hvis du er på Vercel</strong> — ikke noe problem, ingenting å gjøre</li><li><strong>Hvis self-hosted og trenger serverless</strong> — SST + OpenNext på AWS Lambda</li><li><strong>Hvis self-hosted Docker</strong> — erstatt fetch med axios der det er mulig, overvåk RAM, konfigurer automatisk pod-omstart</li><li><strong>Hvis du starter et nytt prosjekt</strong> — vurder SvelteKit eller Nuxt som alternativer uten dette problemet</li></ol>
<h2>Kilder</h2>
<ul><li><a href="https://github.com/vercel/next.js/issues/64212" target="_blank" rel="noopener noreferrer">Issue #64212</a> — Memory Leak with global fetch (April 2024)</li><li><a href="https://github.com/vercel/next.js/issues/68578" target="_blank" rel="noopener noreferrer">Issue #68578</a> — Possible memory leak in Fetch API (August 2024)</li><li><a href="https://github.com/vercel/next.js/issues/85914" target="_blank" rel="noopener noreferrer">Issue #85914</a> — Memory Leak with fetch + standalone (November 2025)</li><li><a href="https://github.com/vercel/next.js/issues/90433" target="_blank" rel="noopener noreferrer">Issue #90433</a> — OOM in 16.0.10 (February 2026)</li><li><a href="https://github.com/vercel/next.js/discussions/88603" target="_blank" rel="noopener noreferrer">Discussion #88603</a> — OOM in Docker/K8s (January 2026)</li><li><a href="https://github.com/vercel/next.js/discussions/88078" target="_blank" rel="noopener noreferrer">Discussion #88078</a> — cacheMaxMemorySize behavior</li><li><a href="https://opennext.js.org/" target="_blank" rel="noopener noreferrer">OpenNext</a></li><li><a href="https://sst.dev/docs/start/aws/nextjs/" target="_blank" rel="noopener noreferrer">SST — Next.js on AWS</a></li><li><a href="https://www.flightcontrol.dev/blog/secret-knowledge-to-self-host-nextjs" target="_blank" rel="noopener noreferrer">Secret knowledge to self-host Next.js</a></li><li><a href="https://nextjs.org/docs/app/guides/memory-usage" target="_blank" rel="noopener noreferrer">Next.js Memory Usage Guide</a></li></ul>]]></content:encoded>
      <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
      <category>Next.js</category>
      <category>Memory Leak</category>
      <category>Docker</category>
      <category>AWS Lambda</category>
      <category>Node.js</category>
    </item>
  </channel>
</rss>