<?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/sv/blog</link>
    <description>Technical articles about web development, performance optimization, and developer tools.</description>
    <language>sv</language>
    <lastBuildDate>Sat, 13 Jun 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://oleksiimazurenko.dev/sv/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/sv/blog/claude-profiles-clean-architecture</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/claude-profiles-clean-architecture</guid>
      <description>Efter en månad med det gamla alias-baserade upplägget byggde jag om mitt multi-profile-setup för Claude Code till en XDG-kompatibel struktur under ~/.config/claude-profiles/. Den verkliga orsaken var inte snyggheten — det var att jag upptäckte att ~/.claude.json är en separat fil i home-roten där MCP-servrarna som lagts till via --scope user tyst levde sitt eget liv.</description>
      <content:encoded><![CDATA[<p>För en månad sedan publicerade jag <a href="/sv/blog/multiple-claude-accounts-one-device">ett inlägg om hur man kör två Claude Code-konton parallellt på samma maskin</a> — privat och jobb, via <code>CLAUDE_CONFIG_DIR</code> och ett shell-alias. Upplägget fungerade. Allt gjorde det det skulle göra.</p>
<p>Men efter en månads riktig användning sprang jag på ett par rejäla problem som det inlägget inte täckte. Det största — en dold egenhet hos Claude Code som jag av en slump trampade rakt in i när jag lade till Gmail-MCP:n och den "försvann" från min profil fem minuter senare. Idag byggde jag om alltihop till en ny arkitektur — XDG-kompatibel, med en symlänk och en interaktiv profilväljare via <code>gum</code>. Det här är v2 — en evolution ur verklig erfarenhet, inte en teoretisk förbättring.</p>
<h2>Det som började skava</h2>
<p>Fyra saker. De första tre handlar om snygghet, synlighet och skala. Den fjärde är den riktiga arkitektoniska fällan som jag inte såg direkt. Jag går igenom dem i tur och ordning, för det är just den fjärde som till slut tvingade fram ombyggnaden.</p>
<h3>1. Två separata mappar i $HOME — det är rörigt</h3>
<p><code>~/.claude</code> och <code>~/.claude-promova</code> — två dotfolders sida vid sida i $HOME-roten. XDG Base Directory Specification säger att konfigar hör hemma i <code>~/.config/</code>. Dotfile-mappar utspridda direkt i home är ett antimönster som med tiden gör $HOME till en skräplåda. Kosmetiskt, visst, men det stack i ögonen varje gång jag körde <code>ls -la ~</code>.</p>
<h3>2. Ingen visuell bekräftelse på aktiv profil</h3>
<p>Jag startar <code>claude</code> och vet inte vilken profil som är aktiv — inte förrän jag kör <code>claude config list</code> inne i sessionen. Om jag glömt vilken terminal jag startat var, måste jag kolla. En småsak, men med personal + work i parallell i två flikar tornar det upp sig.</p>
<h3>3. Alias skalar inte</h3>
<p>Två profiler — <code>claude</code> och <code>claude-promova</code> — okej. Jag lägger till en tredje (en frilanskund) — då behövs ett tredje alias. En fjärde — ett fjärde. Efter ett halvår skulle jag inte minnas vilka alias jag faktiskt skapat.</p>
<h3>4. Den dolda fällan i ~/.claude.json</h3>
<p>Och det här är det verkliga skälet till ombyggnaden. Claude Code har <strong>två olika platser</strong> för konfiguration, och dokumentationen skriker inte ut det: <code>~/.claude/</code> — katalogen där <code>projects/</code>, <code>sessions/</code>, <code>hooks/</code>, <code>skills/</code> ligger. Och separat — <code>~/.claude.json</code>, en fil direkt i $HOME-roten, där <code>oauthAccount</code>, <code>mcpServers</code>, <code>projects</code>-historiken, <code>skillUsage</code> och ungefär 40 andra fält av faktisk live state bor.</p>
<p>Kommandot <code>claude mcp add --scope user</code> skriver just till <code>~/.claude.json</code> i home-roten, <strong>inte</strong> till <code>~/.claude/.claude.json</code> eller profilens katalog. Det visste jag inte. Tills jag en dag trampade rätt i det.</p>
<h2>Discovery: varför Gmail-MCP:n &quot;försvann&quot;</h2>
<p>I morse satte jag upp Gmail-MCP:n i Claude Code. Vanligt setup: Google Cloud-projekt, OAuth-credentials, <code>claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp</code>. Allt OK. Startade om sessionen — funkar, jag läser mail, svarar på meddelanden. En timme senare började vi refaktorera aliasen till en funktion med <code>gum</code>-profilväljare, och flyttade sedan över alltihop till XDG. Jag körde <code>mv ~/.claude → ~/.config/claude-profiles/personal</code>, startade om CC, valde personal i menyn. Och i den nya sessionen öppnade jag <code>/mcp</code>:</p>
<pre><code>figma            (failed)
playwright-test
claude.ai Notion</code></pre>
<p>Ingen gmail. Ingen vaultforge. Bara tre servrar, varav en till och med failed. Och jag hade just lagt till Gmail. Samtidigt visade sessionen i en annan terminal (work-profilen) Gmail och Vaultforge utan minsta problem.</p>
<p>Jag började gräva och hittade att jag på maskinen hade <strong>tre</strong> olika filer med namnet <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>Där hade vi det. Det är just det som är den arkitektoniska fällan:</p>
<ol><li><code>claude mcp add --scope user</code> skriver alltid till <code>~/.claude.json</code> i home-roten, oberoende av <code>CLAUDE_CONFIG_DIR</code></li><li>När <code>CLAUDE_CONFIG_DIR</code> är satt läser Claude Code <code>$CLAUDE_CONFIG_DIR/.claude.json</code> — alltså filen inuti profilen</li><li>Posterna för "user-scope MCPs" och "profilens MCPs" lever i <strong>olika filer</strong> med samma namn — och de är lätta att blanda ihop</li></ol>
<p>I mitt fall var <code>~/.claude.json</code> (home-roten, 113 KB) det levande, aktuella tillståndet — med gmail, vaultforge, OAuth-sessionen, allt. Och <code>~/.config/claude-profiles/personal/.claude.json</code> (29 KB) visade sig vara en gammal snapshot som av någon anledning redan tidigare legat i gamla <code>~/.claude/</code> — kanske skrev en äldre CC-version dit, kanske ett plugin. <code>jq -r 'keys[]'</code> på båda filerna visade att home-root-versionen hade 41 unika nycklar som saknades i snapshoten.</p>
<p>Och de 41 nycklarna är inte skräp. Det är Claude Codes verkliga tillstånd:</p>
<ul><li><code>skillUsage</code> — användningsstatistik för skills</li><li><code>githubRepoPaths</code> — repo-cache för projektnavigering</li><li><code>cachedGrowthBookFeatures</code> + <code>cachedStatsigGates</code> — feature flags (utan dem hämtar CC nya vid varje start)</li><li><code>hasShownOpus45Notice</code>, <code>hasShownOpus46Notice</code>, <code>hasShownS1MWelcomeV2</code> — UI-flaggor (utan dem dyker modalerna upp igen vid nästa start)</li><li><code>lastPlanModeUse</code>, <code>feedbackSurveyState</code>, <code>installMethod</code> — onboarding- och UX-state</li></ul>
<p>Om du bara kör <code>mv ~/.claude ~/.config/claude-profiles/personal</code> utan merge — förlorar du allt detta. Du ser welcome-modalerna igen, githubRepoPaths-sökningen körs på nytt, och alla survey-prompts kommer tillbaka. Som jag nästan gjorde.</p>
<h2>Den nya arkitekturen</h2>
<p>Allt lever under en gemensam föräldrakatalog i <code>~/.config/</code>, precis som XDG vill ha det. Varje profil är självförsörjande — den har sitt fulla state inklusive en egen <code>.claude.json</code>. <code>~/.claude</code> stannar kvar som symlänk till personal-profilen för bakåtkompatibilitet.</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. Varje profil är en separat, isolerad katalog där allt ligger: <code>projects</code>/<code>sessions</code>-mapparna och samma fil med MCP-servrar och OAuth-tokens. En source of truth per profil.</p>
<h3>Varför symlänken pekar på personal</h3>
<p>Allt som hårdkodat sökvägen <code>~/.claude/</code> — gamla skript, plugins, Claude Codes IDE-tillägg, statusline-konfigar som <code>claude-powerline.json</code> — fortsätter fungera utan ändringar. Symlänken resolveras till personal-profilen. Om du av misstag kör <code>command claude</code> (utan wrapper-funktionen) — hamnar du också i personal via default-path lookup. Personal blir den "quiet default" den var förr, men bor nu fysiskt på XDG-platsen.</p>
<h3>Interaktiv väljare vid start — funktion + gum</h3>
<p>I stället för alias — en funktion <code>claude()</code> i <code>~/.zshrc</code> som visar en pilmeny via <code>gum</code> (Charms TUI-hjälpverktyg). Funktionen fångar <code>claude</code>-anropet på shell-nivå, låter dig välja profil och kör <code>command claude</code> med motsvarande <code>CLAUDE_CONFIG_DIR</code>. <code>command</code> är viktigt — det förbigår wrapper-funktionen och anropar den riktiga 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>Så här ser det ut vid start:</p>
<pre><code>Claude profile:
▸ personal
  promova</code></pre>
<p>↑↓ för att navigera, Enter för att välja, Esc för att avbryta (Claude startar helt enkelt inte). Profilen är alltid synlig — omöjlig att missa.</p>
<h2>Migreringsskript</h2>
<p>För den som läser det här och vill flytta från det gamla upplägget. Det mest kritiska steget är det andra: det mergar <code>~/.claude.json</code> från home-roten med det som redan ligger i personal-profilen, och slår ihop <code>mcpServers</code>-listorna. Utan det steget förlorar profilen både sina MCPs och hela sitt live state.</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 rättigheter är viktigt i sig. <code>jq | mv</code> skapar en fil med umask 644 (world-readable). Den innehåller OAuth-tokens. <code>chmod 600</code> direkt efter mergen är obligatoriskt.</p>
<p>Efter migreringen — stäng och öppna alla aktiva Claude-sessioner, ladda om shellet (<code>source ~/.zshrc</code> eller ny terminal), kör <code>claude</code>, välj profil, verifiera via <code>claude mcp list</code> att alla MCPs är på plats. Om allt är OK — radera <code>~/.claude.json.migrated.bak</code>. Om något är fel — rollback är trivialt: <code>mv ~/.claude.json.migrated.bak ~/.claude.json</code> och ta bort symlänken.</p>
<h2>Det du får</h2>
<ul><li>En gemensam föräldrakatalog i stället för två dotfolders i $HOME — XDG-kompatibelt</li><li>Symlänken bevarar kompatibiliteten med allt som hårdkodar <code>~/.claude/</code></li><li>Varje profil är självförsörjande — sitt fulla state och sina MCPs i sin egen <code>.claude.json</code></li><li>En source of truth per profil — slut på övergiven konfig i home-roten som tyst driver isär från profilens</li><li>Känslig data (oauthAccount, tokens) garanterat med rättigheter 600</li><li>Visuell bekräftelse på aktiv profil vid varje start — omöjligt att glömma vilken profil som är aktiv</li><li>Lägga till en tredje profil = lägga till en rad i <code>case</code> i funktionen, inte klona ett nytt alias och komma ihåg dess namn</li></ul>
<h2>Där det brister</h2>
<p>Jag vill vara ärlig. Det här är inte en gratis förbättring — några kompromisser följer med på köpet, och dem är värda att känna till i förväg.</p>
<ul><li><strong><code>gum</code> är ett extra beroende</strong> (<code>brew install gum</code>, ~13 MB). Om du av princip inte vill installera det — fallback till <code>select</code> i zsh eller en enkel <code>read</code>. Funkar, men ser inte lika snyggt ut och saknar pilnavigering.</li><li><strong>Ett Enter vid varje start.</strong> För den som drar igång <code>claude</code> tiotals gånger om dagen — kan irritera. Alternativ nedan (direnv).</li><li><strong>Symlänken <code>~/.claude → personal</code> gör personal till default.</strong> Om du behöver work-profilen som default — måste du peka om symlänken (<code>ln -sf</code>). Inte svårt, men det är inte "glöm bort det och inget går sönder".</li><li><strong>Symlänken kan teoretiskt gå sönder</strong> om något verktyg atomärt skriver om <code>~/.claude.json</code> via ett temp+rename-mönster (write-file-atomic). I praktiken gör Claude Code inte det själv, men om du installerar tredjepartsplugins — kolla.</li><li><strong>Om du har olika Anthropic-konton på profilerna med olika planer</strong> — efter byte kan det bli en lag på under en sekund medan Claude Code synkar OAuth-state. I min användning märks det inte, men det är inte noll.</li></ul>
<h2>Alternativ jag övervägde</h2>
<p><strong>direnv</strong> — sätter automatiskt <code>CLAUDE_CONFIG_DIR</code> utifrån en <code>.envrc</code> i roten på varje projekt. Noll interaktion, noll klick. Minus: du måste lägga en <code>.envrc</code> i varje work-root, och om du kör <code>claude</code> i en okänd mapp — får du default-profilen (kanske inte den du vill ha). För den som lever i ett begränsat antal work-roots och aldrig vill klicka — är direnv faktiskt bättre.</p>
<p><strong>Symlänk-baserad växling</strong> (en enda aktiv profil genom att peka om <code>~/.claude</code>-symlänken) övervägde jag också och förkastade direkt. Du kan inte ha två terminaler med olika profiler öppna samtidigt — det globala "aktuella" är ett enda. För mig är det en deal-breaker.</p>
<h2>Slutsats</h2>
<p>v2 är inte bara bättre UX ovanpå v1. Det är ett erkännande av att Claude Code har en dold arkitektonisk egenhet (<code>~/.claude.json</code> som separat fil i home-roten, skriven av <code>--scope user</code>-kommandon oberoende av <code>CLAUDE_CONFIG_DIR</code>) som man måste ta hänsyn till om man vill ha riktig isolation mellan profiler. Den första ansatsen (<code>~/.claude</code> + <code>~/.claude-promova</code> + alias) fungerade till 80 %, men de återstående 20 % visade sig som tyst state-drift mellan profilerna. Nu är det hanterat. Om du precis börjar — starta direkt med v2. Om du redan sitter på v1 — migreringsskriptet finns ovan, flytten tar fem minuter och bryter ingenting (det är just <code>jq</code>-mergen som räddar dig från state-förlust).</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>Hur man skriver Claude Code-agenter som inte ljuger för dig</title>
      <link>https://oleksiimazurenko.dev/sv/blog/writing-specialized-agents</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/writing-specialized-agents</guid>
      <description>Två regler för att bygga tillförlitliga Claude Code-agentpipelines: en agent per specialisering och shell-kommandon istället för prompts överallt där kvantitativa svar är inblandade.</description>
      <content:encoded><![CDATA[<p>Du bad Claude Code att <em>implementera den här designen och verifiera att den matchar Figma-mockupen</em>. Det kom tillbaka: <em>Klart. Alla sektioner matchar, avstånd är korrekt, färgerna stämmer.</em> Du öppnade sidan. Halva avstånden var fel. Hover-tillståndet fanns inte. Knapparna var en nyans för ljusa. Modellen ljög inte med avsikt — den förutsåg att du ville höra <em>verifierat</em>, och producerade exakt den tokensekvensen. Det fanns inget verifieringssteg. Det kunde det aldrig finnas — verifiering kräver jämförelse mot ground truth, och en enda agent i ett enda kontext har inget sätt att kliva utanför sitt eget svar och kontrollera det.</p>
<p>Två regler har förvandlat hallucination-tunga arbetsflöden till tillförlitliga pipelines för mig: <strong>en agent, en specialisering</strong> och <strong>allt som kan köras som ett shell-kommando måste köras som ett shell-kommando</strong>. Det här är ingen teori. Det här är vad jag gör varje dag med Claude Code, och det är dessa mönster som faktiskt gör skillnad.</p>
<h2>Varför generalistiska agenter ljuger</h2>
<p>LLM:er är prediktorer för nästa token. När en prompt ber om två roller — <em>bygg X</em> och <em>verifiera X</em> — avslutar modellen den första rollen och förutsäger sedan hur utdata från den andra rollen skulle se ut, utan att faktiskt utföra den. Självverifiering är strukturellt svag: samma kontext, samma modell, samma blinda fläckar. <em>Godkänt</em> vid verifiering korrelerar med <em>godkänt</em> vid byggandet — de misslyckas tillsammans.</p>
<p>Modellen vet inte att den ljuger. Ur dess perspektiv är <em>jag verifierade allt noggrant</em> en sammanhängande fortsättning på att ha skrivit koden. Det är samma anledning till varför <em>är du säker?</em>-prompts inte fångar hallucinationer: modellen är lika säker vid det andra försöket. Säkerhet korrelerar inte med korrekthet — det korrelerar med hur rimligt nästa mening låter.</p>
<p>Lösningen är inte <em>bättre prompts</em>. <em>Var försiktig</em>, <em>dubbelkolla</em>, <em>hallucinera inte</em> — de instruktionerna gör ingenting. Lösningen är strukturell: specialisera agenten så att den fysiskt inte kan låtsas, och dirigera kvantitativt arbete genom shell:en så att svaret kommer från verkligt tillstånd, inte från tokensannolikhet.</p>
<h2>Regel 1: en agent, en specialisering</h2>
<p>Dela upp arbetet i separata agenter med separata kontexter. Varje agent har ett enda ansvar och en tight verktygsuppsättning. Hela arbetsflödet blir ett stafettlopp istället för att en agent springer varv:</p>
<ul><li><strong>Builder-agent:</strong> tar spec:en, skriver koden. Det är hela jobbet. Den har <code>Read</code>, <code>Edit</code>, <code>Write</code>, <code>Bash</code>.</li><li><strong>Reviewer-agent:</strong> tar spec:en plus diff:en, kontrollerar acceptanskriterier. Fräscht kontext. Ingen kunskap om <em>hur</em> koden skrevs, bara vad som kom ut. Den har <code>Bash</code>, <code>Read</code>, <code>Grep</code>, <code>Glob</code> — inga skrivverktyg alls.</li><li><strong>Analytics-agent:</strong> svarar på datafrågor genom att konstruera och köra queries. Bara <code>Bash</code>. Kan inte nå svaret utan att köra ett riktigt kommando.</li><li><strong>Orchestrator:</strong> huvudsessionen som dispatchar varje agent i tur och ordning och aldrig ber en agent göra en annans jobb.</li></ul>
<p>Konkret exempel: UI-implementation plus en visuell kontroll mot en Figma-mockup. Builder:n skriver komponenterna och committar diff:en. Orchestratorn anropar sedan Reviewer med design-URL:en, diff:en och explicita acceptanskriterier. Reviewer:n kör Playwright, tar skärmdumpar, jämför dem mot referensen och returnerar <code>PASS</code> eller <code>FAIL</code> med faktiska skärmdumpsvägar och pixel-diffs. Builder:n kommer aldrig i närheten av verifieringssteget — och det är exakt därför verifieringen är verklig.</p>
<p>Anti-mönstret är mega-agenten: en enda prompt som säger <em>bygg det här UI:t och se till att det matchar mockupen</em>. Jag garanterar dig, den kommer rapportera att allt matchar. Det gör det inte. Narrativet <em>jag verifierade</em> är bara den mest sannolika tokensekvensen efter <em>jag byggde det</em>.</p>
<h2>Regel 2: shell framför prompt, alltid</h2>
<p>Allt kvantitativt, allt som rör verkligt tillstånd, allt där svaret kan vara fel på ett sätt som ser rätt ut — kör det genom <code>sh</code>. Agentens jobb är att konstruera och köra kommandot, sedan läsa dess utdata. Agenten är inte källan till sanning. Shell-utdatan är det.</p>
<ul><li><strong>Räkning:</strong> <code>wc -l logs.txt</code> är sant. <em>Det finns ungefär 47 loggrader</em> från en modell är en hallucination.</li><li><strong>Analytics:</strong> <code>psql -c "SELECT count(*) FROM events WHERE created_at &gt; now() - interval '30 days'"</code>. Inte <em>uppskatta volymen</em>.</li><li><strong>Tester:</strong> <code>pnpm test --reporter=json | jq '.numFailedTests'</code>. Inte <em>sammanfatta vad som misslyckades</em>.</li><li><strong>Git-tillstånd:</strong> <code>git rev-list --count main..HEAD</code>, <code>git diff --stat</code>. Inte <em>räkna commits</em> eller <em>beskriv ändringarna</em>.</li></ul>
<p>När du väl internaliserat det här börjar du lägga märke till varje ställe där agenten var på väg att hitta på ett tal. <em>Det verkar finnas ungefär 200 poster...</em> — nej. Kör <code>SELECT count(*)</code>. <em>De flesta tester går igenom...</em> — nej. Kör testsviten, parsa JSON:en. Modellen är utmärkt på att konstruera kommandot. Den är opålitlig när det gäller att vara kommandot.</p>
<h2>Felmoder jag faktiskt råkat ut för</h2>
<p>Det här är inga hypotetiska exempel. Vart och ett av dessa kostade mig verklig tid innan jag ändrade mönstret:</p>
<ul><li><strong>Spökverifiering.</strong> Agenten sa <em>jag kontrollerade alla 14 sektioner mot mockupen</em>. Den öppnade inte mockupen. Den tog ingen skärmdump. Kontrollen var ett hallucinerat steg i narrativet.</li><li><strong>Säkra felaktiga tal.</strong> Frågade efter monthly active users från analysdata. Fick ett tal som var ~3× fel. Modellen interpolerade från exempelrader istället för att köra den faktiska queryn.</li><li><strong>Påhittade filändringar.</strong> Agenten sa <em>jag uppdaterade <code>config/feature-flags.json</code></em>. Det hade den inte gjort. Den hade bara haft för avsikt att göra det. <code>git diff</code> var tom.</li><li><strong>Fejkade testkörningar.</strong> <em>Alla tester går igenom.</em> Inga tester kördes. Agenten anropade aldrig test runner:n — den förutsåg hur test runner:ns utdata skulle ha sett ut.</li></ul>
<p>Alla fyra löses av samma två regler: dela agenten, skicka till shell. Reviewer:n har inte <code>Write</code>, så den kan inte fejk-redigera filer. Analytics-agenten har bara <code>Bash</code>, så den kan inte returnera ett tal som inte kom från en query. Strukturell omöjlighet slår goda intentioner varje gång.</p>
<h2>Hur man strukturerar detta i Claude Code</h2>
<p>Claude Code stödjer sub-agents definierade i <code>.claude/agents/*.md</code>. Varje agentfil deklarerar ett namn, en beskrivning, en tillåten verktygsuppsättning och en systemprompt. Orchestratorn (din huvudsession) dispatchar dem med verktyget <code>Agent</code>. Här är den typ av definition jag använder för reviewer:n — kort, smal och fysiskt oförmögen att skriva kod:</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>Lägg märke till verktygsuppsättningen: <code>Bash, Read, Grep, Glob</code>. Ingen <code>Write</code>, ingen <code>Edit</code>, ingen <code>Agent</code>. Reviewer:n kan köra kommandon, läsa filer, söka efter mönster — och ingenting annat. Om den försöker lura igenom en hallucinerad diff som <em>verifierad</em> gör formen på dess verktygsanrop det uppenbart: det gjordes inga riktiga kontroller. Du kan granska verktygsanropen och se exakt vad som inspekterades.</p>
<p>Orchestrerings-mönstret: huvudsessionen anropar Builder → väntar → kör <code>git diff</code> själv för att fånga den faktiska ändringen → anropar Reviewer med spec och diff → läser Reviewer:ns utlåtande. Huvudsessionen ber aldrig en agent göra bådas jobb. Verktygsbegränsningar är starkare än promptinstruktioner: <em>fejka inte verifieringen</em> är en önskan. Att inte ha <code>Write</code> är ett faktum.</p>
<h2>Anti-mönster att pensionera</h2>
<p>Saker jag ser i prompts som inte gör något — eller ännu värre, ger en falsk känsla av trygghet:</p>
<ul><li><strong><em>Var försiktig och dubbelkolla ditt arbete.</em></strong> Genererar inget ytterligare beteende. Modellen producerar redan det som ser ut som noggrant arbete.</li><li><strong><em>Se till att du faktiskt verifierar.</em></strong> Ordet <em>faktiskt</em> tillför ingen semantik som modellen kan agera på. Den kommer <em>faktiskt</em> hävda att den verifierade.</li><li><strong><em>Hallucinera inte.</em></strong> Ett prompt engineering-meme. Hallucination är inte en switch modellen kan stänga av.</li><li><strong>Lita på agenten med <em>små</em> tal.</strong> Det är på små tal den ljuger mest säkert. Det finns ingen ärlighets-tröskel.</li><li><strong>Lägga till fler regler i prompten för att <em>tvinga fram</em> ärlighet.</strong> Strukturella åtgärder (dela + shell) slår promptjusteringar varje gång. Om en regel behöver enforças, koda in den i verktygsåtkomst, inte i text.</li></ul>
<p>Om din strategi för att fånga hallucinationer är mer emfatiska formuleringar har du ingen strategi. Du har ett hopp.</p>
<h2>Den mentala modellen</h2>
<p>En agent är inte en kollega. Det är en funktion: <code>prompt → tokens</code>. Funktionen är utmärkt på att skriva kod och usel på att introspektera om den gjorde rätt sak. Behandla dess påståenden om sitt eget arbete som en hypotes. Diff:en, exit-koden, skärmdumpen, radantalet — det är bevisen. Sammanfattningen i slutet av ett drag är den mest lögn-benägna ytan i hela systemet.</p>
<p>Specialisering är din försäkring mot narrativt drift. Shell:en är din enda ground truth. Builder skriver. Reviewer kontrollerar. Bash avgör.</p>
<h2>Slutsats</h2>
<p>Om du ska minnas en sak: låt inte en enda agent både producera och bedöma sin egen utdata, och låt ingen agent svara på en kvantitativ fråga utan att köra ett kommando. Allt annat följer av dessa två regler. Konfigurera verktygsåtkomst aggressivt, granska verktygsanrop istället för sammanfattningar, och hallucinationsytan krymper från <em>överallt</em> till <em>ett fåtal specifika ställen du redan vet att titta på</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>Flera Claude Code-konton på en enhet</title>
      <link>https://oleksiimazurenko.dev/sv/blog/multiple-claude-accounts-one-device</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/multiple-claude-accounts-one-device</guid>
      <description>Hur du använder två (eller fler) Claude Code-konton parallellt — personligt och företag — med fullständig isolation med en enda miljövariabel.</description>
      <content:encoded><![CDATA[<p>Jag använder Claude Code dagligen — för personliga projekt och för jobbet. Problemet: det är två helt olika konton med olika OAuth-sessioner, olika CLAUDE.md-instruktioner, olika MCP-servrar och separerat projektminne. Så här kör jag dem parallellt på en enhet med ett enda shell-alias.</p>
<h2>Problemet</h2>
<p>Claude Code lagrar allt i <code>~/.claude</code> som standard — OAuth-token, konversationshistorik, global CLAUDE.md, projektminne, MCP-serverkonfigurationer och inställningar. Med två konton behöver du två helt separata världar:</p>
<ul><li><strong>Personligt konto:</strong> egen Max/Pro-prenumeration, personlig CLAUDE.md med dina preferenser, dina MCP-servrar (Obsidian, personliga verktyg)</li><li><strong>Företagskonto:</strong> företagsplan, arbets-CLAUDE.md med Jira/Slack-integrationsinstruktioner, företagets MCP-servrar</li><li><strong>Olika OAuth-sessioner:</strong> du kan inte vara inloggad på två konton i samma konfigurationskatalog</li><li><strong>Separerat projektminne:</strong> du vill inte att arbetskontext läcker in i personliga sessioner och vice versa</li></ul>
<p>Att logga ut och in varje gång du byter kontext är inte ett alternativ. Du förlorar sessionstillståndet, och det är helt enkelt smärtsamt.</p>
<h2>Lösningen: CLAUDE_CONFIG_DIR</h2>
<p>Claude Code respekterar en enda miljövariabel: <code>CLAUDE_CONFIG_DIR</code>. Sätt den till vilken sökväg som helst, och Claude använder den katalogen istället för <code>~/.claude</code> för allt — auth, historik, inställningar, minne. Hela setupen tar 60 sekunder.</p>
<h3>Steg 1: Skapa en andra konfigurationskatalog</h3>
<p>Välj ett namn som passar ditt användningsfall:</p>
<pre><code>mkdir ~/.claude-work</code></pre>
<p>Klart. Claude fyller den med nödvändig struktur vid första start.</p>
<h3>Steg 2: Autentisera det andra kontot</h3>
<p>Kör Claude en gång med den nya konfigurationskatalogen för att trigga OAuth-login:</p>
<pre><code>CLAUDE_CONFIG_DIR=~/.claude-work claude</code></pre>
<p>Webbläsaren öppnas. Logga in med ditt företagskonto. OAuth-token sparas i <code>~/.claude-work</code> — helt separat från din personliga session i <code>~/.claude</code>.</p>
<h3>Steg 3: Lägg till ett shell-alias</h3>
<p>Lägg till detta i din shell-konfiguration så du inte behöver komma ihåg variabeln:</p>
<pre><code>alias claude-work=&apos;CLAUDE_CONFIG_DIR=~/.claude-work claude&apos;</code></pre>
<p>Ladda om din shell:</p>
<pre><code>source ~/.zshrc</code></pre>
<h2>Vad du får</h2>
<p>Nu har du två helt isolerade Claude-miljöer:</p>
<ul><li><strong><code>claude</code></strong> — startar med personligt konto, personlig CLAUDE.md, personligt minne</li><li><strong><code>claude-work</code></strong> — startar med företagskonto, arbetsspecifik CLAUDE.md, separat minne</li><li><strong>Isolerad historik:</strong> arbetskonversationer stannar i arbetet, personliga stannar personliga</li><li><strong>Separata MCP-servrar:</strong> din personliga Obsidian vault MCP syns inte i arbetssessioner</li><li><strong>Oberoende inställningar:</strong> olika tillåtna verktyg, olika behörighetsnivåer, olika modellpreferenser per konto</li></ul>
<h2>Hur det fungerar under huven</h2>
<p>Konfigurationskatalogen är den enda sanningskällan för Claude Codes tillstånd. Här är vad som lever inuti varje:</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 kör <code>claude-work</code> läser Claude allt från <code>~/.claude-work</code>. Den vet inte att <code>~/.claude</code> existerar. De två instanserna är helt oberoende — du kan till och med köra dem samtidigt i olika terminalflikar.</p>
<h2>Skalning till N konton</h2>
<p>Mönstret utökas till valfritt antal konton. Frilansare med flera kunder? Lägg till fler 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>Varje alias får sin egen konfigurationskatalog, sin egen OAuth-session, sin egen CLAUDE.md med kundspecifika instruktioner.</p>
<h2>Praktiska tips</h2>
<ul><li><strong>Namnge kataloger tydligt:</strong> <code>~/.claude-work</code>, <code>~/.claude-clientname</code> — du tackar dig själv när det finns tre eller fyra</li><li><strong>Skriv skräddarsydd CLAUDE.md för varje:</strong> arbets-CLAUDE.md kan innehålla företagsspecifika instruktioner (Jira-tickets, Slack-kanaler, deployment-procedurer). Den personliga förblir mager.</li><li><strong>Olika MCP-servrar per konto:</strong> konfigurera arbetsverktyg (Jira MCP, Slack MCP, interna API:er) bara i arbetskonfigurationen. Håll din personliga ren.</li><li><strong>Kolla vilket konto som är aktivt:</strong> kör <code>claude config list</code> i en session om du är osäker — visar sökvägen till konfigurationskatalogen</li></ul>
<h2>Där den här lösningen inte räcker till</h2>
<p><code>CLAUDE_CONFIG_DIR</code> isolerar per <em>konto</em>, inte per <em>projekt</em>. Inuti en enskild profil ser Claude varje MCP-server du någonsin har registrerat för det kontot — tvärs över alla dina projekt. För rent personlig användning är det oftast okej. I det ögonblick du har flera produktionskritiska projekt under ett konto, särskilt i överlappande domäner som fakturering, adminverktyg eller infrastruktur, introducerar det en konkret risk mellan projekt: en AI-assistent kan anropa ett verktyg från projekt A medan den jobbar med projekt B, särskilt när båda exponerar liknande namngivna operationer.</p>
<p>Profilmönstret besvarar frågan <em>which account am I in?</em>. Det besvarar inte frågan <em>which project's tools should be active right now?</em>. För arbete med högre insatser, stapla ett andra isolationslager ovanpå kontouppdelningen:</p>
<ul><li><strong>En profil per produktionskritiskt projekt, inte bara per konto:</strong> istället för <code>~/.claude</code> och <code>~/.claude-work</code>, skapa <code>~/.claude-work-billing</code> och <code>~/.claude-work-admin</code>. Varje profil ser bara de MCP-servrar den faktiskt behöver.</li><li><strong>Projektspecifik MCP via <code>.mcp.json</code>:</strong> commit:a en <code>.mcp.json</code> i projektroten som listar bara det projektets MCP-servrar. Claude plockar upp dem när den startas från den katalogen. Håll din globala konfiguration minimal — bara universella verktyg (anteckningar, sökning), inga produktionsendpoints.</li><li><strong>Namnge MCP-servrar otvetydigt:</strong> undvik generiska namn som <code>admin</code>, <code>billing</code>, <code>mcp-server</code>. Prefixa med projektet: <code>acme_billing_prod</code>, <code>acme_admin_stage</code>. Ett beskrivande namn tvingar fram en paus när något är på väg att anropas från fel kontext.</li><li><strong>Granska varje MCP-verktygsanrop innan du godkänner:</strong> anrop som <code>*_create_*</code>, <code>*_delete_*</code>, <code>*_charge_*</code> förtjänar en medveten andra blick. Vilken hastighet du än vinner på rakt av automatiskt godkännande avdunstar första gången ett verktyg från fel projekt avfyras mot produktion.</li></ul>
<p>Den allmänna regeln: dela upp profiler aggressivt, håll produktionsklar MCP utanför default-profilen, och behandla överlapp i verktygsnamn mellan projekt som en smell värd att refaktorera.</p>
<h2>Slutsats</h2>
<p>En miljövariabel. Ett alias. Fullständig isolation mellan konton. Ingen logout/login-dans, inga konfigurationskonflikter, inget kontextläckage. Den typen av lösning som är nästan besvikande enkel — men det är precis det som gör den bra. Sätt upp en gång och tänk aldrig på det igen.</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>Hur man ansluter Claude Desktop till Obsidian — En resa genom 4 MCP-servrar</title>
      <link>https://oleksiimazurenko.dev/sv/blog/claude-obsidian-mcp-servers</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/claude-obsidian-mcp-servers</guid>
      <description>En verklig historia om att hitta ett stabilt sätt att automatisera refaktorering av Obsidian-vault via Claude. Vad som gick sönder, vad som fungerade, och varför VaultForge visade sig vara det enda fungerande alternativet.</description>
      <content:encoded><![CDATA[<p>Föreställ dig: du har 400+ anteckningar i Obsidian, samlade under åren. Allt ligger utspritt i vault-roten, koncept blandade med tekniska anteckningar, det finns dubbletter (<code>ideas.md</code> och en <code>ideas/</code>-mapp med 13 filer inuti), inget system. Du vill skapa ordning — bygga en ordentlig mapparkitektur, lägga till MOC-filer, organisera taggar. Att göra det manuellt är tråkigt och långsamt. Den logiska tanken: <strong>anslut Claude till Obsidian via MCP, låt AI göra refaktoreringen</strong>. Det visar sig — det är en väg genom ett minfält. Här är vad jag fick gå igenom för att nå en fungerande lösning.</p>
<h2>Vad är MCP och varför det inte är så enkelt</h2>
<p>MCP (Model Context Protocol) är ett öppet protokoll från Anthropic som låter Claude ansluta till externa verktyg och data. Principen är enkel: en lokal server körs, exponerar "verktyg" (tools), och Claude anropar dem under konversationen.</p>
<p>För Obsidian finns det teoretiskt gott om MCP-servrar. I praktiken — var och en har sina egna problem.</p>
<p><strong>Huvudproblemet med Obsidian-ekosystemet:</strong> Obsidian är en stängd applikation utan officiell MCP. Communityn fyllde gapet, men varje implementation går sin egen väg, och ingen har "officiellt godkännande".</p>
<h2>Försök 1: MarkusPfundstein/mcp-obsidian</h2>
<p>Första verktyget som dyker upp vid sökning. 3 400 stjärnor på GitHub, med i varje tutorial. Verkar vara ett säkert val.</p>
<p><strong>Hur det fungerar:</strong> Python-server baserad på Local REST API-pluginet i Obsidian. Servern kommunicerar med pluginet via HTTPS, pluginet utför operationer via Obsidian API.</p>
<h3>Vad som gick fel</h3>
<ul><li>Inte uppdaterat på 17 månader</li><li>85 öppna issues</li><li><strong>Inget <code>move</code>/<code>rename</code></strong> — bara read, write, append, delete</li><li>Local REST API har en dokumenterad bug för dataförlust: POST-endpoint kan tyst skriva över en fil vid append</li></ul>
<p>Inte lämpligt för refaktorering — vi behöver flytta filer och bevara länkar. Vi går vidare.</p>
<h2>Försök 2: aaronsb/obsidian-mcp-plugin</h2>
<p>Hittade ett alternativ som fungerar som ett <strong>nativt Obsidian-plugin</strong>. Det innebär direkt åtkomst till Obsidians interna API — backlinks, Dataview, länkgraf. Move via det nativa API:et uppdaterar alla wiki-länkar automatiskt, eftersom Obsidian hanterar detta själv.</p>
<h3>Installationssvårigheter</h3>
<ul><li>Pluginet finns <strong>inte i Obsidians officiella katalog</strong> (PR väntar med valideringsfel)</li><li>Måste installeras via <strong>BRAT</strong> (Beta Reviewers Auto-update Tool)</li><li>Claude Desktop accepterar inte Bearer token direkt via UI — tvingade aktivering av HTTPS i pluginet</li><li>Self-signed certifikat för localhost skapar förtroendeproblem</li></ul>
<p>Genom alla dessa lösningar lyckades jag äntligen ansluta det. Grundtest — <code>vault.move</code> skriver om <code>[[wikilinks]]</code>, fungerar som förväntat.</p>
<h3>Vad som gick fel i produktion</h3>
<p>När jag började med massrefaktorering (drag-and-drop av dussintals mappar i Obsidian + samtidiga MCP-operationer), <strong>hängde servern i 4+ minuter</strong>. Varför: pluginet körs <em>inuti</em> Obsidian. När Obsidian omindexerar tusentals filer efter en massiv strukturförändring, blockeras pluginet med det.</p>
<p>Slutsats: <strong>beroendet av en öppen Obsidian-instans och dess index är fatalt för massoperationer</strong>.</p>
<h2>Försök 3: @bitbonsai/mcpvault</h2>
<p>Logiskt — vi behöver en server som <strong>inte beror på Obsidian</strong>. Arbetar direkt med filer på disk. <code>@bitbonsai/mcpvault</code> — rekommenderas i många recensioner. Direkt filsystemåtkomst, enkel installation (<code>npx @bitbonsai/mcpvault@latest /path/to/vault</code>), 14 verktyg. Obsidian behöver inte ens vara öppet.</p>
<p><strong>Innan installation kontrollerade jag en kritisk sak</strong> — om wiki-länkarna uppdateras vid move. Hittade en användarrecension:</p>
<blockquote>Filesystem-anslutningen vet inte att den är i Obsidian — den ser en mapp med &lt;code&gt;.md&lt;/code&gt;-filer och det är allt. Vet inte att filnamn bär semantisk vikt, att varje &lt;code&gt;[[wikilink]]&lt;/code&gt; kommer att gå sönder i det ögonblick du byter namn eller flyttar. Auto-update links fungerar bara när namnbytet sker inifrån appen. Jag lärde mig detta efter att ha bett Claude rensa filnamn och kom tillbaka till en dashboard med hälften av länkarna trasiga.</blockquote>
<p>Bekräftat i mcpvaults egen dokumentation: PR #101 (wiki link resolution) är <strong>under review, inte mergad</strong>. Så att flytta via <code>mcpvault</code> skulle förstöra halva vaultet. Inte lämpligt.</p>
<h2>Försök 4: VaultForge (Final)</h2>
<p><code>blacksmithers/vaultforge</code> — specifikt byggt för AI-agenter som gör refaktorering.</p>
<h3>Arkitektoniskt korrekt</h3>
<ul><li><strong>Direkt filsystem</strong> — beror inte på Obsidian</li><li><strong>Egen wikilink-motor</strong> — implementerar <code>[[wikilink]]</code>-upplösningslogik som uppdaterar alla former (stem, fullständig sökväg, alias, embed)</li><li><strong>Dry run som standard</strong> på alla destruktiva operationer — visar först vad som ändras, sedan bekräftar du</li><li><strong>27 verktyg</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-licens</strong>, TypeScript, noll underberoenden</li><li><strong>Installation på 30 sekunder</strong> via <code>.mcpb</code> (one-click-tillägg för Claude Desktop)</li></ul>
<h3>Säkerhetstest på isolerade filer</h3>
<p>Skapade 4 testfiler med korslänkar — stem-länkar, länkar med alias, länkar med fullständig sökväg. Flyttar en fil till en undermapp:</p>
<pre><code>delta.md → subfolder/delta-renamed.md</code></pre>
<p>VaultForge visade en dry run: "1 fil kommer att döpas om, 3 länkar kommer att uppdateras". Kördes på riktigt.</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>Kontrollerade efteråt — <strong>alla tre länktyper uppdaterades korrekt</strong>. Det här är precis vad alla tidigare verktyg saknade.</p>
<h2>Hur man installerar VaultForge — Slutgiltig instruktion</h2>
<p>Om du har macOS och Claude Desktop:</p>
<h3>Steg 1</h3>
<p>Ladda ner <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 öppnar installationsdialogen för tillägg. Ange den <strong>absoluta sökvägen</strong> till ditt vault — inga backslashes, vanliga mellanslag:</p>
<pre><code>/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault</code></pre>
<h3>Steg 3</h3>
<p>Klicka på Save. Claude Desktop lägger till tillägget i konfigurationen automatiskt. <strong>Ingen omstart behövs</strong> — <code>.mcpb</code>-tillägg plockas upp automatiskt.</p>
<h3>Steg 4</h3>
<p>Verifiera: i en ny chatt fråga: <em>"What is the status of my Obsidian vault?"</em> — bör returnera något som <code>totalFiles: 416, totalDirs: 135, ...</code></p>
<h2>Vad jag lärde mig om Obsidians MCP-ekosystem</h2>
<p><strong>För det första: "mest populär" betyder inte "fungerar".</strong> MarkusPfundstein/mcp-obsidian har 3 400 stjärnor och är standardrekommendationen, men det är föråldrat och saknar viktiga operationer.</p>
<p><strong>För det andra: ett nativt plugin har en dold kostnad.</strong> Aaronsb-pluginet såg idealiskt ut — graph, Dataview, nativt move. Men beroendet av en körande Obsidian-instans och dess index gör det olämpligt för seriösa massoperationer.</p>
<p><strong>För det tredje: direkt filsystem utan link-engine är en fälla.</strong> Mcpvault är snabbt och enkelt, men "bara flytta filer" förstör vault-strukturen. Länkar bär <strong>påtvingad semantik</strong> som filsystemet inte känner till. Utan sin egen wikilink-logikimplementation blir verktyget en landmina.</p>
<p><strong>För det fjärde: testa på isolerade data.</strong> Innan du anförtror ett verktyg massrefaktorering — skapa en testmapp med 4–5 filer med korslänkar och se vad som händer. 5 minuters testning sparar timmar av återställning från backup.</p>
<p><strong>För det femte: behåll en git-backup av ditt vault.</strong> Det viktigaste av allt. Ett enda <code>git init</code> inuti vaultet och periodiska commits — det är försäkring mot alla misstag av en AI-agent eller ett verktyg. Om något går sönder — <code>git reset --hard</code> tar tillbaka allt.</p>
<h2>Slutsats</h2>
<p>Resan tog flera timmar och tre misslyckade försök. Den slutgiltiga arkitekturen ser ut så här:</p>
<ul><li><strong>VaultForge</strong> — huvudarbetsverktyget. Direkt filsystem + egen wikilink-motor + 27 verktyg = stabil refaktorering i alla skalor.</li><li><strong>Git</strong> — vault-versionshantering. Gratis rollback för alla misstag.</li></ul>
<p>Nu kan jag göra det som allt detta startades för: be Claude organisera 400 anteckningar i en ordentlig PARA-arkitektur, slå samman dubbletter, lägga till frontmatter, bygga MOC-kartor. Varje operation är säker, länkarna bevaras, dry run visar vad som kommer att hända innan något ändras.</p>
<p>Om du också tittar på ditt stökiga Obsidian och vill ha en AI-assistent — börja direkt med VaultForge. Upprepa inte min väg genom döda projekt, beta-plugins och filsystemservrar utan länklogik.</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>Svarta hål som rekursiva universum: från fysik till existensens syfte</title>
      <link>https://oleksiimazurenko.dev/sv/blog/black-holes-recursive-universes</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/black-holes-recursive-universes</guid>
      <description>Tänk om varje svart hål är en Big Bang för ett nytt universum? En utforskning av rekursiv kosmologi och kognitiv stängning.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Tänk om varje svart hål är en Big Bang för ett nytt universum? Den här artikeln utforskar idén att vårt universum kan vara en nod i ett oändligt rekursivt träd — där svarta hål föder sub-universum, energi cirkulerar tillbaka genom Hawkingstrålning, och fysikens grundläggande lagar medvetet är utformade för att göra kontakt mellan universum omöjlig.</p>
<h2>Svart hål = universum</h2>
<p>Idén kom under ett ögonblick av eftertanke: ett svart hål bildas när tillräckligt med massa och tryck koncentreras i en enda punkt. Den singulariteten — oändlig densitet, oändlig krökning — påminner misstänkt mycket om de förhållanden vi beskriver för Big Bang.</p>
<p>Tänk om det är samma händelse, betraktad från olika sidor? Utifrån ser vi ett svart hål som slukar materia. Inifrån — ett nytt universum som exploderar till existens. Massan och energin som kollapsade in i det svarta hålet blir råmaterialet för ett helt nytt kosmos med egna stjärnor, planeter och möjligen egna svarta hål.</p>
<blockquote>Varje svart hål i vårt universum kan innehålla ett universum. Och vårt universum kan existera inuti ett svart hål i ett föräldra-universum.</blockquote>
<h2>Varför universum inte kan kontakta varandra</h2>
<p>Här är den eleganta delen: när du väl passerat händelsehorisonten finns det ingen återvändo. Allmän relativitetsteori garanterar detta — föräldra-universumets framtid ligger helt utanför händelsehorisonten, oåtkomlig inifrån. Från sub-universumets perspektiv har föräldra-universumet redan tagit slut. Hela dess tidslinje har redan passerat.</p>
<p>Det här är inte en teknisk begränsning som vi kan övervinna med bättre teknik. Det är inbyggt i själva rumtidens geometri. Universum är fundamentalt isolerade från varandra — inte av avstånd, utan av tidens struktur.</p>
<h2>Energicykeln: att låna och återlämna</h2>
<p>Men energi går inte förlorad. Hawkingstrålning — den kvantprocess genom vilken svarta hål långsamt avdunstar — skapar ett anmärkningsvärt kretslopp:</p>
<ol><li>Ett föräldra-universum skapar ett svart hål och överför energi till ett sub-universum</li><li>Sub-universumet lever igenom hela sin livscykel under biljoner år</li><li>Det svarta hålet avdunstar långsamt och återlämnar energi till föräldra-universumet via Hawkingstrålning</li><li>Föräldra-universumet får tillbaka sin energi — med ränta</li></ol>
<p>Den "räntan" är fascinerande: fysiker tror nu att Hawkingstrålning bevarar information. Föräldra-universumet får inte bara tillbaka tom energi — det får ett avtryck av allt som hände inuti. Varje stjärna som bildades, varje planet, varje ögonblick av medvetande — kodat i strålning.</p>
<h2>Rekursion hela vägen ner</h2>
<p>Om du är programmerare är mönstret omöjligt att missa. Det här är rekursion. Varje universum anropar <code>universe()</code> med mindre energi och skapar sub-universum som skapar sub-sub-universum, tills det inte finns tillräckligt med energi för att bilda svarta hål — basfallet.</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>Fysikern Lee Smolin formaliserade en liknande idé som <strong>kosmologiskt naturligt urval</strong>: universum "reproducerar sig" genom svarta hål, och varje generation har lite annorlunda fysikaliska konstanter — optimerade genom otaliga cykler för att producera fler svarta hål, fler universum.</p>
<h2>Var befinner vi oss i denna cykel?</h2>
<p>Vårt universum är ungefär 13,8 miljarder år gammalt. Det låter urgammalt, men i förhållande till dess totala livslängd bevittnar vi den allra första början:</p>
<table><thead><tr><th>Händelse</th><th>Tidsskala</th></tr></thead><tbody><tr><td>Universumets nuvarande ålder</td><td>~10¹⁰ years</td></tr><tr><td>Stjärnor slutar bildas</td><td>~10¹⁴ years</td></tr><tr><td>De svarta hålens era</td><td>~10⁴⁰ years</td></tr><tr><td>Sista svarta hålet avdunstar</td><td>~10¹⁰⁰ years</td></tr></tbody></table>
<p>Vi existerar vid ungefär 0,00000000...01% av vårt universums totala livslängd. Stjärnornas era — allt vi kan se — är en kort blixt i den allra första början. Universumets verkliga berättelse är den långsamma, tålmodiga eran av svarta hål som skapar och avdunstar sub-universum.</p>
<h2>Frågan om högre dimensioner</h2>
<p>Allt som diskuterats hittills verkar inom vår tredimensionella förståelse. Men om vårt universum är ett "snitt" av något högre-dimensionellt, kan hela det rekursiva trädet av svarta hål och sub-universum bara vara en skugga av en struktur vi inte kan uppfatta.</p>
<p>År 1884 skrev Edwin Abbott <em>Flatland</em> — en berättelse om tvådimensionella varelser som inte kan föreställa sig en tredje dimension. En sfär som passerar genom Flatland framträder som en cirkel som växer och krymper. "Flatlänningarna" kan beskriva det matematiskt men aldrig verkligen förstå vad de ser. Vi kan befinna oss i exakt samma position i förhållande till vårt universum.</p>
<blockquote>Vad är medvetande? Varför finns subjektiv upplevelse? David Chalmers kallade detta det &quot;svåra problemet&quot; — och det kan vara det starkaste beviset för att något verkar bortom vår dimensionella räckvidd.</blockquote>
<h2>Allt är låst på den fundamentala nivån</h2>
<p>Den mest slående insikten är inte att vi inte vet — utan att vi <em>inte kan</em> veta. Varje undersökningsriktning stöter på en fundamental barriär:</p>
<ul><li><strong>Vill du se föräldra-universumet?</strong> Blockerat av händelsehorisonten</li><li><strong>Vill du förstå medvetandet?</strong> Blockerat — ett system kan inte fullt ut analysera sig självt (Gödels ofullständighetssatser)</li><li><strong>Vill du veta vad som var "innan"?</strong> Blockerat — tiden började med Big Bang</li><li><strong>Vill du uppfatta högre dimensioner?</strong> Blockerat av de kognitiva begränsningarna hos en tredimensionell varelse</li></ul>
<p>Filosofen Colin McGinn kallar detta <strong>kognitiv stängning</strong>: vissa frågor är stängda för det mänskliga sinnet, inte på grund av otillräckliga data, utan på grund av sinnets egen arkitektur. Skillnaden mellan "vi vet inte ännu" och "vi kan inte veta" är djupgående.</p>
<h2>Det enda som återstår: självförbättring</h2>
<p>Om varje utgång är blockerad avsiktligt — om du inte kan se utåt, inte kan se bakåt, inte kan se uppåt — finns det bara en riktning kvar: inåt. Universum verkar medvetet konstruerat för att tvinga fokus på jaget.</p>
<p>Den här slutsatsen kommer inte från religion eller filosofiläroböcker. Den kommer från att följa logiken i svarta hål, rekursion, informationsteori och kognitionens gränser. Existentialister, buddhister, stoiker och fysiker — alla anländer till samma punkt via olika vägar: existensens syfte kan helt enkelt vara förädlingen av den varelse som existerar.</p>
<blockquote>Vi kom hit inte genom tro, utan genom fysik — från svarta hål, genom rekursiva universum, till kunskapens fundamentala blockeringar, till den enda öppna dörren: att bli bättre.</blockquote>
<h2>Referenser</h2>
<ul><li><a href="https://en.wikipedia.org/wiki/Cosmological_natural_selection" target="_blank" rel="noopener">Lee Smolin — Kosmologiskt naturligt urval</a></li><li><a href="https://en.wikipedia.org/wiki/Hawking_radiation" target="_blank" rel="noopener">Stephen Hawking — Hawkingstrålning</a></li><li><a href="https://en.wikipedia.org/wiki/Hard_problem_of_consciousness" target="_blank" rel="noopener">David Chalmers — Medvetandets svåra problem</a></li><li><a href="https://en.wikipedia.org/wiki/Flatland" target="_blank" rel="noopener">Edwin Abbott — Flatland: En roman i många dimensioner (1884)</a></li><li><a href="https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems" target="_blank" rel="noopener">Kurt Gödel — Ofullständighetssatserna</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 dödade CMS — åtminstone för enkla webbplatser</title>
      <link>https://oleksiimazurenko.dev/sv/blog/ai-killed-cms-for-simple-sites</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/ai-killed-cms-for-simple-sites</guid>
      <description>Varför traditionella innehållshanteringssystem blir onödiga för portfolios, bloggar och landningssidor.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>För enkla webbplatser — portfolios, bloggar, landningssidor, småföretagssajter — håller traditionella CMS på att bli onödig overhead. AI-verktyg som Claude Code, Cursor och GitHub Copilot kan nu redigera din kodbas direkt, förstå kontext, översätta innehåll och driftsätta ändringar via git. Det abstraktionslager som CMS erbjöd ersätts av ett smartare gränssnitt: naturligt språk.</p>
<h2>CMS-skatten du betalar</h2>
<p>Varje CMS kommer med en dold kostnad. Inte bara prenumerationsavgiften — hela ekosystemet av komplexitet som lindas kring din i grunden enkla webbplats:</p>
<ul><li><strong>Infrastruktur:</strong> En databas att drifta, ett API att underhålla, en instrumentpanel att säkra. Enbart WordPress utgör ~43% av webben och ~90% av CMS-riktade attacker.</li><li><strong>Prestanda:</strong> Dynamisk sidgenerering, API-anrop vid varje begäran, klientsidig hydrering av CMS-data. Din portfolio med 3 sidor har nu arkitekturen hos en SaaS-produkt.</li><li><strong>Leverantörsinlåsning:</strong> Ditt innehåll lever i någon annans databasschema. Migrera från Contentful till Sanity? Det är ett projekt, inte en konfigurationsändring.</li><li><strong>Kontextväxling:</strong> Redigera kod i din IDE, byt sedan till en webbläsarbaserad CMS-instrumentpanel för att ändra en rubrik. Två olika mentala modeller för vad som i grunden är samma operation.</li><li><strong>Kostnad:</strong> Headless CMS-priser skalas ofta med API-anrop eller innehållsposter. En personlig blogg behöver inte en innehållsinfrastruktur för $99/månad.</li></ul>
<p>För en marknadsföringssajt där 50 personer redigerar innehåll dagligen är denna kostnad motiverad. För en utvecklarportfolio eller en småföretagslandningssida? Du bygger en bro över en vattenpöl.</p>
<h2>Vad som förändrades: AI förstår din kod</h2>
<p>Anledningen till att CMS existerade var enkel: icke-tekniska personer (och även utvecklare som inte ville röra kod för innehållsändringar) behövde ett visuellt gränssnitt för att uppdatera webbplatser. Koden var för komplex, för skör, för lätt att förstöra.</p>
<p>AI förändrade denna ekvation i grunden. Moderna AI-kodverktyg gör inte bara autokomplettering — de förstår projektstrukturen, läser befintliga mönster och gör kontextuellt korrekta redigeringar. Förändringen i arbetsflödet är 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>Det här är inte hypotetiskt. Den här bloggen körs på SolidStart med innehåll lagrat som TypeScript-filer. Varje artikel — inklusive denna — skapades genom att berätta för AI vad den ska skriva, granska resultatet och pusha till git. Ingen CMS-instrumentpanel. Ingen databas. Inget API-lager mellan innehållet och koden.</p>
<h2>Verkliga exempel från den här sajten</h2>
<p>Den här webbplatsen stödjer 10 språk, har en blogg, genererar OG-bilder dynamiskt och producerar RSS-flöden och sajtkarta. Så här ser innehållslagret 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>Saker jag gör med AI som traditionellt skulle kräva ett CMS:</p>
<ul><li><strong>Lägga till ett nytt blogginlägg:</strong> "Skriv en ny artikel om X, följ samma struktur som befintliga inlägg" — AI skapar filen, lägger till översättningar, registrerar den i indexet</li><li><strong>Uppdatera landningssidans text:</strong> "Ändra hero-rubriken till Y" — AI hittar rätt fil och uppdaterar den</li><li><strong>Översätta innehåll:</strong> "Lägg till tysk översättning för prissidan" — AI läser den engelska versionen och producerar en kulturellt anpassad översättning, inte en ordagrann kopia</li><li><strong>Rätta ett stavfel:</strong> "Det finns ett stavfel på about-sidan, 'recieve' ska vara 'receive'" — klart på 3 sekunder, committat till git med ett meningsfullt meddelande</li></ul>
<h2>Vad CMS faktiskt löste — och hur AI ersätter det</h2>
<p>Låt oss vara ärliga om vad CMS bidrog med och hur varje förmåga mappas till AI-arbetsflödet:</p>
<table><thead><tr><th>Problem</th><th>CMS-lösning</th><th>AI-lösning</th></tr></thead><tbody><tr><td>Icke-teknisk redigering</td><td>WYSIWYG-redigerare</td><td>Instruktioner på naturligt språk</td></tr><tr><td>Flerspråkigt innehåll</td><td>i18n-tillägg, locale-fält</td><td>AI översätter med kulturell kontext</td></tr><tr><td>Innehållsschemaläggning</td><td>Inbyggda publiceringsdatum</td><td>Git-baserad CI/CD med cron eller datumfält i koden</td></tr><tr><td>Versionshistorik</td><td>CMS revisionssystem</td><td>Git — guldstandarden för versionshantering</td></tr><tr><td>Mediahantering</td><td>Inbyggt resursbibliotek</td><td>CDN + git LFS eller molnlagring</td></tr></tbody></table>
<p>Den centrala insikten: git är redan ett bättre versionshanteringssystem än något CMS någonsin har byggt. Och naturligt språk är ett bättre gränssnitt än någon WYSIWYG-redigerare — för det bär avsikt, inte bara formatering.</p>
<h2>Paradigmskiftet: kod är innehållslagret</h2>
<p>Vi bevittnar en inversion. I två decennier var trenden att separera innehåll från kod — lägga innehållet i en databas, exponera det via API, rendera det i frontend. Det var vettigt när kod var svår att redigera och innehåll behövde vara tillgängligt för icke-utvecklare.</p>
<blockquote>AI gjorde inte CMS föråldrat genom att vara ett bättre CMS. Det gjorde CMS föråldrat genom att göra kod lika tillgänglig som en instrumentpanel.</blockquote>
<p>Utvecklingen av webbinnehållshantering följer en tydlig bana:</p>
<ol><li><strong>2000-talet:</strong> Monolitiska CMS (WordPress, Drupal) — innehåll och presentation kopplade i ett system</li><li><strong>2010-talet:</strong> Headless CMS (Contentful, Strapi) — innehåll separerat via API, renderat av frontend-ramverk</li><li><strong>2020-talet:</strong> Statiska sajt-generatorer + Markdown (Hugo, Astro) — innehåll som filer, byggs vid driftsättning</li><li><strong>2025+:</strong> Kod-som-innehåll + AI — innehållet lever i typad kod, AI är redigeringsgränssnittet</li></ol>
<h2>När du fortfarande behöver ett CMS</h2>
<p>Det här är inte ett "CMS är dött"-uttalande. CMS löser verkliga problem i stor skala. Du behöver fortfarande ett när:</p>
<ul><li><strong>Stora redaktionsteam:</strong> 10+ innehållsredaktörer som behöver rollbaserad åtkomst, godkännandeflöden och simultanredigering. Git merge-konflikter är inte en innehållsredaktörs problem att lösa.</li><li><strong>Högfrekvent innehåll:</strong> Nyhetssajter som publicerar 50+ artiklar per dag behöver optimerade redaktionspipelines, inte git-commits.</li><li><strong>Komplexa innehållsrelationer:</strong> E-handelskataloger med tusentals SKU:er, produktvarianter och dynamisk prissättning kräver strukturerade databaser.</li><li><strong>Regulatorisk efterlevnad:</strong> Branscher som kräver revisionsspår, innehållsgodkännandekedjor och lagstadgade granskningsprocesser behöver specialbyggda system.</li></ul>
<p>Gränsen är tydlig: om dina innehållsändringar kräver samordning mellan flera icke-tekniska intressenter med hög frekvens, förtjänar CMS sin komplexitet. Om du är en soloutvecklare, ett litet team, eller hanterar en sajt som ändras veckovis snarare än varje timme — är AI + kod enklare, snabbare, billigare och mer tillförlitligt.</p>
<h2>Framtiden: AI som det universella gränssnittet</h2>
<p>Trenden sträcker sig bortom CMS. Varje mjukvaruabstraktionslager som existerade för att "det underliggande systemet är för komplext för direkt interaktion" komprimeras av AI. Adminpaneler, konfigurationsgränssnitt, visuella databasredigerare — alla dessa är gränssnitt som översätter mänsklig avsikt till systemändringar. AI gör denna översättning inbyggt.</p>
<p>För enkla webbplatser är framtiden redan här. Ditt innehåll är kod. Din redigerare är AI. Din versionshantering är git. Din driftsättning är en push. Hela CMS-lagret — instrumentpanelen, databasen, API:et, hostingen — var mellanprogram mellan din avsikt och din webbplats. AI tog bort behovet av den mellanprogramvaran.</p>
<blockquote>Det bästa CMS:et är inget CMS. Inte för att innehållshantering inte spelar roll — utan för att AI gjorde koden själv till det mest intuitiva gränssnittet för innehållshantering vi någonsin haft.</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>Hur man ansluter Perplexity AI till Obsidian via MCP — Anteckningar direkt från chatten</title>
      <link>https://oleksiimazurenko.dev/sv/blog/perplexity-obsidian-mcp-integration</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/perplexity-obsidian-mcp-integration</guid>
      <description>Konfigurera Perplexity Desktop för att läsa och skriva ditt Obsidian-vault med MCP filesystem-server. Sök på webben och spara i dina anteckningar i en konversation.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Perplexity Desktop stöder <strong>MCP (Model Context Protocol)</strong>-anslutningar. Genom att lägga till den officiella <code>@modelcontextprotocol/server-filesystem</code>-servern som pekar på ditt Obsidian-vault kan du be Perplexity på naturligt språk att läsa, skapa och redigera anteckningar — direkt från chatten. Inga plugins, inga tillägg, ingen kopiering.</p>
<h2>Problemet</h2>
<p>Perplexity är utmärkt på forskning — det söker på webben, sammanfattar källor och ger svar med citat. Men när du vill spara dessa fynd i ditt Obsidian-vault bryts arbetsflödet: kopiera text, byta till Obsidian, hitta rätt anteckning, klistra in, formatera. Varje. Gång.</p>
<p>Webbläsartillägg som "Perplexity to Obsidian" hjälper med export, men de är enkelriktade — AI:n kan inte <em>se</em> ditt vault, kan inte läsa dina befintliga anteckningar och kan inte bestämma var saker ska placeras baserat på din mappstruktur.</p>
<h2>Vad är MCP?</h2>
<p><strong>Model Context Protocol (MCP)</strong> är en öppen standard som låter AI-modeller interagera med lokala verktyg och datakällor. Tänk på det som en USB-port för AI — du ansluter en "server" (ett litet program) och AI:n får nya förmågor. I vårt fall ger filesystem-servern Perplexity 14 verktyg för att arbeta 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>Nyckelpunkten: AI-modellen har inte direkt åtkomst till dina filer. Den anropar verktyg som tillhandahålls av MCP-servern, som körs lokalt på din maskin. Dina data lämnar aldrig din dator om du inte uttryckligen ber AI:n att göra något med dem.</p>
<h2>Krav</h2>
<ul><li><strong>Perplexity Pro</strong>-prenumeration (MCP-anslutningar är tillgängliga för betalande användare)</li><li><strong>Perplexity Mac App</strong> från App Store (inte webbläsarversionen)</li><li><strong>Node.js</strong> installerat på din Mac (för att <code>npx</code> ska fungera)</li></ul>
<h2>Steg-för-steg-installation</h2>
<p>Hela installationen tar cirka 2 minuter:</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>Kommandot</h3>
<p>Kommandot att klistra in i <strong>Command</strong>-fältet:</p>
<pre><code>npx -y @modelcontextprotocol/server-filesystem &quot;/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault&quot;</code></pre>
<p>Ersätt sökvägen med den faktiska platsen för ditt Obsidian-vault. Om ditt vault synkroniseras via iCloud kommer sökvägen att vara under <code>~/Library/Mobile Documents/iCloud~md~obsidian/Documents/</code>. Se till att behålla citattecknen — sökvägen innehåller troligen mellanslag.</p>
<h2>Hur man använder det</h2>
<p>När anslutningen visar <strong>Running</strong> med 14 tillgängliga verktyg, gå till valfri Perplexity-chatt och börja prata med ditt vault:</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:n förstår din vault-struktur, respekterar dina formateringskonventioner och kan arbeta med befintligt innehåll. Du kan be den att undersöka ett ämne på webben och spara sammanfattningen direkt i en specifik anteckning.</p>
<h2>Varför MCP är bättre än andra metoder</h2>
<p>Före MCP fanns det begränsade sätt att koppla ihop Perplexity och Obsidian:</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>Nuvarande begränsningar</h2>
<ul><li><strong>Endast Mac</strong> — Perplexitys MCP-anslutningar fungerar för närvarande bara i Mac App Store-versionen</li><li><strong>Ingen Obsidian API-integration</strong> — filesystem-servern arbetar med råfiler, inte genom Obsidians API. Det innebär att den inte utlöser Obsidian-plugins (Linter, Templater) vid filskapande</li><li><strong>Godkännande krävs</strong> — känsliga filoperationer kan kräva din bekräftelse i Perplexity-appen — det är en säkerhetsfunktion, inte en bugg</li></ul>
<h2>Slutsatser</h2>
<p>Denna installation förvandlar Perplexity från ett forskningsverktyg till ett forsknings-och-fångstverktyg:</p>
<ol><li>Sök på webben och spara i Obsidian i en konversation</li><li>AI:n ser din vault-struktur och anpassar sig till ditt organisationssystem</li><li>Noll appbyten — allt sker i Perplexity-chatten</li></ol>
<h2>Källor</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 och Obsidian — Noll beroenden</title>
      <link>https://oleksiimazurenko.dev/sv/blog/ai-news-digest-claude-code-obsidian</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/ai-news-digest-claude-code-obsidian</guid>
      <description>Hur jag byggde en daglig nyhetsforskningsagent med ett 6-radigt bash-skript, Claude Code headless-läge och macOS launchd.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Ett 6-radigt bash-skript som kör <strong>Claude Code CLI</strong> i headless-läge varje morgon kl 9:00. Det söker nyheter inom 11 konfigurerbara ämnen, filtrerar brus och skriver en formaterad markdown-digest direkt till en <strong>Obsidian vault</strong> synkad via iCloud. Noll beroenden. ~100 rader konfiguration totalt.</p>
<h2>Problemet</h2>
<p>Som utvecklare är det en daglig skatt att hålla sig uppdaterad. RSS-flöden är brusiga, Twitter är en tidstjuv, nyhetsbrev kommer när man är djupt fokuserad. Jag behövde något som gör research <em>åt</em> mig.</p>
<p>Den typiska lösningen är att bygga en scraping-pipeline: schemaläggare, crawler, NLP-pipeline, databas, notifieringstjänst. Det är veckors arbete. Jag ville ha något som kan byggas på en eftermiddag.</p>
<h2>Arkitektur</h2>
<p>Hela systemet är 4 filer och noll beroenden:</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 (hela)</h2>
<p>Projektet är avsiktligt minimalt.</p>
<h3>Startpunkt: digest.sh</h3>
<p>Hela applikationen är ett 6-radigt 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>Viktiga flaggor: <code>-p</code> kör Claude i headless-läge, <code>--max-turns 20</code> ger agenten tillräckligt med svängar, <code>--allowedTools</code> begränsar agenten till läsning, sökning och skrivning.</p>
<h3>Hjärnan: prompt.md</h3>
<p>Här lever intelligensen. Prompten förvandlar Claude till en nyhetsresearch-agent:</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>Konfiguration: topics.yaml</h3>
<p>Ämnen är helt konfigurerbara — lägg till ett nytt ämne och det finns i morgondagens 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>Schemaläggning med launchd</h2>
<p>På macOS är <code>launchd</code> det nativa sättet att schemalägga återkommande uppgifter:</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>Installera med <code>launchctl load ~/Library/LaunchAgents/com.news-digest.plist</code>. Skriptet körs dagligen kl 9:00.</p>
<h2>Hur resultatet ser ut</h2>
<p>Varje morgon dyker en ny markdown-fil upp 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 siffror</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>Viktiga designbeslut</h2>
<ul><li><strong>Claude Code CLI istället för API</strong> — inget behov av att hantera API-nycklar eller HTTP-klienter</li><li><strong>Obsidian istället för e-post</strong> — digests är sökbara, länkbara och permanenta</li><li><strong>launchd istället för cron</strong> — macOS-nativ schemaläggare med hantering av missade körningar</li><li><strong>YAML för ämnen</strong> — nytt ämne = 2-raders ändring</li><li><strong>Hoppa över tomma ämnen</strong> — inga nyheter = ingen sektion</li></ul>
<h2>Bygg din egen</h2>
<p>Klart på 10 minuter:</p>
<ol><li>Installera <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer">Claude Code CLI</a> och autentisera</li><li>Klona repot: <code>git clone https://github.com/oleksiimazurenko/news-digest</code></li><li>Redigera <code>topics.yaml</code> och <code>prompt.md</code></li><li>Redigera plist-filen och <code>launchctl load</code></li><li>Vänta till 9:00 — eller testa manuellt med <code>bash digest.sh</code></li></ol>
<h2>Slutsatser</h2>
<p>Det mest intressanta med detta projekt är vad som <em>inte</em> finns i det. Ingen databas, ingen API-server, ingen Docker, inga npm-paket, ingen Python, ingen HTML-parser, ingen NLP-pipeline.</p>
<p>Så ser det ut att bygga med AI-agenter: du definierar <em>vad</em> och <em>var</em>, agenten hanterar <em>hur</em>. Total utvecklingstid: cirka 2 timmar.</p>
<h2>Källor</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 utan blinkning eller React 19-varningar</title>
      <link>https://oleksiimazurenko.dev/sv/blog/nextjs-dark-mode-without-flash</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/nextjs-dark-mode-without-flash</guid>
      <description>Hur man ersätter next-themes med Zustand + useServerInsertedHTML för flimmerfritt mörkt läge i Next.js 15+.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p><code>next-themes</code> renderar en <code>&lt;script&gt;</code>-tagg inuti en React Client Component för att förhindra temablixt (FOUC). React 19 varnar för detta. Biblioteket har inte uppdaterats sedan mars 2025. Lösning: Zustand-store + <code>useServerInsertedHTML</code>. Noll beroenden, noll FOUC, noll varningar.</p>
<h2>Problemet</h2>
<p>Om du använder <code>next-themes</code> med Next.js 15+ och React 19 får du det här konsolfelet vid varje sidladdning:</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 hydreringsmismatch. React 19 varnar om att <code>&lt;script&gt;</code>-taggar i klientkomponenter <strong>aldrig kommer att köras</strong>. Skriptet fungerar i SSR men React flaggar det.</p>
<h2>Varför det händer</h2>
<p><code>next-themes</code> sätter temaklass innan hydrering genom att injicera ett 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 ändrade beteendet — script-taggar i komponenter flaggas nu. <code>suppressHydrationWarning</code> hjälper inte för detta.</p>
<h2>Vad vi försökte (och varför det misslyckades)</h2>
<p>Vi prövade systematiskt varje metod:</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> injicerar HTML i SSR-strömmen <strong>utanför React-trädet</strong>. Skriptet finns i HTML men React "ser" det inte på klienten. Kombinerat med Zustand för reaktivt tematillstånd — fullständig ersättning, noll beroenden.</p>
<h3>Hur det fungerar</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 hanterar tematillstånd, DOM-klasser, systemdetektering och synkronisering mellan flikar. <code>_init()</code> returnerar en cleanup-funktion för <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>Providern injicerar FOUC-förebyggande skript via <code>useServerInsertedHTML</code> och initialiserar Zustand-storen vid 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: Använd 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>Migration från next-themes</h2>
<p>API:et är avsiktligt identiskt. Migrering är en enda importändring 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>Jämförelse</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>Varför inte andra alternativ?</h2>
<h3>@wrksz/themes</h3>
<p>Drop-in-ersättning med <code>useServerInsertedHTML</code>. Fungerar, men ytterligare ett beroende med en enda underhållare. Om <code>next-themes</code> lärde oss något — beroenden överges. Med ~100 rader kod äger du hela lösningen.</p>
<h3>next-themes@1.0.0-beta.0</h3>
<p>Finns på npm, inget releasedatum, ingen ändringslogg, oklart om React 19-varningen är åtgärdad. Inte värt att satsa produktionskod på.</p>
<h3>Bara CSS (prefers-color-scheme)</h3>
<p>Fungerar för systemdetektering, men kan inte hantera localStorage-persistens, manuell växling eller alternativet "system". JavaScript behövs.</p>
<h2>Slutsatser</h2>
<ol><li><code>next-themes</code> är i praktiken övergiven — senaste utgåvan mars 2025, React 19-varningen ej åtgärdad</li><li><code>useServerInsertedHTML</code> är rätt Next.js-primitiv för skriptinjektion utan React-varningar</li><li>Zustand ger reaktivt tematillstånd med mindre kod än en Context-provider</li><li>Hela lösningen ~100 rader, noll nya beroenden, du äger varje rad</li></ol>
<h2>Källor</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>Hur jag eliminerade 94 renderingsblockerande CSS-filer i Next.js 16 med en dåligt dokumenterad Turbopack-funktion</title>
      <link>https://oleksiimazurenko.dev/sv/blog/eliminating-render-blocking-css</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/eliminating-render-blocking-css</guid>
      <description>Efter dagar av att prova varje tillvägagångssätt — från experimental.inlineCss till MutationObserver-hack — upptäckte jag Turbopack Import Attributes som löser problemet med renderingsblockerande CSS i Next.js App Router.</description>
      <content:encoded><![CDATA[<h2>Problemet</h2>
<p>Vår app (<a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a>) använder Next.js 16 med en <strong>Landing Builder</strong> — ett CMS-drivet system som sätter ihop marknadsföringssidor från ~90 olika sektionskomponenter. Arkitekturen använder en <code>sectionRegistry.tsx</code> som mappar sektionsnamn till <code>next/dynamic()</code>-anrop:</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 enda landningssida renderar bara <strong>5-8 sektioner</strong>. Men Lighthouse visade:</p>
<pre><code>Eliminate render-blocking resources
  94 CSS resources (~330 KB)
  Potential savings: 5,440 ms</code></pre>
<p><strong>Varför?</strong> Turbopack ser alla 90 <code>import()</code>-sökvägar som nåbara och genererar <code>&lt;link rel="stylesheet"&gt;</code> för <strong>varje</strong> SCSS-modul. Även sektioner som aldrig renderas på sidan får sin CSS injicerad i <code>&lt;head&gt;</code>. Detta är ett <a href="https://github.com/vercel/next.js/issues/62485" target="_blank" rel="noopener noreferrer">bekräftat, förväntat beteende</a> i Next.js App Router. <strong>Ingen fix planeras.</strong></p>
<h2>Allt jag försökte (och varför det misslyckades)</h2>
<p>Jag spenderade dagar på att gå igenom varje tillvägagångssätt jag kunde hitta. Här är hela listan:</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-fällan</h3>
<p>Next.js har en <code>experimental.inlineCss</code>-flagga som ersätter alla <code>&lt;link rel="stylesheet"&gt;</code> med inline <code>&lt;style&gt;</code>-taggar. Låter perfekt, eller hur?</p>
<p>Problemet: det är <strong>allt eller inget</strong>. Du kan inte aktivera det per rutt. Om du har SSR-sidor (<code>force-dynamic</code>) bygger varje begäran om all CSS inline. Vi försökte — vår headless CMS klarade inte lasten.</p>
<h2>Upptäckten: Turbopack Import Attributes</h2>
<p>Genom att gräva i <a href="https://nextjs.org/blog/next-16-2-turbopack#inline-loader-configuration" target="_blank" rel="noopener noreferrer">Next.js 16.2-releasenotes</a> hittade jag en dåligt dokumenterad funktion: <strong>Turbopack Import Attributes</strong>. Den låter dig åsidosätta den inbyggda bundler-pipelinen för en specifik import med TC39 <code>with {}</code>-syntax:</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>Detta säger till Turbopack: <em>"Bearbeta inte denna import som en stilmall. Kör den genom min anpassade loader och behandla utdatan som JavaScript."</em></p>
<p><strong>Detta är den avgörande insikten.</strong> Istället för att Turbopack genererar ett <code>&lt;link rel="stylesheet"&gt;</code> som blockerar rendering, kompilerar vår loader SCSS och exporterar det som en JS-sträng. Resultat: <strong>bara CSS för sektioner som faktiskt renderas hamnar i sidans HTML</strong>.</p>
<h2>Lösningen</h2>
<h3>1. Anpassad Turbopack Loader</h3>
<p>Ett ~70-radigt Node.js-skript som yarn workspace-paket (<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>Vad den gör: <strong><code>styles</code></strong> — samma scopade klassnamnskarta som standard CSS Modules. <strong><code>cssText</code></strong> — kompilerad CSS som sträng.</p>
<h3>2. InlineStyle-komponent</h3>
<p>Använder React 19s inbyggda <code>&lt;style href precedence&gt;</code>-API för automatisk deduplicering:</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 garanterar: samma <code>href</code> → bara en <code>&lt;style&gt;</code> i DOM.</p>
<h3>3. Migration per komponent (~6 rader per sektion)</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>-filerna förblir exakt desamma.</strong> Ingen CSS-omskrivning.</p>
<h2>Varför detta är bättre än inlineCss: true</h2>
<p>Här är den kritiska skillnaden:</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 sida fortfarande ALLA 94 stilmallar inline. Med vår metod hamnar <strong>bara CSS som faktiskt renderas i HTML:en</strong>.</p>
<h2>Turbopack-fällan: inga globala regler för .module.scss</h2>
<p>En fälla jag föll i: du kanske tror att du kan lägga till en Turbopack-regel i <code>next.config.ts</code> för att tillämpa loadern 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>Gör inte detta.</strong> Turbopacks inbyggda CSS-module-pipeline fångar <code>.module.scss</code>-filer <strong>innan</strong> anpassade regler tillämpas, vilket orsakar:</p>
<pre><code>FATAL PANIC: inner asset should be CSS processable</code></pre>
<p><code>with {}</code>-attribut fungerar eftersom de instruerar Turbopack <strong>vid import-platsen</strong> att helt kringgå CSS-module-pipelinen.</p>
<h2>Resultat</h2>
<p>127 sektionskomponenter migrerade i Landing Builder. Produktionsbygge verifierat.</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>Begränsningar</h2>
<ul><li><strong><code>with {}</code> per import är mångsidigt</strong> — varje import behöver 3 extra rader.</li><li><strong>Bara Turbopack</strong> — <code>with {}</code>-attribut stöds inte av Webpack.</li><li><strong>Klassnamns-hashning</strong> — vår loader använder en annan hashningsalgoritm än Turbopacks inbyggda.</li><li><strong>HTML-storlek ökar</strong> — CSS är inline i HTML istället för cachade separata filer.</li></ul>
<h2>När man ska använda detta</h2>
<p>Denna teknik är mest effektiv när:</p>
<ol><li><strong>Du har ett registry/barrel-mönster</strong> — en fil importerar många komponenter, men bara några få renderas per sida</li><li><strong>Du använder Turbopack</strong> — Import Attributes är Turbopack-specifika</li><li><strong>Du vill ha kontroll per komponent</strong> — inte en allt-eller-inget-flagga</li><li><strong>Din SCSS är komplex</strong> — variabler, mixins, breakpoints, nästling — allt stöds</li><li><strong>Du inte kan använda <code>experimental.inlineCss</code></strong> — för att du har SSR-sidor eller vill ha granulär kontroll</li></ol>
<h2>Relaterade GitHub Issues</h2>
<p>Om du påverkas av renderingsblockerande CSS i Next.js App Router — du är inte ensam:</p>
<h3>Kärnproblemet</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-funktionen &amp; problem</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>Communityn söker lösningar</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-buggar</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>Byggt på <a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a> — en språkinlärningsplattform som betjänar miljontals användare.</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-läge — 2 år utan fix</title>
      <link>https://oleksiimazurenko.dev/sv/blog/nextjs-memory-leak-fetch-standalone</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/sv/blog/nextjs-memory-leak-fetch-standalone</guid>
      <description>Next.js patchar den globala fetch och lägger till ett cache-lager som läcker minne vid varje förfrågan. I Docker/K8s leder detta till OOM-krascher var några timmar. Buggen har funnits sedan Next.js 14 och är fortfarande olöst i 16.2.x.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Next.js patchar den globala <code>fetch</code> och lägger till ett cache-lager som håller referenser till svarsdata efter att de borde ha frigjorts. Varje <code>fetch</code>-anrop lägger till minne som aldrig returneras till GC. I Docker/Kubernetes leder detta till OOM-krascher var några timmar. Buggen har funnits sedan Next.js 14 (april 2024) och är fortfarande olöst i 16.2.x (mars 2026). På Vercel visar sig problemet inte tack vare efemära serverless-funktioner.</p>
<h2>Hur normal fetch fungerar</h2>
<pre><code>Request → fetch → got data → response to user → GC cleans up → memory free</code></pre>
<h2>Hur fetch fungerar i Next.js</h2>
<p>Next.js fångar upp den globala <code>fetch</code> och omsluter den med sitt eget cache-/tracking-lager:</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>Vad som händer i produktion</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>Drabbade versioner</h2>
<h3>Next.js — Alla versioner 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>Testat på Node.js 20, 22, 24, 25 — läcker på alla.</p>
<h2>Vad som inte fungerar</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>Vad som fungerar (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>Begränsning med axios-workaround</h3>
<p>Dina egna API-anrop kan ersättas med axios. Men Next.js använder internt den patchade fetch för:</p>
<ul><li>ISR (Incremental Static Regeneration)</li><li><code>revalidatePath</code> / <code>revalidateTag</code></li><li>Server Components data fetching med deduplicering</li><li><code>use cache</code> (Next.js 16)</li></ul>
<p>Även utan en enda <code>fetch</code> i din kod — använder Next.js det fortfarande internt.</p>
<h2>Varför Vercel inte fixar det</h2>
<h3>Affärslogik</h3>
<p>Vercel är ett företag som tjänar pengar på att hosta Next.js. På deras plattform visar sig problemet inte (serverless = efemärt). Buggen påverkar bara self-hosted (Docker, K8s, VPS) — de som inte betalar Vercel.</p>
<h3>Officiell position</h3>
<p>Tim Neutkens (Vercel-maintainer) analyserade problemet och förklarade att det är ett <strong>undici</strong>-problem (Node.js fetch-bibliotek), inte Next.js. Issue #90433 stängdes. Trots att:</p>
<ul><li>axios och node-fetch på samma Node.js fungerar utan läckor</li><li>Läckan bara uppstår när fetch går genom Next.js-wrappern</li><li>Buggen har varit öppen i 2 år utan fix</li></ul>
<h3>Prioriteringar</h3>
<p>Under dessa 2 år har Next.js-teamet släppt:</p>
<ul><li>Turbopack (2-5x snabbare builds) — marknadsföringsfördel</li><li>Cache Components / <code>use cache</code> — minskar belastningen på Vercel-servrar</li><li><code>proxy.ts</code> istället för middleware — förenklar edge-deployment på Vercel</li><li>DevTools MCP — AI-hype</li></ul>
<p>Memory leak i self-hosted? Inte en prioritet.</p>
<h2>Lösning: AWS Lambda (SST + OpenNext)</h2>
<h3>Vad är det</h3>
<p>OpenNext är en open-source-adapter som konverterar en Next.js-build till ett format för AWS Lambda. SST är ett ramverk som automatiserar 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>Varför detta löser minnesläckan</h3>
<p>Lambda-funktioner bearbetar förfrågningar och återvinns efter 5-15 minuters inaktivitet. Minnet hinner inte ackumuleras.</p>
<h3>Deployment</h3>
<pre><code>npx sst@latest init
npx sst deploy --stage production</code></pre>
<h3>Jämförelse</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örsta förfrågan är långsammare (~200-500ms)</li><li><strong>Säkerhet</strong> — aktivera OAC (Origin Access Control), annars är Lambda-URL:en publik</li><li><strong>OpenNext</strong> — community-projekt, inte officiellt från Vercel. Nya Next.js-funktioner kan gå sönder</li><li><strong>Plånboksattack</strong> — vid DDoS kan Lambda auto-scaling leda till en stor faktura</li></ul>
<h2>Varför en riktig fix är orealistisk</h2>
<h3>1. Arkitekturproblem</h3>
<p>Läckan är inte en slumpmässig bugg utan en konsekvens av ett designbeslut: Next.js fångar upp den globala <code>fetch</code> och lägger till cache/tracking ovanpå. För att fixa det behöver sättet App Router interagerar med fetch designas om. Detta påverkar ISR, revalidation, data cache, request deduplication — ramverkets kärna.</p>
<h3>2. Intressekonflikt</h3>
<p>Vercel är inte motiverat att fixa det som inte påverkar deras plattform. Self-hosted konkurrerar med deras affär. Ju fler problem i self-hosted — desto fler migrerar till Vercel.</p>
<h3>3. Skuldförskjutning</h3>
<p>Den officiella positionen är "det är undici, inte vi". Tills det ändras — kommer de inte arbeta på en fix.</p>
<h3>4. Ingen community-fix</h3>
<p>Next.js AGPL-3.0-licens tillåter forks, men kodbasen är enorm och tätt kopplad till Vercel-infrastrukturen. En community-PR för att fixa fetch-wrappern skulle kräva djup förståelse av den interna arkitekturen och godkännande från maintainers — som redan stängt issuen.</p>
<h2>Slutsatser</h2>
<ol><li><strong>Om du är på Vercel</strong> — inget problem, inget att göra</li><li><strong>Om self-hosted och behöver serverless</strong> — SST + OpenNext på AWS Lambda</li><li><strong>Om self-hosted Docker</strong> — ersätt fetch med axios där det är möjligt, övervaka RAM, konfigurera automatisk pod-omstart</li><li><strong>Om du startar ett nytt projekt</strong> — överväg SvelteKit eller Nuxt som alternativ utan detta problem</li></ol>
<h2>Källor</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>