<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>OXIDT</title>
<link>https://oxidt.com/blog</link>
<description>I’m Hauke, a solo developer building SaaS with Rust. I write about Dioxus, self-hosting, and the AI tools I use to ship.</description>
<language>en</language>
<atom:link href="https://oxidt.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
<lastBuildDate>Wed, 26 Aug 2026 14:38:02 +0000</lastBuildDate>
<item>
<title>Self-Hosting Honcho for Hermes Agent: Long-Term Memory Without Another SaaS</title>
<link>https://oxidt.com/blog/self-hosting-honcho-hermes-agent-memory</link>
<guid isPermaLink="true">https://oxidt.com/blog/self-hosting-honcho-hermes-agent-memory</guid>
<pubDate>Wed, 26 Aug 2026 14:38:02 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>I connected Hermes Agent to a self-hosted Honcho instance so preferences, project context, and useful history survive across conversations.</description>
<category>ai-agents</category>
<category>developer-tools</category>
<category>hermes-agent</category>
<category>honcho</category>
<category>self-hosting</category>
<content:encoded><![CDATA[<p>My agent remembered the current chat perfectly and forgot everything useful the moment I opened a new one. I wanted memory that survived the tab, without handing another service my entire working history.</p>
<p>So I connected Hermes Agent to a self-hosted Honcho instance.</p>
<p>This isn’t about giving an AI a giant transcript and hoping it finds the right paragraph. Honcho stores conversations, builds representations of users and agents, and retrieves relevant context later. Hermes then injects that context into a new conversation, or queries it through memory tools. What you get is closer to continuity than to storage.</p>
<p>It remembers that I build Rust products with Dioxus and Axum, that I prefer direct answers, and that a given project has its own deployment convention. Those facts stay useful long after the original conversation has disappeared into session history.</p>
<p>That’s the promise, anyway. Setting it up also taught me that an enabled memory provider isn’t necessarily a working one. Authentication breaks quietly, background processing dies without telling you, and a status command will happily print <code>OK</code> while the useful part does precisely nothing.</p>
<p>So here’s the setup, the architecture, and the traps I’d avoid the second time.</p>
<h2 id="why-ordinary-chat-history-isn-t-enough">Why ordinary chat history isn’t enough<a class="heading-anchor" href="#why-ordinary-chat-history-isn-t-enough" aria-hidden="true" tabindex="-1">#</a></h2>
<p>A conversation has short-term memory by default. The model sees the messages in its current context window and refers back to them. That works fine until the conversation gets compacted, or I start a new session, or I switch from the desktop app to another interface. It also breaks down when the useful fact lives in a conversation from three weeks ago, or when the history is too large to inject without burying the current task.</p>
<p>Saving every transcript solves storage. It doesn’t solve recall.</p>
<p>An agent needs to answer two different questions:</p>
<ol>
<li>What happened in that old conversation?</li>
<li>What from that conversation still matters now?</li>
</ol>
<p>The first is search. The second takes judgment.</p>
<p>Hermes already has local memory files and searchable session history, and Honcho sits beside those rather than replacing them. It adds a longer-lived model of the user and the agent, plus semantic search and reasoning over past conversations.</p>
<p>I like that split. A small explicit memory file is still the right home for durable facts I want to control by hand. Session search is still what I reach for when I need the exact conversation where a decision got made. Honcho earns its place in the fuzzy middle: preferences, recurring patterns, shifting project context, and the facts that quietly became important without anyone bothering to promote them into a file.</p>
<h2 id="what-honcho-actually-runs">What Honcho actually runs<a class="heading-anchor" href="#what-honcho-actually-runs" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The self-hosted stack isn’t one magical memory container. The official Compose setup runs four services:</p>
<ul>
<li><strong>api</strong>, which accepts peers, sessions, messages, and queries on port 8000</li>
<li><strong>deriver</strong>, the background worker that turns raw conversation into observations, summaries, and peer representations</li>
<li><strong>database</strong>, PostgreSQL with pgvector, holding durable state and embeddings</li>
<li><strong>redis</strong>, for caching and background coordination</li>
</ul>
<p>The deriver matters more than it first appears. If the API is healthy but the deriver isn’t processing, your messages still get stored while the memory layer never becomes useful. Congratulations, you’ve built a very elaborate chat archive.</p>
<p>Honcho also needs an LLM provider and won’t start without one, since it uses models for extraction, summaries, dialectic queries, and memory consolidation. Worth being clear-eyed about what that means: self-hosting the database doesn’t keep all your conversation content on your machine. If Honcho calls a hosted model, the relevant content still leaves your server for inference.</p>
<p>Which gives you three useful deployment categories.</p>
<h3 id="managed-honcho">Managed Honcho<a class="heading-anchor" href="#managed-honcho" aria-hidden="true" tabindex="-1">#</a></h3>
<p>The least work, since Plastic Labs runs both the service and the memory models. This is where I’d start if I only wanted to find out whether persistent memory improves my workflow.</p>
<h3 id="self-hosted-storage-hosted-inference">Self-hosted storage, hosted inference<a class="heading-anchor" href="#self-hosted-storage-hosted-inference" aria-hidden="true" tabindex="-1">#</a></h3>
<p>The database, vectors, and memory state live on infrastructure I control, while Honcho still calls an external model provider for extraction and reasoning. This is the practical middle ground for most developers, and it’s what I run. You get control over the durable memory without standing up a local model server.</p>
<h3 id="fully-local">Fully local<a class="heading-anchor" href="#fully-local" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Honcho and the inference models both run on your own machines. Strongest privacy boundary, largest operational bill, paid in electricity, hardware, and evenings lost to model-server configuration. Pick it because the data genuinely requires it.</p>
<h2 id="starting-honcho-with-docker">Starting Honcho with Docker<a class="heading-anchor" href="#starting-honcho-with-docker" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The upstream repository ships the Compose setup:</p>
<pre><code>git clone https://github.com/plastic-labs/honcho.git
cd honcho
cp .env.template .env
cp docker-compose.yml.example docker-compose.yml
</code></pre>
<p>Your <code>.env</code> needs credentials for whichever model provider Honcho will use. The current upstream default expects <code>LLM_OPENAI_API_KEY</code> with an OpenAI-compatible text and embedding model, and you can point the Deriver, Dialectic, Summary, and Dream features somewhere else through the <code>*_MODEL_CONFIG__TRANSPORT</code>, <code>*_MODEL_CONFIG__MODEL</code>, and base URL overrides. Check the current docs before copying model names out of an old tutorial, because this part moves quickly.</p>
<p>Then build and start it:</p>
<pre><code>docker compose up -d --build
</code></pre>
<p>Don’t skip <code>--build</code>. The Compose file builds Honcho from source, so the first run takes a while.</p>
<p>Once it’s up, check the API and the background worker separately:</p>
<pre><code>docker compose ps
curl http://localhost:8000/health
docker compose logs deriver --tail 20
</code></pre>
<p>Don’t stop at the health endpoint. The upstream docs say plainly that it “only confirms the process is running. It does not check database or LLM connectivity.” It also tells you nothing about whether migrations finished, whether embeddings work, or whether Hermes can authenticate.</p>
<p>All Compose ports bind to <code>127.0.0.1</code> by default, which is the right call. If Honcho only ever runs on the same machine as Hermes, leave it there. For a remote agent machine, put it behind HTTPS and authentication instead of hanging port 8000 out on the internet.</p>
<p>My own instance doesn’t run from that Compose file. It runs as a Coolify service behind HTTPS on a private domain, with the <code>api</code> and <code>deriver</code> containers on the <code>ghcr.io/plastic-labs/honcho:latest</code> image. That’s where the next section comes from: the moment you put Honcho behind a domain you have to turn authentication on, and that’s where things got interesting.</p>
<h2 id="the-authentication-trap">The authentication trap<a class="heading-anchor" href="#the-authentication-trap" aria-hidden="true" tabindex="-1">#</a></h2>
<p>This was the annoying part, and it has three separate layers.</p>
<h3 id="authentication-is-off-by-default">Authentication is off by default<a class="heading-anchor" href="#authentication-is-off-by-default" aria-hidden="true" tabindex="-1">#</a></h3>
<p>A fresh self-hosted Honcho ships with <code>AUTH_USE_AUTH=false</code>, so every request is accepted. Fine on localhost. Not fine the moment the API has a public hostname.</p>
<p>Turning it on takes two steps. First, generate the secret:</p>
<pre><code>python scripts/generate_jwt_secret.py   # produces AUTH_JWT_SECRET
</code></pre>
<p>Set <code>AUTH_USE_AUTH=true</code> and that secret in <code>.env</code>, then mint a token the server will actually accept. Inside a running container:</p>
<pre><code>docker exec &lt;api-container&gt; /app/.venv/bin/python \
  scripts/generate_jwt.py --admin --print-only
</code></pre>
<p>To revoke everything you’ve issued, rotate <code>AUTH_JWT_SECRET</code> and restart.</p>
<h3 id="cloud-keys-aren-t-self-hosted-keys">Cloud keys aren’t self-hosted keys<a class="heading-anchor" href="#cloud-keys-aren-t-self-hosted-keys" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Honcho Cloud API keys and self-hosted tokens are different animals. A managed key starts with <code>hch-v2-</code>. A self-hosted server with auth enabled wants a JWT signed with <em>your</em> <code>AUTH_JWT_SECRET</code>. Paste a cloud key from <code>app.honcho.dev</code> into a self-hosted config and you get:</p>
<pre><code>{"detail": "Invalid JWT"}
</code></pre>
<p>It’ll never validate, no matter how many times you re-copy it.</p>
<h3 id="two-upstream-bugs-in-the-token-script">Two upstream bugs in the token script<a class="heading-anchor" href="#two-upstream-bugs-in-the-token-script" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Both of these cost me time, and neither one is documented.</p>
<p>The first: <code>uv run</code> doesn’t work inside the container. The obvious invocation fails.</p>
<pre><code>$ docker exec &lt;api-container&gt; uv run python scripts/generate_jwt.py --admin
error: honcho-ai references a workspace ... not a workspace member
</code></pre>
<p>Call the virtualenv interpreter directly instead, at <code>/app/.venv/bin/python</code>.</p>
<p>The second one is nastier: the <code>--expires</code> flag mints tokens the server then rejects. It writes the JWT <code>exp</code> claim as an ISO 8601 string where the spec wants a numeric timestamp, so the server refuses its own freshly issued token.</p>
<pre><code>401 Unauthorized
{"detail": "Invalid JWT"}
</code></pre>
<p>Note that the error text is identical to the wrong-key case, which is exactly what made it slow to diagnose. Mint without <code>--expires</code>, and rotate the secret when you want to revoke.</p>
<h3 id="verify-the-token-server-side-first">Verify the token server-side first<a class="heading-anchor" href="#verify-the-token-server-side-first" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Before touching any client config, prove the token works against the API on its own:</p>
<pre><code>curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}' \
  https://honcho.example.com/v3/workspaces/list
</code></pre>
<p>A <code>200</code> with a JSON page object means the credential is good. Anything else is a server or token problem, and no amount of client configuration will fix it. That one command separates “my token is wrong” from “my integration is wrong,” which are the two failures that look completely identical from inside a chat client.</p>
<h2 id="connecting-hermes-agent">Connecting Hermes Agent<a class="heading-anchor" href="#connecting-hermes-agent" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Hermes has a first-party Honcho memory provider, and the supported path is the CLI wizard:</p>
<pre><code>hermes memory setup honcho
</code></pre>
<p>Drop the provider name if you’d rather have the picker. For a local instance, give it the server base URL:</p>
<pre><code>http://localhost:8000
</code></pre>
<p>For a remote deployment, use its HTTPS base URL.</p>
<h3 id="the-v3-distinction">The <code>/v3</code> distinction<a class="heading-anchor" href="#the-v3-distinction" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Hermes wants the server base URL. The Honcho Claude Code plugin wants the versioned API endpoint. Same server, two different strings:</p>
<pre><code>Hermes:            https://honcho.example.com
Claude Code:       https://honcho.example.com/v3
</code></pre>
<p>That bit me while connecting the second client. Similar-looking settings aren’t interchangeable just because they eventually talk to the same API.</p>
<p>Then check what Hermes actually resolved:</p>
<pre><code>hermes memory status
</code></pre>
<p>There’s no <code>hermes honcho</code> subcommand, in case you go looking for one. <code>hermes memory</code> takes <code>setup</code>, <code>status</code>, <code>off</code>, and <code>reset</code>, and <code>status</code> is where the provider, resolved host, workspace, peers, and connection result show up.</p>
<p>My starting shape:</p>
<pre><code>workspace:          hermes
user peer:          hauke
AI peer:            hermes
session strategy:   per-directory
recall mode:        hybrid
</code></pre>
<p>Session strategy also accepts <code>per-repo</code>, <code>per-session</code>, and <code>global</code>. I use <code>per-directory</code> because it keeps a stable session mapping for work done from the same folder, which matches how I actually work: one project, one directory, one accumulating thread of context.</p>
<p><code>hybrid</code> gives Hermes both automatic context injection and the explicit Honcho tools. That’s a sensible default for a personal agent, since you get useful standing context without needing the model to remember to search every time, and the tools are still there when a task needs deeper recall.</p>
<p>One warning about reading the output. A status command can end with <code>OK</code> while the connection detail printed just above it says <code>Invalid JWT</code>. Read the connection block, not the final line. That’s the whole difference between checking that configuration exists and checking that it works.</p>
<h2 id="memory-gets-built-asynchronously">Memory gets built asynchronously<a class="heading-anchor" href="#memory-gets-built-asynchronously" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Honcho won’t hand you a mature profile right after the first message. Hermes writes conversations asynchronously, Honcho’s deriver processes them in the background, and peer cards, conclusions, and summaries all need messages to observe first. A brand new system will connect perfectly and still return nothing useful.</p>
<p>That’s not a failure. It’s just empty.</p>
<p>The distinction I use:</p>
<ul>
<li><strong>Connection failure:</strong> invalid token, unreachable endpoint, API error</li>
<li><strong>Processing failure:</strong> messages arrive, but the deriver or the model calls fail</li>
<li><strong>Fresh memory:</strong> connection works, there just isn’t enough history yet</li>
<li><strong>Working recall:</strong> a new session retrieves a fact from an older one</li>
</ul>
<p>Only that last one proves the feature I care about. The best smoke test is deliberately boring:</p>
<ol>
<li>Tell Hermes one harmless, specific preference.</li>
<li>End the session.</li>
<li>Start a fresh session.</li>
<li>Ask Hermes to retrieve it with <code>honcho_search</code>.</li>
</ol>
<p>Don’t test with something that already lives in <code>USER.md</code>, <code>MEMORY.md</code>, a repo instruction file, or the current prompt, or you’ll get a correct answer for the wrong reason.</p>
<h2 id="how-hermes-uses-the-memory">How Hermes uses the memory<a class="heading-anchor" href="#how-hermes-uses-the-memory" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Hermes runs Honcho in three recall modes.</p>
<p><strong><code>hybrid</code></strong> injects standing context automatically and hands the model the Honcho tools. It’s the default, and mine. Continuity without turning every message into a manual search.</p>
<p><strong><code>context</code></strong> injects context but withholds the tools. Simpler and more predictable, at the cost of deeper on-demand recall mid-task.</p>
<p><strong><code>tools</code></strong> injects nothing and lets the model call Honcho when it decides it needs memory. Keeps prompts lean and makes retrieval visible, but it leans on the agent noticing that history matters in the first place.</p>
<p>Hermes exposes four tools, and they do genuinely different jobs:</p>
<table><thead><tr><th>Tool</th><th>Layer</th><th>Use it when</th></tr></thead><tbody>
<tr><td><code>honcho_profile</code></td><td>Peer card</td><td>You want cheap standing context about who this person is</td></tr>
<tr><td><code>honcho_search</code></td><td>Semantic search</td><td>You need the actual excerpt, like the exact command we settled on</td></tr>
<tr><td><code>honcho_context</code></td><td>Dialectic Q&amp;A</td><td>You want a synthesized answer, like how I prefer technical feedback</td></tr>
<tr><td><code>honcho_conclude</code></td><td>Durable facts</td><td>Something learned this session should outlive it</td></tr>
</tbody></table>
<p>If I need the exact command we agreed on, that’s search. If I want a synthesis of a preference, <code>honcho_context</code> fits better. And if I need one stable fact available cheaply in every single conversation, it belongs in the profile or in an explicit memory file, not in a retrieval call.</p>
<h2 id="sharing-one-backend-with-claude-code">Sharing one backend with Claude Code<a class="heading-anchor" href="#sharing-one-backend-with-claude-code" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Yes, you can, and this turned out to be the most useful part of the whole setup.</p>
<p>Honcho ships a Claude Code plugin that talks to the same self-hosted backend. It inherits nothing from your Hermes configuration. It reads <code>~/.honcho/config.json</code>, and the workspace and peer names are yours to pick:</p>
<pre><code>{
  "apiKey": "&lt;your self-hosted JWT&gt;",
  "peerName": "hauke",              // shared user identity
  "hosts": {
    "claude_code": {
      "workspace": "hermes",        // NOT the default "claude_code"
      "aiPeer": "claude"
    }
  },
  "endpoint": {
    "baseUrl": "https://honcho.example.com/v3"
  },
  "sessionStrategy": "per-directory"
}
</code></pre>
<p>Two details do the real work here.</p>
<p>The workspace has to match, and the defaults won’t do it for you. The plugin defaults to a <code>claude_code</code> workspace, while Hermes writes to whatever you named during its setup. Leave both at their defaults and you end up with two perfectly healthy integrations that share absolutely nothing. I pointed Claude Code at the <code>hermes</code> workspace on purpose, and that single line is what makes the memory common.</p>
<p>Then there’s <code>observationMode</code>, which decides whether the agents stay distinct. The default, <code>unified</code>, pools observations together. Set it to <code>directional</code> and each AI peer keeps its own view of you, so Claude’s observations stay with Claude and Hermes’ stay with Hermes:</p>
<pre><code>{ "observationMode": "directional" }
</code></pre>
<p>That produces the architecture I wanted:</p>
<pre><code>                    ┌── Hermes AI peer
Hauke user peer ────┤
                    └── Claude AI peer

          inside one deliberate workspace
</code></pre>
<p>One shared user identity, a separate representation per agent. Common facts about me travel between them without pretending Hermes and Claude are the same thing.</p>
<p>Two smaller things, each of which cost me a restart. The plugin reads <code>apiKey</code> from the config file, and while <code>HONCHO_API_KEY</code> works too, changing it means restarting Claude Code. And the endpoint here needs the <code>/v3</code> suffix that Hermes specifically doesn’t want.</p>
<p>One last thing: don’t claim two agents share memory until you’ve written a fact through one and read it back through the other. Matching JSON files aren’t a test.</p>
<h2 id="what-i-d-do-differently">What I’d do differently<a class="heading-anchor" href="#what-i-d-do-differently" aria-hidden="true" tabindex="-1">#</a></h2>
<p>I’d verify the system in layers, instead of configuring the whole thing and then asking why it remembers nothing. My order now:</p>
<ol>
<li>Start PostgreSQL, Redis, the API, and the deriver.</li>
<li>Check container health and deriver logs.</li>
<li>Verify the Honcho API locally over plain HTTP.</li>
<li>Add HTTPS and <code>AUTH_USE_AUTH=true</code>, but only if remote access is actually required.</li>
<li>Mint a token and prove it with <code>curl</code> against <code>/v3/workspaces/list</code>.</li>
<li>Only then configure Hermes with the server base URL.</li>
<li>Read the full connection block from <code>hermes memory status</code>.</li>
<li>Write one harmless test fact.</li>
<li>Retrieve it from a fresh session.</li>
<li>Add the second client, matching the workspace name deliberately.</li>
<li>Only then start tuning recall modes, reasoning depth, and observation mode.</li>
</ol>
<p>This keeps each failure small. Every step I skipped on the first attempt is a step that later cost me an hour of guessing which layer was broken.</p>
<p>I’d also start with managed Honcho if I were evaluating the idea rather than the infrastructure. Self-hosting before proving the memory improves my work would be classic developer behavior: building a small distributed system to avoid filling out one signup form.</p>
<h2 id="is-self-hosting-worth-it">Is self-hosting worth it?<a class="heading-anchor" href="#is-self-hosting-worth-it" aria-hidden="true" tabindex="-1">#</a></h2>
<p>For most people trying Hermes, not on day one.</p>
<p>Managed Honcho takes the API, database, vector extension, worker, model configuration, authentication, backups, and upgrades out of the experiment entirely. That’s a much cleaner way to answer the first question, which is simply whether long-term agent memory helps you at all.</p>
<p>Self-hosting starts making sense when:</p>
<ul>
<li>conversation history is sensitive</li>
<li>you want control over retention and backups</li>
<li>multiple agents should share one memory backend</li>
<li>you already operate the infrastructure it needs</li>
<li>you want to pick the inference provider</li>
<li>the system has become valuable enough to own</li>
</ul>
<p>That third one was my actual reason. Two agents, one memory, one identity.</p>
<p>The tradeoff is straightforward enough. I get control over durable memory, and I take on responsibility for keeping the memory pipeline alive.</p>
<p>That last word is the one that matters. Persistent AI memory isn’t a JSON file with better branding, it’s a pipeline: ingestion, storage, background extraction, embeddings, retrieval, context assembly, and model inference. Every stage fails independently, and most of them fail quietly.</p>
<p>But when it works, the agent stops feeling like a clever stranger I have to brief every morning. It remembers the kind of software I build, the conventions around my projects, and the way I like to work. Not perfectly, and not magically. Just enough that the next conversation starts somewhere better than zero.</p>
<p>That’s worth a little infrastructure.</p>
<p>Just maybe not before coffee.</p>
<h2 id="references">References<a class="heading-anchor" href="#references" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li><a href="https://github.com/plastic-labs/honcho?ref=oxidt.com">Honcho source and self-hosting overview</a></li>
<li><a href="https://docs.honcho.dev/v3/contributing/self-hosting?ref=oxidt.com">Official Honcho self-hosting guide</a></li>
<li><a href="https://docs.honcho.dev/v3/guides/integrations/hermes?ref=oxidt.com">Honcho integration guide for Hermes Agent</a></li>
<li><a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/honcho?ref=oxidt.com">Hermes Agent Honcho memory documentation</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>SEO from the terminal: how I use claude-code-seo for my side projects</title>
<link>https://oxidt.com/blog/seo-from-the-terminal-claude-code</link>
<guid isPermaLink="true">https://oxidt.com/blog/seo-from-the-terminal-claude-code</guid>
<pubDate>Fri, 17 Jul 2026 23:18:10 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>My whole SEO workflow runs as slash commands in Claude Code, using a repo I did not write. The loop, my adaptations, and the AI-search submission checklist that is not in the repo.</description>
<category>ai-search</category>
<category>build-in-public</category>
<category>claude</category>
<category>seo</category>
<category>solo-dev</category>
<content:encoded><![CDATA[<p>I run several small products next to everything else. SeggWat, Stepshots, infra.page, this blog.</p>
<p>None of them can afford an SEO agency. None of them can afford much of my attention either.</p>
<p>SEO tools always lost me at the same point: a dashboard, a subscription, and a score that says my site is a 34 without telling me what to publish on Tuesday.</p>
<p>What finally stuck was a git repo.</p>
<h2 id="the-repo">The repo<a class="heading-anchor" href="#the-repo" aria-hidden="true" tabindex="-1">#</a></h2>
<p>First, credit where it belongs: <a href="https://github.com/mxfschr/claude-code-seo?ref=oxidt.com">claude-code-seo</a> is not my work. mxfschr distilled years of r/SEO advice, mostly u/WebLinkr’s, into a markdown knowledge base and nine slash commands for Claude Code. I just use it, and adapted a few edges for my setup.</p>
<p>The idea is simple and a little radical: SEO as something you execute, not something you read about.</p>
<pre><code>/seo-analyze mydomain.com
</code></pre>
<p>That pulls 90 days of my Google Search Console data over MCP, works out the hardest keywords I already rank top-7 for, and hands me three to five topics that extend that authority. With slugs, titles, and an internal linking plan.</p>
<p>Notice what is missing: a domain rating. The bar for “can this page rank” is what my pages already proved, not what a third-party score guesses.</p>
<p>Two details made it stick where the dashboards did not.</p>
<p><strong>It hands me a plan.</strong> <code>/seo-analyze</code> returns three to five candidates with titles, slugs, and internal-link wiring. <code>/seo-next-content</code> turns the one I approve into a publish-ready brief.</p>
<p><strong>It is honest.</strong> When there are no quick wins, it prints “0 quick wins” instead of inventing some. When a ranking only exists in a country I do not sell to, it says so. And before a content brief may claim a product feature, <code>/seo-product-facts</code> reads the actual code and cites file and line. No invented features, which matters more than ever now that LLMs repeat whatever you publish.</p>
<p>Running it costs almost nothing. The GSC API is free, DataForSEO is prepaid pay-per-call, and my weekly runs land between zero and eighteen cents.</p>
<h2 id="my-cadence">My cadence<a class="heading-anchor" href="#my-cadence" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li><strong>Weekly:</strong> <code>/seo-quick-wins</code>, which finds pages at position 4 to 15 that need exactly one push.</li>
<li><strong>Per topic I green-light:</strong> <code>/seo-next-content</code> for a publish-ready brief.</li>
<li><strong>Every six weeks or so:</strong> <code>/seo-analyze</code> re-maps where the authority is now.</li>
</ul>
<p>I also avoid touching the same page twice within seven days. Search Console needs time to register each change, and stacked edits make cause and effect unreadable.</p>
<h2 id="what-i-keep-outside-the-terminal">What I keep outside the terminal<a class="heading-anchor" href="#what-i-keep-outside-the-terminal" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The Search Console MCP stays because the commands need GSC data. URL submission is separate. For ordinary pages, I submit the sitemap and use Search Console’s URL Inspection tool to request indexing on the URLs that matter.</p>
<p>Google’s <a href="https://developers.google.com/search/apis/indexing-api/v3/quickstart?ref=oxidt.com">Indexing API</a> only supports pages with <code>JobPosting</code> or livestream <code>BroadcastEvent</code> markup. It is not a general URL-push API for blog posts, so I do not keep a separate indexing MCP server in the config. The terminal analyzes; Search Console handles manual requests.</p>
<h2 id="the-checklist-that-is-not-in-the-repo">The checklist that is not in the repo<a class="heading-anchor" href="#the-checklist-that-is-not-in-the-repo" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The repo is Google-first, and that is correct: Search Console is where the feedback loop lives.</p>
<p>But my audience is developers, and developers increasingly ask ChatGPT or Claude before they ever type into Google. Those products use their own crawlers and external search partners, so Google cannot be my only indexing check.</p>
<p><strong>1. Google Search Console.</strong> The baseline. Submit the sitemap, and for pages that matter, request indexing on the URL directly. Nothing new here.</p>
<p><strong>2. Bing Webmaster Tools, with IndexNow.</strong> OpenAI says ChatGPT Search sometimes works with external search providers and <a href="https://help.openai.com/en/articles/9237897-chatgpt-search?ref=oxidt.com">lists Bing among them</a>. OpenAI also runs OAI-SearchBot, so Bing is not the whole pipe. It still matters. <a href="https://blogs.bing.com/webmaster/June-2025/Start-Using-Bing-Webmaster-Tools-to-Improve-Your-Site-Visibility?ref=oxidt.com">Bing Webmaster Tools</a> can import verified sites from GSC. Then set up IndexNow: it immediately notifies Bing and the <a href="https://www.indexnow.org/en_gb/index?ref=oxidt.com">other participating engines</a> when a URL changes. This blog <a href="https://oxidt.com/ping-search-engines-on-every-deploy-with-indexnow/">pings IndexNow on every deploy</a>, straight from the GitHub Actions pipeline. Most developers never open Bing. That habit is now backwards.</p>
<p><strong>3. Brave.</strong> Anthropic tells site owners that Claude web search uses <a href="https://support.claude.com/en/articles/10684638-reporting-blocking-and-removing-content-from-claude?ref=oxidt.com">search partners</a>, and its government web-search connector explicitly <a href="https://support.claude.com/en/articles/14503775-mcp-web-search?ref=oxidt.com">calls the Brave Search API</a>. That does not prove every commercial Claude search comes solely from Brave. It is enough for me to treat Brave’s independent index as another discovery surface. <a href="https://search.brave.com/submit-url?ref=oxidt.com">Submitting a URL</a> takes a minute.</p>
<p>Ten minutes of submissions, total. If your users ask AI assistants before they search, those ten minutes might matter more than the difference between position 3 and 4 on Google.</p>
<p>Two more things I do that are less checklist and more habit:</p>
<p><strong>Ship open source, and let the platforms rank for you.</strong> github.com, npmjs.com, crates.io, docs.rs: these are some of the strongest domains on the internet, and every real project you publish gets a page on them. Those pages rank for your product name and your problem space long before your own young domain does, they link back to you, and LLMs read and cite them constantly. This is not a link scheme, and it does not work as one. It works when the repo or crate is a real thing people star and use. But if you were going to open source it anyway, publish it properly: good README, clear problem statement, link to your site.</p>
<p><strong>Show the workflow instead of describing it.</strong> For tutorials, I record step-by-step demos with <a href="https://stepshots.com/?ref=oxidt.com">Stepshots</a>, which is my own project, so calibrate for bias. The reason it belongs in an SEO post: tutorial pages that show every click answer the “how do I actually do this” queries that assistants and searchers both reward, and readers stay long enough to finish them.</p>
<h2 id="what-to-expect">What to expect<a class="heading-anchor" href="#what-to-expect" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Indexing is admission, not ranking. Submitting a URL across these discovery surfaces gets you evaluated, nothing more.</p>
<p>The repo’s own expectation management is the most honest I have seen in SEO material: new pages index in one to two weeks, reach position 10 to 15 in four to eight weeks if the internal linking is actually executed, and get their real evaluation over about thirteen months of click data. The maintainer took a zero-authority domain to page one in its niche with this exact loop, and still writes “there is almost certainly still some dumb-ass shit in here” at the top of the README.</p>
<p>That sentence is why I trust the repo. SEO compounds slowly, and the only way to survive that is a loop that runs on real data and admits what it does not know.</p>
<p>If you know SEO better than I do, the maintainer explicitly asks you to tear the repo apart. Open an issue. I will keep running the loop in the meantime: publish into proven authority, push what is almost there, and let GSC tell me what actually worked.</p>
]]></content:encoded>
</item>
<item>
<title>Ping search engines on every deploy with IndexNow</title>
<link>https://oxidt.com/blog/ping-search-engines-on-every-deploy-with-indexnow</link>
<guid isPermaLink="true">https://oxidt.com/blog/ping-search-engines-on-every-deploy-with-indexnow</guid>
<pubDate>Thu, 18 Jun 2026 12:00:00 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>We wired IndexNow into our GitHub Actions pipeline so Bing and friends recrawl the moment a release goes live. Here&apos;s the whole setup: the Rust side, the CI side, and the tradeoffs.</description>
<category>ci-cd</category>
<category>devops</category>
<category>github-actions</category>
<category>rust</category>
<category>seo</category>
<content:encoded><![CDATA[<p>You ship a release. The new pages are live. Then you wait. Sometimes for days. Search engines eventually wander back and notice something changed.</p>
<p>That waiting is dumb, and there’s a fix for it. IndexNow lets you tell search engines “this URL changed, come look now” with a single HTTP POST. We added it to both SeggWat and infra.page and tied it to our deploy pipeline, so every release pings the search engines automatically. It took an afternoon. This post is the whole thing.</p>
<h2 id="what-indexnow-actually-is-and-isn-t">What IndexNow actually is, and isn’t<a class="heading-anchor" href="#what-indexnow-actually-is-and-isn-t" aria-hidden="true" tabindex="-1">#</a></h2>
<p>IndexNow is a tiny open protocol. You POST a list of URLs to one endpoint, and it fans the submission out to every participating search engine: Bing, Yandex, Seznam, Naver. One request, everyone hears about it.</p>
<p>Be honest with yourself about the “everyone,” though: Google is not on that list. Google has its own opinions about crawl scheduling and ignores IndexNow. So this isn’t a Google play. It’s a “get indexed fast on the other half of search” play, which is still worth an afternoon if you’re a small product trying to show up anywhere at all.</p>
<p>There are two pieces to it:</p>
<ol>
<li>Proof you own the domain. You serve a key at <code>https://yoursite.com/{key}.txt</code> that returns the key as plaintext. That’s how IndexNow knows you’re allowed to submit URLs for that host.</li>
<li>The submission itself. A POST with the list of URLs you want recrawled.</li>
</ol>
<p>Most tutorials show you doing both by hand, once. We wanted it to be automatic and zero-maintenance. Here’s how each piece works.</p>
<h2 id="piece-1-serve-the-key-file-don-t-commit-it">Piece 1: serve the key file, don’t commit it<a class="heading-anchor" href="#piece-1-serve-the-key-file-don-t-commit-it" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The lazy way is to drop a static <code>key.txt</code> in your <code>public/</code> folder and forget about it. Don’t. That puts your key in the repo, ships it to every environment including local dev, and means the file exists whether or not the feature is actually configured.</p>
<p>Instead, serve it from the app, gated on a secret. The route only exists when the key is set, and the route path <em>is</em> the key. The handler just echoes it back:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Serve the IndexNow key file at /{key}.txt.
</span><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> The route is only registered when the key is configured, and its path
</span><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> literally is the key, so any request that reaches this handler returns
</span><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> that exact key as plaintext, which is all IndexNow wants.
</span><span class="hl-storage hl-modifier hl-rust">pub</span> async <span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-rust"><span class="hl-storage hl-type hl-function hl-rust">fn</span> </span><span class="hl-entity hl-name hl-function hl-rust">indexnow_key</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-begin hl-rust">(</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-end hl-rust">)</span></span></span></span><span class="hl-meta hl-function hl-rust"> <span class="hl-meta hl-function hl-return-type hl-rust"><span class="hl-punctuation hl-separator hl-rust">-&gt;</span> Response</span> </span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-storage hl-type hl-rust">let</span> state <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-support hl-function hl-rust">app_state</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-keyword hl-control hl-rust">match</span> state.indexnow_key.<span class="hl-support hl-function hl-rust">as_deref</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        <span class="hl-support hl-type hl-rust">Some</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>key</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=&gt;</span> <span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>
            <span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-meta hl-path hl-rust">header<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-constant hl-other hl-rust">CONTENT_TYPE</span><span class="hl-punctuation hl-separator hl-rust">,</span> <span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>text/plain; charset=utf-8<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
            key.<span class="hl-support hl-function hl-rust">to_string</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
        </span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
            .<span class="hl-support hl-function hl-rust">into_response</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
        <span class="hl-support hl-type hl-rust">None</span> <span class="hl-keyword hl-operator hl-rust">=&gt;</span> <span class="hl-meta hl-path hl-rust">StatusCode<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-constant hl-other hl-rust">NOT_FOUND</span>.<span class="hl-support hl-function hl-rust">into_response</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>Wire it up conditionally. No key, no route:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Registered only when INDEXNOW_KEY is set. The whole feature stays inert
</span><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> on dev and on self-hosted deployments that never configured it.
</span><span class="hl-storage hl-type hl-rust">let</span> router <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-keyword hl-control hl-rust">if</span> <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-support hl-type hl-rust">Some</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>key</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=</span> state.indexnow_key.<span class="hl-support hl-function hl-rust">clone</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    router.<span class="hl-support hl-function hl-rust">route</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-keyword hl-operator hl-rust">&amp;</span><span class="hl-support hl-macro hl-rust">format!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>/<span class="hl-constant hl-other hl-placeholder hl-rust">{key}</span>.txt<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span> <span class="hl-support hl-function hl-rust">get</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>indexnow_key</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span> <span class="hl-keyword hl-control hl-rust">else</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    router
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
</span></code></pre>
<p>The key itself is just <code>openssl rand -hex 16</code>, loaded from an env var. The payoff is simple: the secret never touches the repo, the file only exists in production, and everywhere else the route 404s. Nothing to clean up, nothing to leak.</p>
<h2 id="piece-2-submit-on-deploy-not-on-build">Piece 2: submit on deploy, not on build<a class="heading-anchor" href="#piece-2-submit-on-deploy-not-on-build" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Now the submission. The instinct is to fire it during the build. Resist it. You have to submit <em>after</em> the new content is actually reachable. Otherwise you’re inviting crawlers to URLs that may still 404 because the deploy hasn’t swapped over yet.</p>
<p>So this runs as the last step of the deploy job, after the container is up. Here’s the real step from our pipeline, with comments left in:</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Submit URLs to IndexNow</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">env</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">INDEXNOW_KEY</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">${{ secrets.INDEXNOW_KEY }}</span>
    <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">SITE</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">${{ vars.SITE_URL || &#39;https://infra.page&#39; }}</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-keyword hl-control hl-flow hl-block-scalar hl-literal hl-yaml">|</span>
<span class="hl-string hl-unquoted hl-block hl-yaml">    if [ -z &quot;$INDEXNOW_KEY&quot; ]; then
      echo &quot;INDEXNOW_KEY not set — skipping.&quot;
      exit 0
    fi

    # host with no scheme/path, for the payload
    HOST=&quot;${SITE#http://}&quot;; HOST=&quot;${HOST#https://}&quot;; HOST=&quot;${HOST%%/*}&quot;

    # Wait until the NEW release is live and serving the key file.
    # This is the bit everyone forgets: during a rolling swap there&#39;s a
    # window where the old container still answers. Poll until the key
    # file comes back with the expected value before submitting anything.
    KEY_URL=&quot;$SITE/$INDEXNOW_KEY.txt&quot;
    ready=
    for i in $(seq 1 30); do
      body=$(curl -fsS --max-time 10 &quot;$KEY_URL&quot; 2&gt;/dev/null || true)
      if [ &quot;$body&quot; = &quot;$INDEXNOW_KEY&quot; ]; then ready=1; break; fi
      echo &quot;Waiting for $KEY_URL (attempt $i/30)…&quot;
      sleep 10
    done
    if [ -z &quot;$ready&quot; ]; then
      echo &quot;::warning::Key file not reachable after 5min — skipping.&quot;
      exit 0
    fi

    # The URL list IS the sitemap. One source of truth, can&#39;t drift.
    urls=$(curl -fsS --max-time 30 &quot;$SITE/sitemap.xml&quot; | grep -oP &#39;(?&lt;=&lt;loc&gt;)[^&lt;]+&#39; || true)
    count=$(printf &#39;%s\n&#39; &quot;$urls&quot; | grep -c . || true)
    [ &quot;$count&quot; -eq 0 ] &amp;&amp; { echo &quot;::warning::Empty sitemap — skipping.&quot;; exit 0; }

    payload=$(printf &#39;%s\n&#39; &quot;$urls&quot; | jq -R . | jq -s \
      --arg host &quot;$HOST&quot; --arg key &quot;$INDEXNOW_KEY&quot; --arg loc &quot;$KEY_URL&quot; \
      &#39;{host: $host, key: $key, keyLocation: $loc, urlList: .}&#39;)

    status=$(curl -s -o /tmp/resp.txt -w &#39;%{http_code}&#39; \
      -X POST &#39;https://api.indexnow.org/indexnow&#39; \
      -H &#39;Content-Type: application/json; charset=utf-8&#39; \
      --data &quot;$payload&quot; || echo &quot;000&quot;)

    if [ &quot;$status&quot; -ge 200 ] &amp;&amp; [ &quot;$status&quot; -lt 300 ]; then
      echo &quot;✅ IndexNow accepted $count URLs (HTTP $status).&quot;
    else
      echo &quot;::warning::IndexNow returned HTTP $status. Deploy is unaffected.&quot;
    fi
</span></span></code></pre>
<p>A few details matter more than they look.</p>
<p>The poll loop is the big one. During a rolling deploy there’s a window where the old container is still answering requests. If you submit in that window, you might be telling crawlers about content that isn’t live yet, or the request goes to a box that’s about to die. So we poll the key file until the <em>new</em> release serves it. The key file does double duty here: it proves ownership and gives us a cheap “is the new version up?” check.</p>
<p>The sitemap is the URL list. We don’t hand-maintain a list of URLs to submit. We read <code>sitemap.xml</code> and pull every <code>&lt;loc&gt;</code>. Your sitemap already knows what’s indexable, so reusing it means the submission list can’t drift from reality. Add a page, it shows up in the sitemap, it gets submitted. No second list to forget about.</p>
<p>Best-effort, always. Every failure path is a <code>::warning::</code> and an <code>exit 0</code>. An SEO ping must never fail your deploy. By the time this step runs, your app is already live and serving users. If IndexNow is having a bad day, that’s IndexNow’s problem, not your release’s. Letting a search-engine nicety break a production deploy is exactly backwards.</p>
<h2 id="the-tradeoffs-we-made-on-purpose">The tradeoffs we made on purpose<a class="heading-anchor" href="#the-tradeoffs-we-made-on-purpose" aria-hidden="true" tabindex="-1">#</a></h2>
<p>This is a deploy-time mechanism, so it has edges. Fine. Just know which ones you’re accepting:</p>
<ul>
<li>It fires on deploy, not on content change. For mostly-static sites like a marketing site, docs, or a blog, that’s perfect. Content changes <em>are</em> deploys. For infra.page, where users create public pages at runtime, those pages only get re-pinged on the next deploy. They’re still in the sitemap so they do get submitted, just not instantly. If you need instant pings for runtime content, fire one from your app when the content changes. The deploy step and a live ping can live together.</li>
<li>Each deploy re-submits the whole sitemap. We don’t diff. IndexNow is fine with this, and “submit everything that’s indexable” beats maintaining a changed-since-last-time list. If your sitemap grows into the thousands and this starts to feel rude, that’s when you add a diff. Not before.</li>
<li>It’s bash in CI, not code in the app. On purpose. The trigger is deterministic, it’s visible in the workflow log, it’s re-runnable, and it submits only after the release is provably live. Doing it in-app means wiring it into your boot sequence and reasoning about <em>which</em> instance submits. For deploy-time submission, the CI step is the simpler, more honest place for it.</li>
</ul>
<p>That last point is also why the same pattern dropped cleanly into two different products with one decision flipped per product: SeggWat scopes submission to our own first-party marketing and docs pages, while infra.page lets the sitemap include public user pages too. Same machinery, different <code>sitemap.xml</code>. The pattern doesn’t care what’s in the list.</p>
<h2 id="did-it-work">Did it work?<a class="heading-anchor" href="#did-it-work" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Generate your key, set <code>INDEXNOW_KEY</code> as a CI secret (and in your runtime env so the app serves the file), deploy, and then check Bing Webmaster Tools → IndexNow. It shows submission counts and any rejected URLs. If the key file isn’t reachable it’ll tell you there, which is a much nicer feedback loop than guessing.</p>
<h2 id="worth-it">Worth it?<a class="heading-anchor" href="#worth-it" aria-hidden="true" tabindex="-1">#</a></h2>
<p>This is the kind of infra chore that pays rent quietly. It cost an afternoon, it costs nothing to run, and now every release tells half the search world to come look, without anyone remembering to do it. For a small team shipping a product nobody’s heard of yet, “get found faster, for free, forever” is an easy yes.</p>
<p>The whole thing is maybe sixty lines split across a Rust handler and a CI step. If you’ve got a sitemap and a deploy pipeline, you already have the hard parts. Go connect them.</p>
]]></content:encoded>
</item>
<item>
<title>Typed WebSockets in Dioxus 0.7: A Small Fullstack Rust Example</title>
<link>https://oxidt.com/blog/dioxus-07-websockets-fullstack-rust</link>
<guid isPermaLink="true">https://oxidt.com/blog/dioxus-07-websockets-fullstack-rust</guid>
<pubDate>Wed, 17 Jun 2026 09:16:04 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>A small practical example of typed WebSockets in Dioxus 0.7, using fullstack Rust patterns for realtime app features.</description>
<category>axum</category>
<category>dioxus</category>
<category>fullstack-rust</category>
<category>rust</category>
<category>websockets</category>
<content:encoded><![CDATA[<p>WebSockets are one of those features that look simple until your app has to keep frontend and backend message shapes in sync.</p>
<p>Sending <code>"hello"</code> over a socket is easy. Maintaining a growing pile of loosely shaped JSON messages across client code, server handlers, UI state, and tests is where the fun starts to rot.</p>
<p>That is why the WebSocket support in Dioxus 0.7 is interesting. Dioxus Fullstack builds on top of Axum’s WebSocket API, but adds the thing Rust developers usually want: shared types.</p>
<p>Instead of treating the socket as a string tunnel, you can model the messages flowing in both directions.</p>
<p>That sounds small. It is not.</p>
<p>It means your realtime feature can start with actual Rust types instead of “please remember this payload shape in three places”.</p>
<h2 id="why-websockets">Why WebSockets?<a class="heading-anchor" href="#why-websockets" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Most web apps can get pretty far with plain HTTP.</p>
<p>A form submit is fine when the user sends something once and waits for a result. A normal API call is fine when the UI needs to fetch a list, update a record, or save a setting.</p>
<p>WebSockets become useful when the connection itself matters.</p>
<p>Examples:</p>
<ul>
<li>a feedback widget receiving live replies</li>
<li>a build log streaming output</li>
<li>a multiplayer or collaborative UI</li>
<li>progress updates for long-running jobs</li>
<li>notifications that should arrive without polling</li>
<li>a dashboard that reflects backend state quickly</li>
</ul>
<p>You can fake some of this with polling. Sometimes polling is the right boring answer.</p>
<p>But once the client and server need to talk repeatedly, in order, over the same session, a WebSocket is the better primitive.</p>
<h2 id="the-dioxus-0-7-shape">The Dioxus 0.7 shape<a class="heading-anchor" href="#the-dioxus-0-7-shape" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Dioxus Fullstack provides WebSocket support that works alongside server functions.</p>
<p>The important pieces are:</p>
<ul>
<li><code>WebSocketOptions</code></li>
<li><code>Websocket</code></li>
<li><code>options.on_upgrade(...)</code></li>
<li>typed input and output messages</li>
<li>optional custom encoding</li>
</ul>
<p>At the simplest level, a server function accepts <code>WebSocketOptions</code> and returns a <code>Websocket</code>.</p>
<p>The official Dioxus docs show the basic idea with an uppercase echo socket:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">get</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>/api/uppercase_ws<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
async <span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-rust"><span class="hl-storage hl-type hl-function hl-rust">fn</span> </span><span class="hl-entity hl-name hl-function hl-rust">uppercase_ws</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-begin hl-rust">(</span><span class="hl-variable hl-parameter hl-rust">options</span><span class="hl-punctuation hl-separator hl-rust">:</span> WebSocketOptions</span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-end hl-rust">)</span></span></span></span><span class="hl-meta hl-function hl-rust"> <span class="hl-meta hl-function hl-return-type hl-rust"><span class="hl-punctuation hl-separator hl-rust">-&gt;</span> <span class="hl-meta hl-generic hl-rust">Result<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span>Websocket<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span></span> </span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-support hl-type hl-rust">Ok</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>options.<span class="hl-support hl-function hl-rust">on_upgrade</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-storage hl-modifier hl-rust">move</span> <span class="hl-keyword hl-operator hl-rust">|</span><span class="hl-storage hl-modifier hl-rust">mut</span> socket<span class="hl-keyword hl-operator hl-rust">|</span> async <span class="hl-storage hl-modifier hl-rust">move</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Send an initial greeting.
</span>        <span class="hl-keyword hl-operator hl-rust">_</span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">send</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>Hello!<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span>.<span class="hl-support hl-function hl-rust">to_string</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>

        <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Echo messages back in uppercase.
</span>        <span class="hl-keyword hl-control hl-rust">while</span> <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-support hl-type hl-rust">Ok</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>msg</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">recv</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
            <span class="hl-keyword hl-operator hl-rust">_</span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">send</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>msg.<span class="hl-support hl-function hl-rust">to_ascii_uppercase</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>
        </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>That already gives you a useful realtime channel.</p>
<p>The server upgrades the HTTP request to a WebSocket connection, then keeps receiving and sending messages while the socket is alive.</p>
<p>Under the hood, Dioxus wraps Axum’s WebSocket upgrade flow. The nice part is that you do not have to drop out of the Dioxus Fullstack model just because one part of your app needs bidirectional communication.</p>
<h2 id="the-default-websocket-type">The default <code>Websocket</code> type<a class="heading-anchor" href="#the-default-websocket-type" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The <code>Websocket</code> type is generic over three parameters:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-struct hl-rust"><span class="hl-storage hl-modifier hl-rust">pub</span> <span class="hl-storage hl-type hl-struct hl-rust">struct</span> </span><span class="hl-meta hl-struct hl-rust"><span class="hl-meta hl-generic hl-rust"><span class="hl-entity hl-name hl-struct hl-rust">Websocket</span><span class="hl-meta hl-generic hl-rust"><span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span>In = String, Out = String, E = JsonEncoding<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span></span></span><span class="hl-meta hl-struct hl-rust"> </span><span class="hl-meta hl-struct hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> ...
</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>Those parameters are:</p>
<ol>
<li>the type received from the client</li>
<li>the type sent back to the client</li>
<li>the encoding used for messages</li>
</ol>
<p>By default, both message directions use <code>String</code>, encoded as JSON.</p>
<p>That is fine for a toy echo example. It is less nice once your feature has real events.</p>
<p>For real app code, I usually want explicit event types.</p>
<p>Not because enums are fancy. Because debugging random string protocols at 1am is a self-inflicted wound.</p>
<h2 id="adding-typed-messages">Adding typed messages<a class="heading-anchor" href="#adding-typed-messages" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Let’s say the client sends text input and the server responds with the uppercase version.</p>
<p>We can model that as two enums:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-keyword hl-other hl-rust">use</span> <span class="hl-meta hl-path hl-rust">serde<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>Deserialize<span class="hl-punctuation hl-separator hl-rust">,</span> Serialize</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

<span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">derive</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust">Serialize<span class="hl-punctuation hl-separator hl-rust">,</span> Deserialize<span class="hl-punctuation hl-separator hl-rust">,</span> Debug</span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
<span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ClientEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    TextInput<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-support hl-type hl-rust">String</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>

<span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">derive</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust">Serialize<span class="hl-punctuation hl-separator hl-rust">,</span> Deserialize<span class="hl-punctuation hl-separator hl-rust">,</span> Debug</span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
<span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ServerEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    Uppercase<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-support hl-type hl-rust">String</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>Now the WebSocket function can return a typed socket:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">get</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>/api/uppercase_ws<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
async <span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-rust"><span class="hl-storage hl-type hl-function hl-rust">fn</span> </span><span class="hl-entity hl-name hl-function hl-rust">uppercase_ws</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-begin hl-rust">(</span>
    <span class="hl-variable hl-parameter hl-rust">options</span><span class="hl-punctuation hl-separator hl-rust">:</span> WebSocketOptions,
</span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-end hl-rust">)</span></span></span></span><span class="hl-meta hl-function hl-rust"> <span class="hl-meta hl-function hl-return-type hl-rust"><span class="hl-punctuation hl-separator hl-rust">-&gt;</span> <span class="hl-meta hl-generic hl-rust">Result<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span><span class="hl-meta hl-generic hl-rust">Websocket<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span>ClientEvent, ServerEvent<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span><span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span></span> </span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-support hl-type hl-rust">Ok</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>options.<span class="hl-support hl-function hl-rust">on_upgrade</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-storage hl-modifier hl-rust">move</span> <span class="hl-keyword hl-operator hl-rust">|</span><span class="hl-storage hl-modifier hl-rust">mut</span> socket<span class="hl-keyword hl-operator hl-rust">|</span> async <span class="hl-storage hl-modifier hl-rust">move</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        <span class="hl-keyword hl-control hl-rust">while</span> <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-support hl-type hl-rust">Ok</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>event</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">recv</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
            <span class="hl-keyword hl-control hl-rust">match</span> event <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
                <span class="hl-meta hl-path hl-rust">ClientEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>TextInput<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>text</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=&gt;</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
                    <span class="hl-storage hl-type hl-rust">let</span> response <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-meta hl-path hl-rust">ServerEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Uppercase<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>text.<span class="hl-support hl-function hl-rust">to_ascii_uppercase</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
                    <span class="hl-keyword hl-operator hl-rust">_</span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">send</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>response</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>
                </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
            </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
        </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>This is the part worth caring about.</p>
<p>The socket is no longer “some text comes in, some text goes out”.</p>
<p>It is:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust">ClientEvent <span class="hl-meta hl-function hl-return-type hl-rust"><span class="hl-punctuation hl-separator hl-rust">-&gt;</span> ServerEvent
</span></span></code></pre>
<p>The compiler now knows what the protocol looks like.</p>
<p>If you add a new client event later, Rust can help you find the places that need to handle it. If the server sends a response variant the UI does not expect, that mismatch becomes visible in code instead of hiding in runtime behavior.</p>
<p>That is the boring magic of fullstack Rust.</p>
<h2 id="a-more-realistic-event-shape">A more realistic event shape<a class="heading-anchor" href="#a-more-realistic-event-shape" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The uppercase example is useful because it is tiny. But most actual realtime features are event streams.</p>
<p>For example, a feedback widget might send events like this:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">derive</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust">Serialize<span class="hl-punctuation hl-separator hl-rust">,</span> Deserialize<span class="hl-punctuation hl-separator hl-rust">,</span> Debug</span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
<span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ClientEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    DraftChanged <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        text<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
    FeedbackSubmitted <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        page_url<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
        message<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>And the server might respond with:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">derive</span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust">Serialize<span class="hl-punctuation hl-separator hl-rust">,</span> Deserialize<span class="hl-punctuation hl-separator hl-rust">,</span> Debug</span></span><span class="hl-meta hl-annotation hl-parameters hl-rust"><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
<span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ServerEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    Connected <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        session_id<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
    DraftAccepted<span class="hl-punctuation hl-separator hl-rust">,</span>
    FeedbackReceived <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        id<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
    Error <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        message<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span><span class="hl-punctuation hl-separator hl-rust">,</span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>That already reads like a small protocol.</p>
<p>And because it is plain Rust, you can keep it boring:</p>
<ul>
<li>simple enums</li>
<li>simple structs</li>
<li>no mystery strings</li>
<li>no undocumented JSON blobs</li>
<li>no “frontend knows this by convention” nonsense</li>
</ul>
<p>This is the part where Dioxus starts to feel different from a typical frontend/backend split. The message contract can live close to both sides of the feature.</p>
<h2 id="websockets-are-stateful">WebSockets are stateful<a class="heading-anchor" href="#websockets-are-stateful" aria-hidden="true" tabindex="-1">#</a></h2>
<p>There is one caveat you should not ignore: WebSockets are stateful.</p>
<p>A WebSocket connection ties a client and server together for a session. That is the point. It is also the cost.</p>
<p>This matters when deploying.</p>
<p>If your app runs in an environment where requests are short-lived, serverless, or aggressively moved around, you need to think carefully about where the connection lives and how session state is preserved.</p>
<p>For a normal long-running Axum-style service, this is usually fine.</p>
<p>For serverless environments with request time limits, it can get awkward quickly.</p>
<p>That is not a Dioxus problem. That is WebSockets being WebSockets.</p>
<h2 id="encoding-choices">Encoding choices<a class="heading-anchor" href="#encoding-choices" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Dioxus also lets you customize the encoding.</p>
<p>The default is JSON, which is the right choice when compatibility matters. If third-party clients might connect, JSON is boring and useful.</p>
<p>For Rust-only clients, binary encodings like CBOR or MsgPack can make more sense.</p>
<p>The type shape looks like this:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-generic hl-rust">Websocket<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span>ClientEvent, ServerEvent, CborEncoding<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span>
</span></code></pre>
<p>I would not start there unless you have a reason.</p>
<p>JSON first. Optimize later. Future-you has enough problems.</p>
<h2 id="where-this-is-actually-useful">Where this is actually useful<a class="heading-anchor" href="#where-this-is-actually-useful" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Typed WebSockets are not something every app needs on day one.</p>
<p>But they become valuable the moment “submit and wait” stops being enough.</p>
<p>A few places where this pattern fits well:</p>
<h3 id="live-feedback">Live feedback<a class="heading-anchor" href="#live-feedback" aria-hidden="true" tabindex="-1">#</a></h3>
<p>For a feedback product like SeggWat, WebSockets could support richer interactions than a plain form submit.</p>
<p>For example:</p>
<ul>
<li>user starts typing feedback</li>
<li>widget validates or classifies input</li>
<li>server sends acknowledgement</li>
<li>dashboard receives the new feedback event</li>
<li>support/admin UI updates without refresh</li>
</ul>
<p>Not every feedback flow needs that. A calm form is often better than a hyperactive realtime UI.</p>
<p>But once feedback becomes part of a live product surface, the protocol matters.</p>
<h3 id="build-logs-and-job-progress">Build logs and job progress<a class="heading-anchor" href="#build-logs-and-job-progress" aria-hidden="true" tabindex="-1">#</a></h3>
<p>If your app starts long-running jobs, WebSockets are a natural fit.</p>
<p>The client sends:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-path hl-rust">ClientEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>StartJob <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span> id </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
</span></code></pre>
<p>The server streams:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-path hl-rust">ServerEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>JobStarted
<span class="hl-meta hl-path hl-rust">ServerEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>LogLine<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-keyword hl-operator hl-rust">...</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
<span class="hl-meta hl-path hl-rust">ServerEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Progress<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-keyword hl-operator hl-rust">...</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>
<span class="hl-meta hl-path hl-rust">ServerEvent<span class="hl-punctuation hl-accessor hl-rust">::</span></span>JobFinished
</span></code></pre>
<p>That is much cleaner than hammering <code>/status</code> every second and hoping the UI catches up.</p>
<h3 id="collaborative-interfaces">Collaborative interfaces<a class="heading-anchor" href="#collaborative-interfaces" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Anything collaborative eventually needs a shared event model.</p>
<p>Even if you do not build full Google Docs-style collaboration, small shared-state features benefit from typed messages:</p>
<ul>
<li>presence</li>
<li>cursor position</li>
<li>comments</li>
<li>edit notifications</li>
<li>shared dashboard updates</li>
</ul>
<p>Again, the win is not that WebSockets exist. The win is that the messages are modeled directly.</p>
<h2 id="keep-the-protocol-boring">Keep the protocol boring<a class="heading-anchor" href="#keep-the-protocol-boring" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The biggest mistake with realtime features is getting clever too early.</p>
<p>A good WebSocket protocol should be almost dull.</p>
<p>Prefer this:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ClientEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    MessageSent <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span> text<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span> </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>

<span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">ServerEvent</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    MessageAccepted <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span> id<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span> </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
    MessageRejected <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span> reason<span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-support hl-type hl-rust">String</span> </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>Over this:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-struct hl-rust"><span class="hl-storage hl-type hl-struct hl-rust">struct</span> </span><span class="hl-meta hl-struct hl-rust"><span class="hl-entity hl-name hl-struct hl-rust">SocketPayload</span> </span><span class="hl-meta hl-struct hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-variable hl-other hl-member hl-rust">kind</span><span class="hl-punctuation hl-separator hl-rust">:</span> String,
    <span class="hl-variable hl-other hl-member hl-rust">data</span><span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-meta hl-path hl-rust">serde_json<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Value,
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>The second version feels flexible. It is also where type safety goes to die quietly behind the shed.</p>
<p>Use <code>serde_json::Value</code> at the edges if you must. Do not make it the center of your app protocol unless you enjoy writing bugs with both hands.</p>
<h2 id="practical-gotchas">Practical gotchas<a class="heading-anchor" href="#practical-gotchas" aria-hidden="true" tabindex="-1">#</a></h2>
<p>A few things worth remembering:</p>
<h3 id="handle-disconnects-normally">Handle disconnects normally<a class="heading-anchor" href="#handle-disconnects-normally" aria-hidden="true" tabindex="-1">#</a></h3>
<p>A WebSocket can close. The network can vanish. A laptop can sleep. A browser tab can disappear.</p>
<p>Design the server loop assuming <code>recv()</code> eventually fails.</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-keyword hl-control hl-rust">while</span> <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-support hl-type hl-rust">Ok</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>event</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">recv</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> handle event
</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
</span></code></pre>
<p>When the loop ends, clean up anything tied to that connection.</p>
<h3 id="do-not-hide-all-errors">Do not hide all errors<a class="heading-anchor" href="#do-not-hide-all-errors" aria-hidden="true" tabindex="-1">#</a></h3>
<p>The examples often use:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-keyword hl-operator hl-rust">_</span> <span class="hl-keyword hl-operator hl-rust">=</span> socket.<span class="hl-support hl-function hl-rust">send</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>response</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>
</span></code></pre>
<p>That keeps the sample small. In production code, you may want to log send failures or break out of the loop when the connection is gone.</p>
<h3 id="avoid-huge-message-types">Avoid huge message types<a class="heading-anchor" href="#avoid-huge-message-types" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Typed does not mean enormous.</p>
<p>Keep messages focused. If one enum variant becomes half your app state, split the feature.</p>
<h3 id="think-before-using-websockets-on-serverless">Think before using WebSockets on serverless<a class="heading-anchor" href="#think-before-using-websockets-on-serverless" aria-hidden="true" tabindex="-1">#</a></h3>
<p>WebSockets want a persistent connection. Some serverless platforms support that. Some make it painful. Some technically support it but make the architecture weird.</p>
<p>If the deployment target fights stateful connections, do not pretend it is fine. Pick SSE, polling, a queue, or different infrastructure.</p>
<h2 id="why-i-like-this-pattern">Why I like this pattern<a class="heading-anchor" href="#why-i-like-this-pattern" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The nice thing about Dioxus Fullstack is not that it removes the frontend/backend boundary completely.</p>
<p>It does not. The boundary is still there.</p>
<p>The nice thing is that the boundary can become typed.</p>
<p>That matters most in the small glue code where web apps usually get messy:</p>
<ul>
<li>request payloads</li>
<li>response shapes</li>
<li>socket messages</li>
<li>form types</li>
<li>route params</li>
<li>optimistic UI state</li>
</ul>
<p>WebSockets make that mess more visible because the communication is long-lived. If the protocol is vague, the feature gets vague too.</p>
<p>Dioxus gives you a way to keep it explicit.</p>
<p>Client sends <code>ClientEvent</code>.</p>
<p>Server sends <code>ServerEvent</code>.</p>
<p>Encoding is declared.</p>
<p>The connection upgrade is handled through the fullstack layer.</p>
<p>That is a nice default.</p>
<h2 id="final-thought">Final thought<a class="heading-anchor" href="#final-thought" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Typed WebSockets are not magic. You still have to handle deployment, disconnects, errors, and state.</p>
<p>But they remove one very annoying class of bugs: the kind where the frontend and backend silently disagree about what a message is supposed to look like.</p>
<p>For realtime features, that is a big deal.</p>
<p>And for fullstack Rust, it is exactly the kind of boring correctness that makes the stack worth using.</p>
<h2 id="references">References<a class="heading-anchor" href="#references" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li><a href="https://dioxuslabs.com/learn/0.7/essentials/fullstack/websockets?ref=oxidt.com">Dioxus 0.7 WebSockets documentation</a></li>
<li><a href="https://dioxuslabs.com/learn/0.7/essentials/fullstack/?ref=oxidt.com">Dioxus Fullstack overview</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Self-Hosting Your Trust Layer: A Status Page and a Feedback Board</title>
<link>https://oxidt.com/blog/self-hosting-your-trust-layer-status-page-feedback-board</link>
<guid isPermaLink="true">https://oxidt.com/blog/self-hosting-your-trust-layer-status-page-feedback-board</guid>
<pubDate>Tue, 16 Jun 2026 10:00:00 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>A small self-hosted trust layer for indie products: one public status page, one feedback board, and a changelog that proves someone is home.</description>
<category>feedback</category>
<category>indie</category>
<category>observability</category>
<category>rust</category>
<category>self-hosted</category>
<content:encoded><![CDATA[<p>I run a couple of small products. The thing that quietly costs me users has almost never been a missing feature. It’s something fuzzier and harder to put on a roadmap: whether people believe the lights are on.</p>
<p>You can ship the best feature in your niche and still lose someone who decided, with no evidence either way, that you’re a ghost town. That decision is the default. People assume abandonment unless you give them a reason not to.</p>
<p>Underneath it are two questions running in the back of every user’s head:</p>
<ol>
<li>Is this thing actually working?</li>
<li>Does anyone here listen, and do they ship?</li>
</ol>
<p>Most of us answer neither in public. The infra is fine — but only I can see the dashboard. I read every piece of feedback — but from the outside it looks like it falls into a void. The information that would answer both questions already exists. It’s just private.</p>
<p>I think of the fix as a trust layer: a small set of public-facing surfaces whose only job is to answer those two questions before anyone has to ask. And because I’d rather own my stack than rent it, I run that layer self-hosted. This post is how I wire it up.</p>
<h2 id="why-self-hosted-specifically">Why self-hosted, specifically<a class="heading-anchor" href="#why-self-hosted-specifically" aria-hidden="true" tabindex="-1">#</a></h2>
<p>There’s a particular irony in renting your trust layer.</p>
<p>A status page hosted on someone else’s SaaS can go down when <em>they</em> go down — and now your “is it up?” page is part of the outage. A feedback board on a third-party platform means your users’ words, your roadmap, and your changelog live in someone else’s database, behind someone else’s export button, priced per seat.</p>
<p>For the surfaces whose entire point is <em>trust</em>, I want the opposite properties: I own the data, I control what’s public, and the thing runs on infrastructure I can see. That’s not ideology for its own sake — it’s just matching the tool to the job. A trust layer you don’t control isn’t fully a trust layer.</p>
<p>Both halves below are self-hosted, open-source, and ship as a single binary or a Docker image. I build most of my stack in Rust for exactly the reasons you’d guess: small footprint, boring reliability, runs happily on a cheap VM.</p>
<h2 id="surface-1-a-status-page-you-own">Surface 1: a status page you own<a class="heading-anchor" href="#surface-1-a-status-page-you-own" aria-hidden="true" tabindex="-1">#</a></h2>
<p>When something feels slow or broken, a user has two options. Email you and wait. Or assume you’re down and walk.</p>
<p>A public status page gives them a third: check for themselves. It answers “is it just me?” at 3am on a Sunday without a support ticket, and it answers it the same whether you’re awake or not.</p>
<p>For a small product this doesn’t need to be a wall of green checkmarks maintained by an SRE team. It’s one public page showing what’s actually running: uptime, recent incidents, the services you depend on, maybe a couple of live metrics.</p>
<p>The one I run is <a href="https://infra.page/?ref=oxidt.com">infra.page</a> — a self-hosted, open-source infrastructure dashboard. Think Linktree for your project’s infrastructure: it pulls the things you already operate (uptime monitors, GitHub, Sentry, analytics, and more) into drag-and-drop widgets on a single page. It has owner-vs-visitor visibility, so you decide exactly what the outside world sees versus what stays internal, you can point it at your own domain, and since it’s self-hosted the page isn’t coupled to a vendor that can fail alongside you.</p>
<p>A quick rule of thumb for what to expose:</p>
<ul>
<li>Public: uptime, current incidents, a short note after something breaks.</li>
<li>Public if you can: a response-time or request-volume trend — a visible heartbeat is reassuring.</li>
<li>Keep internal: anything that’s a security signal (exact topology, raw error detail, cost data).</li>
</ul>
<p>The honesty test for an incident note: would you be comfortable if a user read it <em>during</em> the outage? If yes, publish. Owning a 20-minute blip in public buys more trust than pretending it never happened — they already noticed.</p>
<h2 id="surface-2-a-feedback-board-you-own">Surface 2: a feedback board you own<a class="heading-anchor" href="#surface-2-a-feedback-board-you-own" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The second question is the one I care about most, because feedback tools usually get it backwards.</p>
<p>You can collect feedback flawlessly and still look dead. If every request lands in a private inbox, the person who sent it has no idea whether it arrived, whether anyone agreed, or whether anything happened. From their seat it’s indistinguishable from piping to /dev/null.</p>
<p>The fix is to move the loop into the open. Three pieces that reinforce each other:</p>
<ul>
<li>A public feature-request board. Users post ideas, vote on each other’s, and watch status move from “under review” to “planned” to “shipped.” A vote count is a public promise that demand is real and that you can see it.</li>
<li>A roadmap that’s just the board, sorted. You don’t need a second tool. “Here’s what’s planned, here’s what’s in progress” is the same board filtered by status. The point isn’t precision — nobody holds you to dates — it’s signal that someone’s steering.</li>
<li>A public changelog. The closing half of every loop: someone asked, you built it, you say so. A steady “what’s new” stream is the most underrated trust signal a small product has, because it can’t be faked — you either ship or you don’t.</li>
</ul>
<p>The feedback half I run is <a href="https://seggwat.com/?ref=oxidt.com">SeggWat</a>, which is also self-hosted-friendly and gives you the board, the roadmap, and the changelog out of one widget. Its changelog can pull straight from GitHub releases and completed requests, so keeping it current isn’t another chore competing with actual work.</p>
<p>The magic is the handoff between the three: a request comes in → collects votes → moves to “planned” → ships → shows up in the changelog → the person who asked sees their idea become a line item. That visible arc is what turns a passive user into someone who recommends you.</p>
<h2 id="the-two-surfaces-are-one-promise">The two surfaces are one promise<a class="heading-anchor" href="#the-two-surfaces-are-one-promise" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Here’s why I bundle them instead of treating them as unrelated tools.</p>
<p>A status page says: <em>the machine is running.</em><br />
A feedback board and changelog say: <em>the people are running it.</em></p>
<p>Users need both. A product that’s always up but never changes feels abandoned. A product that ships constantly but keeps falling over feels reckless. Show both, in public, and you’ve answered the only two trust questions that matter — before anyone had to email to ask.</p>
<p>And neither asks you to manufacture anything. The infra is already up; the status page just points at it. You already read feedback and ship; the board and changelog just turn that outward. The trust layer isn’t extra work. It’s the work you already do, made visible.</p>
<h2 id="a-small-starting-point">A small starting point<a class="heading-anchor" href="#a-small-starting-point" aria-hidden="true" tabindex="-1">#</a></h2>
<p>You don’t need all of this on day one. The minimum viable trust layer is two links in your footer, both pointed at things already true about your product:</p>
<ul>
<li>Status → a self-hosted dashboard like infra.page wired to your uptime monitor.</li>
<li>Roadmap / Changelog → a feedback board like SeggWat so users can ask, vote, and see what shipped.</li>
</ul>
<p>Two links. Owned, not rented. You’re not promising transparency in a blog post nobody reads — you’re just no longer hiding the parts that build trust.</p>
<p>Show people the machine is running. Show them someone’s home. Then get back to building.</p>
]]></content:encoded>
</item>
<item>
<title>Why I built an MCP server into my feedback tool</title>
<link>https://oxidt.com/blog/why-i-built-an-mcp-server-into-my-feedback-tool</link>
<guid isPermaLink="true">https://oxidt.com/blog/why-i-built-an-mcp-server-into-my-feedback-tool</guid>
<pubDate>Fri, 22 May 2026 07:52:44 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>Most SaaS tools have an API. Some have webhooks. Very few have a Model Context Protocol server that lets AI assistants query, filter, and act on your data conversationally.</description>
<content:encoded><![CDATA[<p>Most SaaS tools have an API. Some have webhooks. Very few have a Model Context Protocol server that lets AI assistants query, filter, and act on your data conversationally.</p>
<p>I built one into SeggWat, a feedback widget for websites. Here’s what it does, how it works, and why I think MCP is becoming table stakes for dev tools.</p>
<h2 id="the-real-problem-with-feedback-tools">The real problem with feedback tools<a class="heading-anchor" href="#the-real-problem-with-feedback-tools" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Collection is solved. Nobody actually consumes the result.</p>
<p>The pattern I see all the time:</p>
<ol>
<li>A team installs a feedback widget.</li>
<li>Users submit bug reports, feature ideas, and ratings.</li>
<li>Feedback piles up in a dashboard nobody opens.</li>
<li>Product decisions get made on gut feel anyway.</li>
</ol>
<p>Dashboards make you go and look. Email digests get filtered. Slack notifications turn into noise. The signal is there, no one is in the chair.</p>
<p>So I started asking a different question: what if the feedback data was available wherever I’m already working, including inside my AI coding assistant?</p>
<h2 id="what-mcp-actually-is">What MCP actually is<a class="heading-anchor" href="#what-mcp-actually-is" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The Model Context Protocol is an open protocol from Anthropic. A standard way for any LLM to ask structured questions to your application. Think of it as the REST API for AI workflows: instead of your own code asking questions you anticipated, any AI can ask anything and the protocol negotiates the right tool calls.</p>
<p>Before:</p>
<blockquote>
<p>Me: “What are users complaining about?”<br />
AI: “I don’t have access to your feedback data.”</p>
</blockquote>
<p>With MCP:</p>
<blockquote>
<p>Me: “What are users complaining about?”<br />
AI: <em>calls list_feedback</em> → “47 submissions this week. Top issues: mobile login timeout (12), dark mode contrast in settings (8), broken Stripe redirect (3)…”</p>
</blockquote>
<p>Now the AI is actually useful for the question you wanted to ask.</p>
<h2 id="how-seggwat-s-mcp-server-works">How SeggWat’s MCP server works<a class="heading-anchor" href="#how-seggwat-s-mcp-server-works" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The server lives at <a href="https://seggwat.com/mcp?ref=oxidt.com">https://seggwat.com/mcp</a>. It uses streamable HTTP transport: clients POST JSON-RPC, the server replies (and streams) via Server-Sent Events. Auth is your project’s API key, passed either as <code>Authorization: Bearer &lt;key&gt;</code> or <code>X-API-Key: &lt;key&gt;</code>. Every session is scoped to a single project. No cross-tenant data leakage.</p>
<p>The flow:</p>
<pre><code>Client (Claude Desktop, Cursor, etc.)
  → POST /mcp
  → JSON-RPC tool call
  → API-key auth + project scoping
  → MongoDB query
  → Structured JSON back
  → AI interprets and presents
</code></pre>
<h2 id="the-tools">The tools<a class="heading-anchor" href="#the-tools" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The current tool surface:</p>
<ul>
<li><code>list_projects</code>: projects under your API key.</li>
<li><code>list_feedback</code>, <code>get_feedback</code>: query and read feedback with filters for status, type, search, and page.</li>
<li><code>create_feedback</code>, <code>update_feedback</code>, <code>delete_feedback</code>: full lifecycle. The AI can triage, change status, write resolution notes, soft-delete.</li>
<li><code>list_ratings</code>, <code>get_rating</code>: query helpful, star, and NPS ratings.</li>
<li><code>create_rating</code>, <code>create_typed_rating</code>, <code>delete_rating</code>: write ratings of all three types.</li>
<li><code>get_rating_stats</code>, <code>get_typed_rating_stats</code>: aggregates including helpful percentages, star distributions, and NPS with promoter / passive / detractor breakdown.</li>
</ul>
<p>Every tool returns structured JSON the AI can compare and reason about.</p>
<h2 id="use-cases-i-actually-run">Use cases I actually run<a class="heading-anchor" href="#use-cases-i-actually-run" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Morning standup context. I ask “summarize feedback from the last 24 hours, flag anything critical” and get a prioritized briefing in chat instead of opening a dashboard.</p>
<p>Sprint planning. “What are the top requested feature ideas this month? Group by theme.” The AI clusters by topic and gives me a ranked list, no manual sorting in a UI.</p>
<p>Release validation. The widget tags submissions with your app version via the <code>data-version</code> attribute, so I can ask “compare feedback between v2.3 and v2.4, did the new onboarding reduce complaints?” and get a real diff, not a feeling.</p>
<p>Automated triage. Pair the MCP server with a coding agent. After deploy: “any new feedback mentioning errors since 14:30 UTC? If critical, open a GitHub issue.” That closes the loop between deploy, feedback, and action without anyone watching a dashboard.</p>
<p>Customer success. “Show me all 1-star and 2-star ratings this week, draft personalized follow-ups.” The AI already has the page, version, and message, so the replies reference the actual issue.</p>
<h2 id="why-not-just-use-the-rest-api">Why not just use the REST API?<a class="heading-anchor" href="#why-not-just-use-the-rest-api" aria-hidden="true" tabindex="-1">#</a></h2>
<p>You can. SeggWat has one. The difference is intent.</p>
<p>A REST API answers questions your code anticipated. An MCP server answers questions someone thinks up on the spot. With REST you build integrations: <code>GET /feedback?status=new&amp;limit=10</code>. With MCP you say “what’s going wrong?” and the model picks the right tools and combines the results.</p>
<p>That gap gets bigger every month.</p>
<h2 id="the-rust-side">The Rust side<a class="heading-anchor" href="#the-rust-side" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The dashboard is Rust on Axum. The MCP server runs in the same process, using the <code>rmcp</code> crate for transport. Tools are async methods on a <code>SeggwatTools</code> struct, annotated with <code>#[tool(description = "...")]</code>, and the macro generates the JSON schema, so request validation happens at compile time. Serialization is serde. No runtime surprises.</p>
<p>Lessons from building it:</p>
<ul>
<li>Tool names and descriptions are not decoration. <code>list_feedback</code> works, <code>query</code> confuses the model into pseudo-SQL. Be specific.</li>
<li>Keep responses structured but readable. Add computed fields like <code>days_since_submission</code> so the AI can reason temporally without a date library.</li>
<li>Per-project auth, not god-mode keys. MCP sessions are long-lived, and one over-permissioned key is a long-running blast radius.</li>
<li>Rate-limit like the REST API. A single prompt can fire three to five tool calls, and a curious AI can hammer your database without one.</li>
</ul>
<h2 id="where-this-is-going">Where this is going<a class="heading-anchor" href="#where-this-is-going" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The SaaS tools that survive the next few years will be the ones natively accessible to AI workflows. “Add a chatbot” is the bare minimum. Actually being AI-native looks like:</p>
<ul>
<li>Exposing your data via MCP so any assistant can query it.</li>
<li>Accepting structured input so agents can take real actions.</li>
<li>Emitting events so workflows can react in real time.</li>
</ul>
<p>A feedback tool that only works through a web dashboard loses to one that works inside the IDE, the CI pipeline, the Slack channel, and the AI assistant you already have open.</p>
<p>SeggWat’s MCP server is a bet on that. It’s already how a lot of our more technical users interact with their feedback.</p>
<h2 id="try-it">Try it<a class="heading-anchor" href="#try-it" aria-hidden="true" tabindex="-1">#</a></h2>
<ol>
<li>Sign up at seggwat.com.</li>
<li>Drop one script tag on your site.</li>
<li>Generate an API key in Settings → API.</li>
<li>Point your MCP client at <a href="https://seggwat.com/mcp?ref=oxidt.com">https://seggwat.com/mcp</a> with that key.</li>
<li>Ask questions.</li>
</ol>
<p>EU-hosted, GDPR-friendly, screenshot annotation, ideas portal, and an MCP server. $6/month, 14-day free trial.</p>
<hr />
<p><em>Originally published on the <a href="https://seggwat.com/blog/why-i-built-an-mcp-server-into-my-feedback-tool?ref=oxidt.com">SeggWat blog</a>.</em></p>
]]></content:encoded>
</item>
<item>
<title>Monitoring Air Quality with the SEN5x Rust Driver</title>
<link>https://oxidt.com/blog/monitoring-air-quality-with-the-sen5x-rust-driver</link>
<guid isPermaLink="true">https://oxidt.com/blog/monitoring-air-quality-with-the-sen5x-rust-driver</guid>
<pubDate>Wed, 1 Apr 2026 20:29:24 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>If you&apos;ve worked with Sensirion&apos;s SEN5x sensor modules, you know they pack an impressive amount of environmental sensing into a tiny package — particulate matter, VOC index, NOx index, humidity, and temperature, all over a single I2C bus…</description>
<category>Embedded</category>
<category>rust</category>
<content:encoded><![CDATA[<p>If you’ve worked with Sensirion’s SEN5x sensor modules, you know they pack an impressive amount of environmental sensing into a tiny package — particulate matter, VOC index, NOx index, humidity, and temperature, all over a single I2C bus. What’s been missing is a solid <code>no_std</code> Rust driver to talk to them. That’s why I built <a href="https://github.com/hauju/sen5x-rs?ref=oxidt.com">sen5x-rs</a>.</p>
<h2 id="what-is-the-sen5x">What is the SEN5x?<a class="heading-anchor" href="#what-is-the-sen5x" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The SEN5x is Sensirion’s environmental sensor node series. Depending on the variant, you get different measurement capabilities:</p>
<table><thead><tr><th>Variant</th><th style="text-align: center">PM1.0/2.5/4.0/10.0</th><th style="text-align: center">VOC Index</th><th style="text-align: center">NOx Index</th><th style="text-align: center">Humidity</th><th style="text-align: center">Temperature</th></tr></thead><tbody>
<tr><td>SEN50</td><td style="text-align: center">✓</td><td style="text-align: center">—</td><td style="text-align: center">—</td><td style="text-align: center">—</td><td style="text-align: center">—</td></tr>
<tr><td>SEN54</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td><td style="text-align: center">—</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td></tr>
<tr><td>SEN55</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td><td style="text-align: center">✓</td></tr>
</tbody></table>
<p>The SEN55 is the most capable model, giving you a full picture of indoor air quality from a single sensor. All variants communicate over I2C at address <code>0x69</code>.</p>
<h2 id="the-driver">The Driver<a class="heading-anchor" href="#the-driver" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The <code>sen5x</code> crate provides a complete, type-safe interface for all three sensor variants. It’s built on <code>embedded-hal</code> 1.0, works in <code>no_std</code> environments, and comes with full async support via <code>embedded-hal-async</code>.</p>
<p>Add it to your project:</p>
<pre><code class="language-toml">[dependencies]
sen5x = &quot;0.2&quot;

# For async support (e.g., with Embassy)
# sen5x = { version = &quot;0.2&quot;, features = [&quot;embedded-hal-async&quot;] }
</code></pre>
<h3 id="features">Features<a class="heading-anchor" href="#features" aria-hidden="true" tabindex="-1">#</a></h3>
<ul>
<li><strong>Blocking and async APIs</strong> — full feature parity between <code>Sen5x</code> and <code>Sen5xAsync</code></li>
<li><strong><code>no_std</code> by default</strong> — runs on bare-metal microcontrollers without heap allocation</li>
<li><strong>CRC validation</strong> — every I2C read is verified using Sensirion’s CRC8 checksum</li>
<li><strong>defmt support</strong> — opt-in embedded logging with the <code>defmt</code> feature</li>
<li><strong>Command safety</strong> — the driver tracks measurement state internally and prevents invalid command sequences</li>
</ul>
<h2 id="getting-started">Getting Started<a class="heading-anchor" href="#getting-started" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Here’s a minimal example reading air quality data on a Raspberry Pi using <code>linux-embedded-hal</code>:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-keyword hl-other hl-rust">use</span> <span class="hl-meta hl-path hl-rust">linux_embedded_hal<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>Delay<span class="hl-punctuation hl-separator hl-rust">,</span> I2cdev</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
<span class="hl-keyword hl-other hl-rust">use</span> <span class="hl-meta hl-path hl-rust">sen5x<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Sen5x<span class="hl-punctuation hl-terminator hl-rust">;</span>

<span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-rust"><span class="hl-storage hl-type hl-function hl-rust">fn</span> </span><span class="hl-entity hl-name hl-function hl-rust">main</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-begin hl-rust">(</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-end hl-rust">)</span></span></span></span><span class="hl-meta hl-function hl-rust"> <span class="hl-meta hl-function hl-return-type hl-rust"><span class="hl-punctuation hl-separator hl-rust">-&gt;</span> <span class="hl-meta hl-generic hl-rust">Result<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span>, <span class="hl-meta hl-generic hl-rust">Box<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span>dyn <span class="hl-meta hl-path hl-rust">std<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-path hl-rust">error<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Error<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span><span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span></span> </span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-storage hl-type hl-rust">let</span> i2c <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-meta hl-path hl-rust">I2cdev<span class="hl-punctuation hl-accessor hl-rust">::</span></span>new<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>/dev/i2c-1<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-storage hl-modifier hl-rust">mut</span> sensor <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-meta hl-path hl-rust">Sen5x<span class="hl-punctuation hl-accessor hl-rust">::</span></span>new<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>i2c<span class="hl-punctuation hl-separator hl-rust">,</span> Delay</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Reset and give the sensor time to boot
</span>    sensor.<span class="hl-support hl-function hl-rust">device_reset</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    Delay.<span class="hl-support hl-function hl-rust">delay_ms</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-integer hl-decimal hl-rust">200</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Print device info
</span>    <span class="hl-storage hl-type hl-rust">let</span> name <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">product_name</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-storage hl-type hl-rust">let</span> serial <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">serial_number</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-storage hl-type hl-rust">let</span> version <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">version</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-support hl-macro hl-rust">println!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>Product: <span class="hl-constant hl-other hl-placeholder hl-rust">{}</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-separator hl-rust">,</span> <span class="hl-meta hl-path hl-rust">core<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-path hl-rust">str<span class="hl-punctuation hl-accessor hl-rust">::</span></span>from_utf8<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-keyword hl-operator hl-rust">&amp;</span>name</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.<span class="hl-support hl-function hl-rust">trim_end_matches</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-string hl-quoted hl-single hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&#39;</span><span class="hl-constant hl-character hl-escape hl-rust">\0</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&#39;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-support hl-macro hl-rust">println!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>Serial:  <span class="hl-constant hl-other hl-placeholder hl-rust">{}</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-separator hl-rust">,</span> <span class="hl-meta hl-path hl-rust">core<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-path hl-rust">str<span class="hl-punctuation hl-accessor hl-rust">::</span></span>from_utf8<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-keyword hl-operator hl-rust">&amp;</span>serial</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.<span class="hl-support hl-function hl-rust">trim_end_matches</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-string hl-quoted hl-single hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&#39;</span><span class="hl-constant hl-character hl-escape hl-rust">\0</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&#39;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-support hl-macro hl-rust">println!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>Firmware: <span class="hl-constant hl-other hl-placeholder hl-rust">{}</span>.<span class="hl-constant hl-other hl-placeholder hl-rust">{}</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-separator hl-rust">,</span> version.firmware_major<span class="hl-punctuation hl-separator hl-rust">,</span> version.firmware_minor<span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Start measuring
</span>    sensor.<span class="hl-support hl-function hl-rust">start_measurement</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-keyword hl-control hl-rust">loop</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        Delay.<span class="hl-support hl-function hl-rust">delay_ms</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-integer hl-decimal hl-rust">1000</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

        <span class="hl-keyword hl-control hl-rust">if</span> sensor.<span class="hl-support hl-function hl-rust">data_ready</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
            <span class="hl-storage hl-type hl-rust">let</span> data <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">measurement</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
            <span class="hl-support hl-macro hl-rust">println!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust">
                <span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>PM2.5: <span class="hl-constant hl-other hl-placeholder hl-rust">{:.1}</span> µg/m³ | Temp: <span class="hl-constant hl-other hl-placeholder hl-rust">{:.1}</span> °C | Humidity: <span class="hl-constant hl-other hl-placeholder hl-rust">{:.1}</span> % | VOC: <span class="hl-constant hl-other hl-placeholder hl-rust">{:.0}</span> | NOx: <span class="hl-constant hl-other hl-placeholder hl-rust">{:.0}</span><span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-separator hl-rust">,</span>
                data.mass_concentration_pm2p5<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.ambient_temperature<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.ambient_humidity<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.voc_index<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.nox_index<span class="hl-punctuation hl-separator hl-rust">,</span>
            <span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
        </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>The sensor updates every second. After calling <code>start_measurement()</code>, poll <code>data_ready()</code> and read with <code>measurement()</code> when new data is available.</p>
<h2 id="async-on-embedded-esp32-c3-with-embassy">Async on Embedded — ESP32-C3 with Embassy<a class="heading-anchor" href="#async-on-embedded-esp32-c3-with-embassy" aria-hidden="true" tabindex="-1">#</a></h2>
<p>For microcontroller targets, the async driver is the way to go. Here’s how it looks on an ESP32-C3 using Embassy:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-keyword hl-other hl-rust">use</span> <span class="hl-meta hl-path hl-rust">embassy_time<span class="hl-punctuation hl-accessor hl-rust">::</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>Duration<span class="hl-punctuation hl-separator hl-rust">,</span> Timer</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
<span class="hl-keyword hl-other hl-rust">use</span> <span class="hl-meta hl-path hl-rust">sen5x<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Sen5xAsync<span class="hl-punctuation hl-terminator hl-rust">;</span>

<span class="hl-meta hl-annotation hl-rust"><span class="hl-punctuation hl-definition hl-annotation hl-rust">#</span><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">[</span><span class="hl-variable hl-annotation hl-rust">embassy_executor</span>::<span class="hl-variable hl-annotation hl-rust">task</span><span class="hl-punctuation hl-section hl-group hl-end hl-rust">]</span></span>
async <span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-rust"><span class="hl-storage hl-type hl-function hl-rust">fn</span> </span><span class="hl-entity hl-name hl-function hl-rust">air_quality_task</span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-begin hl-rust">(</span><span class="hl-variable hl-parameter hl-rust">i2c</span><span class="hl-punctuation hl-separator hl-rust">:</span> <span class="hl-meta hl-generic hl-rust">I2c<span class="hl-punctuation hl-definition hl-generic hl-begin hl-rust">&lt;</span><span class="hl-storage hl-modifier hl-lifetime hl-rust">&#39;static</span>, Async<span class="hl-punctuation hl-definition hl-generic hl-end hl-rust">&gt;</span></span></span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-function hl-parameters hl-rust"><span class="hl-punctuation hl-section hl-parameters hl-end hl-rust">)</span></span></span></span><span class="hl-meta hl-function hl-rust"> </span><span class="hl-meta hl-function hl-rust"><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    <span class="hl-storage hl-type hl-rust">let</span> <span class="hl-storage hl-modifier hl-rust">mut</span> sensor <span class="hl-keyword hl-operator hl-rust">=</span> <span class="hl-meta hl-path hl-rust">Sen5xAsync<span class="hl-punctuation hl-accessor hl-rust">::</span></span>new<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>i2c<span class="hl-punctuation hl-separator hl-rust">,</span> <span class="hl-meta hl-path hl-rust">embassy_time<span class="hl-punctuation hl-accessor hl-rust">::</span></span>Delay</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    sensor.<span class="hl-support hl-function hl-rust">device_reset</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
    <span class="hl-meta hl-path hl-rust">Timer<span class="hl-punctuation hl-accessor hl-rust">::</span></span>after_millis<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-integer hl-decimal hl-rust">200</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>

    sensor.<span class="hl-support hl-function hl-rust">start_measurement</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>

    <span class="hl-keyword hl-control hl-rust">loop</span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
        <span class="hl-meta hl-path hl-rust">Timer<span class="hl-punctuation hl-accessor hl-rust">::</span></span>after<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-meta hl-path hl-rust">Duration<span class="hl-punctuation hl-accessor hl-rust">::</span></span>from_secs<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-integer hl-decimal hl-rust">1</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await<span class="hl-punctuation hl-terminator hl-rust">;</span>

        <span class="hl-keyword hl-control hl-rust">if</span> sensor.<span class="hl-support hl-function hl-rust">data_ready</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span> <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
            <span class="hl-storage hl-type hl-rust">let</span> data <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">measurement</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span>.await.<span class="hl-support hl-function hl-rust">unwrap</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
            <span class="hl-meta hl-path hl-rust">defmt<span class="hl-punctuation hl-accessor hl-rust">::</span></span>info<span class="hl-keyword hl-operator hl-rust">!</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>
                <span class="hl-string hl-quoted hl-double hl-rust"><span class="hl-punctuation hl-definition hl-string hl-begin hl-rust">&quot;</span>PM2.5: {} | Temp: {} | VOC: {}<span class="hl-punctuation hl-definition hl-string hl-end hl-rust">&quot;</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>
                data.mass_concentration_pm2p5<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.ambient_temperature<span class="hl-punctuation hl-separator hl-rust">,</span>
                data.voc_index<span class="hl-punctuation hl-separator hl-rust">,</span>
            </span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-terminator hl-rust">;</span>
        </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
    </span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span>
</span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>The API is identical to the blocking version — just add <code>.await</code>. No separate learning curve.</p>
<h2 id="beyond-basic-measurements">Beyond Basic Measurements<a class="heading-anchor" href="#beyond-basic-measurements" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The driver exposes the full SEN5x feature set, not just the basics:</p>
<p><strong>Fan management</strong> — trigger manual cleaning cycles or configure the auto-cleaning interval:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust">sensor.<span class="hl-support hl-function hl-rust">start_fan_cleaning</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
<span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> or
</span>sensor.<span class="hl-support hl-function hl-rust">set_fan_auto_cleaning_interval</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-integer hl-decimal hl-rust">604800</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span> <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> weekly
</span></span></code></pre>
<p><strong>Temperature compensation</strong> — correct for self-heating or enclosure effects:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust">sensor.<span class="hl-support hl-function hl-rust">set_temperature_offset_simple</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span><span class="hl-constant hl-numeric hl-float hl-rust">2.</span><span class="hl-constant hl-numeric hl-float hl-rust">5</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span> <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> offset in °C
</span></span></code></pre>
<p><strong>VOC/NOx algorithm tuning</strong> — adjust the on-chip gas index algorithm parameters for your specific environment.</p>
<p><strong>Algorithm state backup</strong> — save and restore the VOC algorithm state across power cycles. This avoids the 12-hour learning phase after every reboot:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-storage hl-type hl-rust">let</span> state <span class="hl-keyword hl-operator hl-rust">=</span> sensor.<span class="hl-support hl-function hl-rust">voc_algorithm_state</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span></span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
<span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> persist `state` to flash/EEPROM
</span><span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> ... after reboot:
</span>sensor.<span class="hl-support hl-function hl-rust">set_voc_algorithm_state</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>state</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-keyword hl-operator hl-rust">?</span><span class="hl-punctuation hl-terminator hl-rust">;</span>
</span></code></pre>
<h2 id="error-handling">Error Handling<a class="heading-anchor" href="#error-handling" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Errors are generic over the underlying I2C implementation, keeping the driver platform-agnostic:</p>
<pre><code class="language-rust"><span class="hl-source hl-rust"><span class="hl-meta hl-enum hl-rust"><span class="hl-storage hl-modifier hl-rust">pub</span> <span class="hl-storage hl-type hl-enum hl-rust">enum</span> <span class="hl-entity hl-name hl-enum hl-rust">Error</span>&lt;E&gt; <span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-begin hl-rust">{</span>
    I2c<span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-begin hl-rust">(</span>E</span><span class="hl-meta hl-group hl-rust"><span class="hl-punctuation hl-section hl-group hl-end hl-rust">)</span></span><span class="hl-punctuation hl-separator hl-rust">,</span>       <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Underlying bus error
</span>    Crc<span class="hl-punctuation hl-separator hl-rust">,</span>          <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Data integrity failure
</span>    NotAllowed<span class="hl-punctuation hl-separator hl-rust">,</span>   <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Command invalid in current state
</span>    Internal<span class="hl-punctuation hl-separator hl-rust">,</span>     <span class="hl-comment hl-line hl-double-slash hl-rust"><span class="hl-punctuation hl-definition hl-comment hl-rust">//</span> Sensor reported a fault
</span></span><span class="hl-meta hl-block hl-rust"><span class="hl-punctuation hl-section hl-block hl-end hl-rust">}</span></span></span>
</span></code></pre>
<p>Enable the <code>thiserror</code> feature for <code>std::error::Error</code> integration, or <code>defmt</code> for embedded-friendly formatting.</p>
<h2 id="links">Links<a class="heading-anchor" href="#links" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li><strong>Crate</strong>: <a href="https://crates.io/crates/sen5x?ref=oxidt.com">crates.io/crates/sen5x</a></li>
<li><strong>Source</strong>: <a href="https://github.com/hauju/sen5x-rs?ref=oxidt.com">github.com/hauju/sen5x-rs</a></li>
<li><strong>Docs</strong>: <a href="https://docs.rs/sen5x?ref=oxidt.com">docs.rs/sen5x</a></li>
<li><strong>Datasheet</strong>: <a href="https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf?ref=oxidt.com">Sensirion SEN5x Datasheet</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Sentry CLI for Agentic Debugging</title>
<link>https://oxidt.com/blog/sentry-cli-for-agentic-debugging</link>
<guid isPermaLink="true">https://oxidt.com/blog/sentry-cli-for-agentic-debugging</guid>
<pubDate>Sat, 14 Feb 2026 14:11:35 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>Debugging production errors used to mean switching between your IDE, Sentry&apos;s web UI, and a dozen browser tabs. You&apos;d copy issue IDs, dig through stack traces, cross-reference with your code, and lose context every time you switched…</description>
<content:encoded><![CDATA[<p>Debugging production errors used to mean switching between your IDE, Sentry’s web UI, and a dozen browser tabs. You’d copy issue IDs, dig through stack traces, cross-reference with your code, and lose context every time you switched windows.</p>
<p>Now there’s a better way. Sentry’s new CLI and MCP server let AI coding agents like Claude Code query your Sentry data directly from the terminal. No tab switching. No copy-pasting. Just ask “what’s breaking in production?” and get answers with full stack traces, right where you write code.</p>
<p>Here’s how to set it up and use it.</p>
<h2 id="what-changed-the-new-sentry-cli">What Changed: The New Sentry CLI<a class="heading-anchor" href="#what-changed-the-new-sentry-cli" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Sentry now has two CLIs. The legacy <code>sentry-cli</code> is the build tool you know — it handles source maps, releases, and debug symbols in CI/CD pipelines. That one stays.</p>
<p>The <strong>new</strong> <code>sentry</code> CLI is different. It’s an interactive developer tool built for both humans and AI agents. It can list issues, inspect events, query the API, and even call Sentry’s AI (Seer) for root cause analysis — all from your terminal.</p>
<p>The key distinction: the new CLI is designed to be <em>used by your coding agent</em> as much as by you.</p>
<h2 id="installation">Installation<a class="heading-anchor" href="#installation" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Install the new CLI:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">curl</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> https://cli.sentry.dev/install<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>fsS</span></span> <span class="hl-keyword hl-operator hl-logical hl-pipe hl-shell">|</span> <span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">bash</span></span>
</span></code></pre>
<p>Or if you prefer a package manager:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> bun</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">bun</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> add<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>g</span> sentry</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> npm</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">npm</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> install<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>g</span> sentry</span>
</span></code></pre>
<p>Then authenticate:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sentry</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> auth login</span>
</span></code></pre>
<p>This opens a browser for OAuth. Once you authorize, credentials are stored in <code>~/.sentry/config.json</code>. Every agent tool that wraps this CLI inherits your auth — no extra tokens to manage.</p>
<p>Verify it works:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sentry</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> auth status</span>
</span></code></pre>
<h2 id="option-1-register-as-a-claude-code-skill">Option 1: Register as a Claude Code Skill<a class="heading-anchor" href="#option-1-register-as-a-claude-code-skill" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The fastest way to give Claude Code access to Sentry:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">npx</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> skills add https://cli.sentry.dev</span>
</span></code></pre>
<p>That’s it. Claude Code can now:</p>
<ul>
<li>List and inspect Sentry issues</li>
<li>View full error events with stack traces</li>
<li>Browse your projects and organizations</li>
<li>Execute arbitrary Sentry API calls</li>
</ul>
<p>This approach uses the CLI under the hood, so it inherits whatever authentication you’ve already set up.</p>
<h2 id="option-2-sentry-mcp-server-more-power">Option 2: Sentry MCP Server (More Power)<a class="heading-anchor" href="#option-2-sentry-mcp-server-more-power" aria-hidden="true" tabindex="-1">#</a></h2>
<p>For deeper integration, Sentry offers a full MCP (Model Context Protocol) server with 16+ tools. This gives agents structured access to more of Sentry’s capabilities.</p>
<h3 id="remote-mode-easiest">Remote Mode (Easiest)<a class="heading-anchor" href="#remote-mode-easiest" aria-hidden="true" tabindex="-1">#</a></h3>
<p>No installation needed. Add this to your Claude Code MCP config:</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
  <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>mcpServers<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
    <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>sentry<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
      <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>url<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>https://mcp.sentry.dev/mcp<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>
    </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
  </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span></code></pre>
<p>On first connection, you authenticate via OAuth in your browser. The server is always up to date since Sentry hosts it.</p>
<h3 id="local-stdio-mode-self-hosted-offline">Local Stdio Mode (Self-Hosted / Offline)<a class="heading-anchor" href="#local-stdio-mode-self-hosted-offline" aria-hidden="true" tabindex="-1">#</a></h3>
<p>If you run a self-hosted Sentry instance or need local control:</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
  <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>mcpServers<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
    <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>sentry<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
      <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>command<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>npx<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
      <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>args<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-array hl-json"><span class="hl-punctuation hl-section hl-array hl-begin hl-json">[</span><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>@sentry/mcp-server<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-section hl-array hl-end hl-json">]</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
      <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>env<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>SENTRY_ACCESS_TOKEN<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>your-sentry-user-auth-token<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>
      </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
    </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
  </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span></code></pre>
<p>For self-hosted deployments, add <code>--host=sentry.example.com</code> to the args.</p>
<p>Your token needs these scopes: <code>org:read</code>, <code>project:read</code>, <code>project:write</code>, <code>team:read</code>, <code>team:write</code>, <code>event:write</code>.</p>
<h3 id="claude-code-plugin">Claude Code Plugin<a class="heading-anchor" href="#claude-code-plugin" aria-hidden="true" tabindex="-1">#</a></h3>
<p>You can also install it as a Claude Code plugin:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> plugin marketplace add getsentry/sentry-mcp</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> plugin install sentry-mcp@sentry-mcp</span>
</span></code></pre>
<h2 id="real-debugging-workflows">Real Debugging Workflows<a class="heading-anchor" href="#real-debugging-workflows" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Here’s where it gets practical. Once your agent has access to Sentry, you can debug production errors without leaving your editor.</p>
<h3 id="what-s-broken-right-now">“What’s broken right now?”<a class="heading-anchor" href="#what-s-broken-right-now" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Just ask:</p>
<blockquote>
<p>“Show me the latest unresolved issues in my project”</p>
</blockquote>
<p>The agent queries Sentry, returns a list of issues with titles, frequency, and first/last seen timestamps. No browser needed.</p>
<h3 id="investigate-a-specific-error">Investigate a Specific Error<a class="heading-anchor" href="#investigate-a-specific-error" aria-hidden="true" tabindex="-1">#</a></h3>
<blockquote>
<p>“What’s the full stack trace for PROJ-1234?”</p>
</blockquote>
<p>The agent pulls the latest event for that issue, showing the complete stack trace, breadcrumbs, tags, and context. You’re looking at the same data you’d see in Sentry’s web UI, but right in your terminal alongside your code.</p>
<h3 id="ai-powered-root-cause-analysis">AI-Powered Root Cause Analysis<a class="heading-anchor" href="#ai-powered-root-cause-analysis" aria-hidden="true" tabindex="-1">#</a></h3>
<p>This is the killer feature. Sentry’s Seer AI can analyze issues deeply:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> From the CLI directly</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sentry</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> issue explain PROJ-1234</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Or ask your agent</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-keyword hl-operator hl-assignment hl-redirection hl-shell">&gt;</span> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>Use Sentry&#39;s Seer to analyze issue PROJ-1234 and suggest a fix<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
</span></code></pre>
<p>Seer doesn’t just show you the stack trace — it analyzes the error pattern, identifies the likely root cause, and generates a step-by-step fix plan. When invoked through your coding agent, it can even propose code changes in context.</p>
<h3 id="cross-reference-errors-with-your-code">Cross-Reference Errors with Your Code<a class="heading-anchor" href="#cross-reference-errors-with-your-code" aria-hidden="true" tabindex="-1">#</a></h3>
<blockquote>
<p>“Check Sentry for errors in src/api/auth.ts and propose fixes”</p>
</blockquote>
<p>The agent searches for Sentry issues related to that file, pulls the relevant stack traces, and suggests fixes based on both the error data and your actual source code. This is the workflow that used to take 20 minutes of tab-switching, now handled in a single prompt.</p>
<h3 id="monitor-in-real-time">Monitor in Real-Time<a class="heading-anchor" href="#monitor-in-real-time" aria-hidden="true" tabindex="-1">#</a></h3>
<p>The new CLI supports log streaming:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sentry</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> log list<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>f</span></span>
</span></code></pre>
<p>The <code>-f</code> flag works like <code>tail -f</code> — it streams new log entries as they arrive. Useful during deployments or when you’re trying to reproduce an issue.</p>
<h2 id="tips-for-effective-agentic-debugging">Tips for Effective Agentic Debugging<a class="heading-anchor" href="#tips-for-effective-agentic-debugging" aria-hidden="true" tabindex="-1">#</a></h2>
<p><strong>Be specific with project names.</strong> If you have multiple projects, include the project name or slug in your prompt. The CLI auto-detects from your codebase when possible, but being explicit avoids ambiguity.</p>
<p><strong>Use JSON output for scripting.</strong> Every CLI command supports <code>--json</code>:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sentry</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> issue list<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> --</span>json</span></span> <span class="hl-keyword hl-operator hl-logical hl-pipe hl-shell">|</span> <span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">jq</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> <span class="hl-string hl-quoted hl-single hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&#39;</span>.[0].title<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&#39;</span></span></span>
</span></code></pre>
<p><strong>Combine with your existing tools.</strong> Pipe Sentry data to <code>jq</code>, <code>fzf</code>, or any other CLI tool. The structured output is designed for composition.</p>
<p><strong>Let the agent fix, not just find.</strong> The real power isn’t just seeing errors — it’s having the agent read the stack trace, find the relevant code, and propose a fix in one flow. Don’t stop at “show me the error.”</p>
<h2 id="mcp-vs-cli-skills-vs-seer-when-to-use-what">MCP vs CLI Skills vs Seer — When to Use What<a class="heading-anchor" href="#mcp-vs-cli-skills-vs-seer-when-to-use-what" aria-hidden="true" tabindex="-1">#</a></h2>
<table><thead><tr><th>Approach</th><th>Best For</th></tr></thead><tbody>
<tr><td><strong>CLI Skill</strong></td><td>Quick setup, basic issue browsing, works with existing CLI auth</td></tr>
<tr><td><strong>MCP Server (Remote)</strong></td><td>Full-featured agent integration, 16+ tools, zero maintenance</td></tr>
<tr><td><strong>MCP Server (Local)</strong></td><td>Self-hosted Sentry, offline environments, custom configurations</td></tr>
<tr><td><strong>Seer</strong> (via CLI or MCP)</td><td>Deep root cause analysis, fix generation, complex debugging</td></tr>
</tbody></table>
<p>You don’t have to choose just one. The CLI skill is great for quick lookups, while the MCP server gives agents deeper access for complex investigations. Seer is the specialist you call in when you need AI to analyze the <em>why</em> behind an error.</p>
<h2 id="what-this-changes">What This Changes<a class="heading-anchor" href="#what-this-changes" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The traditional debugging loop — see alert, open Sentry, read stack trace, switch to IDE, find file, understand context, write fix — collapses into a single conversation with your coding agent.</p>
<p>You say “there’s a TypeError in production, fix it” and the agent:</p>
<ol>
<li>Queries Sentry for the issue</li>
<li>Reads the full stack trace and breadcrumbs</li>
<li>Opens the relevant file in your codebase</li>
<li>Understands the context around the failing code</li>
<li>Proposes (or writes) a fix</li>
</ol>
<p>That’s the promise of agentic debugging. And with Sentry’s CLI and MCP server, it actually works today.</p>
<hr />
<p><em>Get started: <a href="https://cli.sentry.dev/?ref=oxidt.com">cli.sentry.dev</a> | MCP Server: <a href="https://docs.sentry.io/product/sentry-mcp/?ref=oxidt.com">docs.sentry.io/product/sentry-mcp</a></em></p>
]]></content:encoded>
</item>
<item>
<title>Stop Committing .env Files. AI Is Reading Your Repo Now.</title>
<link>https://oxidt.com/blog/stop-committing-env-files-ai-is-reading-your-repo-now</link>
<guid isPermaLink="true">https://oxidt.com/blog/stop-committing-env-files-ai-is-reading-your-repo-now</guid>
<pubDate>Sun, 1 Feb 2026 10:48:29 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>A practical guide to using justfile + Infisical for developer workflows that don&apos;t leak secrets.</description>
<content:encoded><![CDATA[<p><em>A practical guide to using justfile + Infisical for developer workflows that don’t leak secrets.</em></p>
<hr />
<p>Every developer has done it. You clone a repo, copy <code>.env.example</code> to <code>.env</code>, fill in your secrets, and get to work. Maybe you’ve even accidentally committed that <code>.env</code> file once. GitHub helpfully reminds you that you just published your database credentials to the world.</p>
<p>But here’s the thing most teams haven’t caught up with yet: <strong>your codebase is no longer just read by humans and CI pipelines.</strong></p>
<p>AI coding assistants — Claude Code, GitHub Copilot, Cursor, Cody — now read your entire repository as context. Every file. Every config. Every <code>.env.local</code> that “shouldn’t be there but works fine for now.”</p>
<h2 id="the-new-threat-model">The new threat model<a class="heading-anchor" href="#the-new-threat-model" aria-hidden="true" tabindex="-1">#</a></h2>
<p>When you use an AI coding tool, it typically ingests:</p>
<ul>
<li>Your entire project structure</li>
<li>Configuration files</li>
<li>Environment files if they exist in the repo</li>
<li>Docker compose files with hardcoded credentials</li>
<li>Any file that isn’t in <code>.gitignore</code> — and sometimes even those</li>
</ul>
<p>This isn’t a vulnerability in the AI tools. It’s the whole point: they need context to help you code. But if your secrets live in your repo as plain text files, they become part of that context. They get sent to APIs. They appear in autocomplete suggestions. They end up in log files.</p>
<p><strong>The <code>.env</code> file was designed for a world where only your local machine read your code. That world is gone.</strong></p>
<h2 id="what-we-do-instead-justfile-infisical">What we do instead: justfile + Infisical<a class="heading-anchor" href="#what-we-do-instead-justfile-infisical" aria-hidden="true" tabindex="-1">#</a></h2>
<p>We use a combination of <a href="https://github.com/casey/just?ref=oxidt.com">just</a> (a modern command runner) and <a href="https://infisical.com/?ref=oxidt.com">Infisical</a> (an open-source secrets manager) to keep secrets out of the repository entirely.</p>
<p>Here’s what a typical <code>justfile</code> looks like:</p>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">init</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> docker compose up -d</span></span>

</span><span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> cargo run</span></span>

</span><span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">test</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> cargo test</span></span>

</span><span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">migrate</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npx prisma migrate dev</span></span>
</span></span></code></pre>
<p>Notice <code>infisical run --</code> before every command that needs environment variables. That prefix pulls secrets from Infisical’s vault, injects them as environment variables <em>for that process only</em>, and never writes them to disk.</p>
<p>No <code>.env</code> file. No secrets in the repo. No secrets in your shell history. No secrets for AI tools to accidentally ingest.</p>
<h2 id="why-justfile-over-makefile-or-npm-scripts">Why justfile over Makefile or npm scripts?<a class="heading-anchor" href="#why-justfile-over-makefile-or-npm-scripts" aria-hidden="true" tabindex="-1">#</a></h2>
<p><code>just</code> is a command runner, not a build system. The distinction matters:</p>
<p><strong>1. It does one thing well.</strong> No implicit rules, no dependency graphs, no tab-vs-space wars. You write commands. You run them.</p>
<p><strong>2. It works with any language.</strong> Your monorepo has Python, TypeScript, Go, and a docs site? <code>just</code> doesn’t care. It runs shell commands.</p>
<p><strong>3. The syntax is readable.</strong> Compare:</p>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Makefile: Is this a build target or a command? Who knows.</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>
</span><span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">.PHONY</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span> <span class="hl-meta hl-function hl-arguments hl-makefile"><span class="hl-string hl-unquoted hl-makefile">serve</span></span><span class="hl-meta hl-function hl-body hl-makefile">
</span></span></code></pre>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> justfile: It&#39;s a command. Obviously.</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>
</span></span></code></pre>
<p><strong>4. It lists available commands.</strong> Running <code>just --list</code> gives you a summary of every task. New team member? They run <code>just --list</code> and immediately know what’s available.</p>
<p><strong>5. It supports arguments and defaults.</strong> Need to run migrations against different databases?</p>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-variable hl-other hl-makefile">migrate env</span><span class="hl-keyword hl-operator hl-assignment hl-makefile">=</span><span class="hl-string hl-unquoted hl-makefile">&quot;development&quot;:</span>
<span class="hl-variable hl-other hl-makefile">    infisical run --env</span><span class="hl-keyword hl-operator hl-assignment hl-makefile">=</span><span class="hl-string hl-unquoted hl-makefile">{{env}} -- npx prisma migrate deploy</span>
</span></code></pre>
<p>Now <code>just migrate</code> targets development, and <code>just migrate staging</code> targets staging. Try doing that cleanly in a Makefile.</p>
<h2 id="why-infisical-or-any-secrets-manager-over-env-files">Why Infisical (or any secrets manager) over .env files?<a class="heading-anchor" href="#why-infisical-or-any-secrets-manager-over-env-files" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The <code>.env</code> pattern has three fundamental problems:</p>
<h3 id="problem-1-env-files-multiply">Problem 1: .env files multiply<a class="heading-anchor" href="#problem-1-env-files-multiply" aria-hidden="true" tabindex="-1">#</a></h3>
<p>It starts with one <code>.env</code>. Then you need <code>.env.local</code>, <code>.env.development</code>, <code>.env.production</code>, <code>.env.test</code>, <code>.env.staging</code>. Then someone adds <code>.env.example</code> that’s always out of date. Then someone adds <code>secrets.json</code> for a different service.</p>
<p>With Infisical, you have one source of truth with environments (dev, staging, prod) built in. You switch environments, not files.</p>
<h3 id="problem-2-env-files-are-unshareable">Problem 2: .env files are unshareable<a class="heading-anchor" href="#problem-2-env-files-are-unshareable" aria-hidden="true" tabindex="-1">#</a></h3>
<p>New developer joins the team. “Hey, can someone send me the .env file?” Now your secrets are in Slack DMs, email threads, and shared Google Docs. Each copy is a potential leak.</p>
<p>With Infisical, new developers run <code>infisical login</code>, select their project, and they’re ready. Secrets are scoped by role — the intern doesn’t get production database credentials.</p>
<h3 id="problem-3-env-files-have-no-audit-trail">Problem 3: .env files have no audit trail<a class="heading-anchor" href="#problem-3-env-files-have-no-audit-trail" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Who changed the API key? When? Why? With <code>.env</code> files, you’ll never know. Someone overwrites it, someone copies the wrong value, and three hours later you’re debugging a production issue because staging credentials ended up in prod.</p>
<p>Infisical provides version history, access logs, and change tracking for every secret.</p>
<h2 id="the-ai-angle-is-real-not-hypothetical">The AI angle is real, not hypothetical<a class="heading-anchor" href="#the-ai-angle-is-real-not-hypothetical" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Consider what happens with a typical AI-assisted workflow:</p>
<ol>
<li>Developer has <code>.env</code> with <code>DB_URL=mongodb://root:p4ssw0rd@prod-server:27017/</code></li>
<li>Developer asks Claude Code: “Help me debug the database connection”</li>
<li>The AI reads the project context, including <code>.env</code></li>
<li>The secret is now in the API request to the AI provider</li>
</ol>
<p>With <code>infisical run --</code>, that <code>.env</code> file doesn’t exist. The AI sees a clean <code>justfile</code> with <code>infisical run --</code> prefixes. It understands the pattern. It can help you debug without ever seeing your actual credentials.</p>
<p><strong>Your secrets stay where they belong: in the vault, not in context windows.</strong></p>
<h2 id="setting-it-up-15-lines-to-a-secure-workflow">Setting it up: 15 lines to a secure workflow<a class="heading-anchor" href="#setting-it-up-15-lines-to-a-secure-workflow" aria-hidden="true" tabindex="-1">#</a></h2>
<h3 id="1-install-just-and-infisical">1. Install just and Infisical<a class="heading-anchor" href="#1-install-just-and-infisical" aria-hidden="true" tabindex="-1">#</a></h3>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> macOS</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">brew</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> install just infisical/get-cli/infisical</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Linux</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">curl</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> --</span>proto</span> <span class="hl-string hl-quoted hl-single hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&#39;</span>=https<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&#39;</span></span><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> --</span>tlsv1</span>.2<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>sSf</span> https://just.systems/install.sh</span> <span class="hl-keyword hl-operator hl-logical hl-pipe hl-shell">|</span> <span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">bash</span></span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">curl</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>1sLf</span> <span class="hl-string hl-quoted hl-single hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&#39;</span>https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&#39;</span></span></span> <span class="hl-keyword hl-operator hl-logical hl-pipe hl-shell">|</span> <span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sudo</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> bash</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">sudo</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> apt-get install infisical</span>
</span></code></pre>
<h3 id="2-login-and-initialize">2. Login and initialize<a class="heading-anchor" href="#2-login-and-initialize" aria-hidden="true" tabindex="-1">#</a></h3>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> login</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> init    <span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Links your repo to an Infisical project</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span></span></span></code></pre>
<h3 id="3-create-your-justfile">3. Create your justfile<a class="heading-anchor" href="#3-create-your-justfile" aria-hidden="true" tabindex="-1">#</a></h3>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Start all services</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">init</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> docker compose up -d</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Run your app</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Run tests</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">test</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm test</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Run with a specific environment</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve-staging</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> --</span>env</span><span class="hl-keyword hl-operator hl-assignment hl-option hl-shell">=</span>staging<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>
</span></span></code></pre>
<h3 id="4-add-env-to-gitignore-if-you-haven-t-already">4. Add .env to .gitignore (if you haven’t already)<a class="heading-anchor" href="#4-add-env-to-gitignore-if-you-haven-t-already" aria-hidden="true" tabindex="-1">#</a></h3>
<pre><code>.env
**/*.env
secrets.json
</code></pre>
<p>That’s it. From now on, <code>just serve</code> handles everything.</p>
<h2 id="the-team-workflow">The team workflow<a class="heading-anchor" href="#the-team-workflow" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Here’s what the daily workflow looks like:</p>
<ul>
<li><strong>Morning:</strong> <code>just init</code> to start services, <code>just serve</code> to run the app. No copying files, no asking teammates for secrets.</li>
<li><strong>New feature needs a new secret:</strong> Add it in Infisical’s dashboard. Team gets it automatically next time they run any <code>just</code> command.</li>
<li><strong>New team member:</strong> <code>infisical login</code>, <code>just serve</code>. Done.</li>
<li><strong>AI pair programming:</strong> Ask Claude Code anything. It sees your <code>justfile</code>, understands the pattern, and helps you without ever accessing your secrets.</li>
</ul>
<h2 id="common-objections">Common objections<a class="heading-anchor" href="#common-objections" aria-hidden="true" tabindex="-1">#</a></h2>
<p><strong>“But Infisical adds a network dependency.”</strong> True. If Infisical is down, <code>infisical run</code> fails. For CI/CD, you can use Infisical’s service tokens with caching. For local dev, the CLI caches secrets locally (encrypted). It’s not a hard dependency for offline work.</p>
<p><strong>“My team is small, .env files are fine.”</strong> Today. But when you add your first AI coding tool — and you will — your threat surface changes overnight. Setting this up takes less time than recovering from a leaked secret.</p>
<p><strong>“Can I use something other than Infisical?”</strong> Absolutely. The pattern works with any secrets manager:</p>
<pre><code class="language-makefile"><span class="hl-source hl-makefile"><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Infisical</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">infisical</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> Doppler</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">doppler</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> 1Password CLI</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">op</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> run<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>

</span><span class="hl-comment hl-line hl-number-sign hl-makefile"><span class="hl-punctuation hl-definition hl-comment hl-makefile">#</span> AWS Secrets Manager (via chamber)</span>
<span class="hl-meta hl-function hl-makefile"><span class="hl-entity hl-name hl-function hl-makefile">serve</span></span><span class="hl-keyword hl-operator hl-assignment hl-makefile">:</span>
<span class="hl-meta hl-function hl-arguments hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile"></span><span class="hl-meta hl-function hl-body hl-makefile">    <span class="hl-source hl-shell"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">chamber</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> exec production<span class="hl-keyword hl-operator hl-end-of-options hl-shell"> --</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> npm run dev</span></span>
</span></span></code></pre>
<p>The tool doesn’t matter. The pattern does: <strong>secrets injected at runtime, never stored in files.</strong></p>
<h2 id="the-bottom-line">The bottom line<a class="heading-anchor" href="#the-bottom-line" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The combination of <code>justfile</code> + a secrets manager gives you:</p>
<ul>
<li><strong>Zero secrets in your repo</strong> for AI tools (or anyone else) to find</li>
<li><strong>One command</strong> to run anything: <code>just serve</code>, <code>just test</code>, <code>just deploy</code></li>
<li><strong>Consistent environments</strong> across the team without manual file copying</li>
<li><strong>Audit trails</strong> for who accessed which secrets and when</li>
<li><strong>Language-agnostic task running</strong> that works for any project in your monorepo</li>
</ul>
<p>Your <code>.env</code> file was a good idea in 2015. In 2025, with AI reading every file in your project, it’s a liability. Replace it with a pattern that was built for how we actually work now.</p>
]]></content:encoded>
</item>
<item>
<title>E2E Testing with agent-browser</title>
<link>https://oxidt.com/blog/e2e-testing-with-agent-browser</link>
<guid isPermaLink="true">https://oxidt.com/blog/e2e-testing-with-agent-browser</guid>
<pubDate>Wed, 28 Jan 2026 12:35:05 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>I&apos;ve been experimenting with agent-browser in Claude Code, and it&apos;s changing how I think about end-to-end testing.</description>
<content:encoded><![CDATA[<p>I’ve been experimenting with agent-browser in Claude Code, and it’s changing how I think about end-to-end testing.</p>
<h3 id="the-old-way">The Old Way<a class="heading-anchor" href="#the-old-way" aria-hidden="true" tabindex="-1">#</a></h3>
<p>await page.click(‘#submit-btn-v2-new’);<br />
await page.waitForSelector(‘.modal-content &gt; div:nth-child(3) &gt; button’);</p>
<p>Selectors break. Tests become maintenance nightmares.</p>
<h3 id="the-new-way">The New Way<a class="heading-anchor" href="#the-new-way" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Write test cases that read like requirements:</p>
<pre><code>TEST-ONBOARD-001: New User Redirect

Priority: Critical
Preconditions: User with no organizations

Steps:
1. Create test scenario with user having no orgs
2. Login as that user
3. Navigate to /dashboard
4. Verify redirect to /onboarding
5. Verify plan selection cards displayed

Expected Results:
- Users without orgs redirected to onboarding
- Plan cards show Starter and Pro options
</code></pre>
<p>Hand this to agent-browser. It navigates, clicks, validates. Done.</p>
<p><strong>Why This Matters</strong></p>
<ul>
<li>
<p>Tests read like specs. PMs understand them. No translation layer.</p>
</li>
<li>
<p>Resilience to UI changes. Button renamed? Class refactored? AI adapts.</p>
</li>
<li>
<p>Faster authoring. Describe the flow, run it, ship it.</p>
</li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Continuous Learning for Claude</title>
<link>https://oxidt.com/blog/continuous-learning-for-claude</link>
<guid isPermaLink="true">https://oxidt.com/blog/continuous-learning-for-claude</guid>
<pubDate>Mon, 19 Jan 2026 09:11:16 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>Your AI coding assistant forgets everything after each session.</description>
<content:encoded><![CDATA[<p>Your AI coding assistant forgets everything after each session.</p>
<p>You spend time debugging an obscure error. The AI figures it out. Session ends. Next time you hit the same issue? Start from scratch.</p>
<p>This is one of the biggest inefficiencies in AI-assisted development today.</p>
<p>A new open-source Claude Code skill offers a solution: persistent learning across sessions.</p>
<p><strong>How it works:</strong></p>
<p>When Claude Code discovers something non-obvious during a coding session—a debugging technique, a workaround, a project-specific pattern—it automatically saves that<br />
knowledge as a reusable skill.</p>
<p>Next time a similar problem comes up, the skill loads automatically. No re-learning required.</p>
<p><strong>The research behind it:</strong></p>
<p>This isn’t just a clever hack. It’s grounded in academic work on skill libraries for AI agents:</p>
<ul>
<li>Voyager (2023): Showed game-playing agents can build reusable skill libraries over time</li>
<li>CASCADE (2024): Introduced “meta-skills”—skills for acquiring skills</li>
<li>Reflexion (2023): Demonstrated that self-reflection improves agent performance</li>
</ul>
<p>The insight: Agents that persist what they learn outperform agents that start fresh every time.</p>
<p><strong>Why this matters for developers:</strong></p>
<p>We’re moving from AI tools that assist to AI agents that learn. The difference is compounding value—every debugging session makes future sessions faster.</p>
<p>This is early, but it points to where AI development tools are heading: knowledge that accumulates, not knowledge that resets.</p>
<p>Check out the project: <a href="https://github.com/blader/claude-code-continuous-learning-skill?ref=oxidt.com">https://github.com/blader/claude-code-continuous-learning-skill</a></p>
]]></content:encoded>
</item>
<item>
<title>Use Git Worktree for AI Assistant Coding</title>
<link>https://oxidt.com/blog/use-git-worktree-for-ai-assistant-coding</link>
<guid isPermaLink="true">https://oxidt.com/blog/use-git-worktree-for-ai-assistant-coding</guid>
<pubDate>Sat, 10 Jan 2026 13:23:04 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>If you&apos;re using AI coding assistants like Claude Code, GitHub Copilot, or Cursor, there&apos;s a powerful git feature you&apos;re probably overlooking: git worktree.</description>
<content:encoded><![CDATA[<p>If you’re using AI coding assistants like Claude Code, GitHub Copilot, or Cursor, there’s a powerful git feature you’re probably overlooking: <strong>git worktree</strong>.</p>
<h2 id="the-problem-with-ai-assisted-development">The Problem with AI-Assisted Development<a class="heading-anchor" href="#the-problem-with-ai-assisted-development" aria-hidden="true" tabindex="-1">#</a></h2>
<p>When working with AI assistants, you often find yourself in these situations:</p>
<ul>
<li>The AI is making changes to your codebase while you want to continue working</li>
<li>You need to test AI-generated code without risking your current work</li>
<li>Multiple AI sessions are working on different features simultaneously</li>
<li>You want to compare AI implementations against your own</li>
</ul>
<p>Traditionally, you’d either wait, stash your changes, or juggle multiple clones of your repository. All of these options are painful.</p>
<h2 id="enter-git-worktree">Enter Git Worktree<a class="heading-anchor" href="#enter-git-worktree" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Git worktree lets you check out multiple branches of the same repository into different directories simultaneously. Each worktree shares the same <code>.git</code> directory, so they’re all connected.</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Create a new worktree for AI work</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree add ../my-project-ai-feature feature-branch</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Now you have:</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> /my-project          &lt;- your main working directory</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> /my-project-ai-feature &lt;- AI assistant&#39;s playground</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span></span></code></pre>
<h2 id="why-this-is-perfect-for-ai-coding">Why This Is Perfect for AI Coding<a class="heading-anchor" href="#why-this-is-perfect-for-ai-coding" aria-hidden="true" tabindex="-1">#</a></h2>
<h3 id="1-parallel-workflows">1. Parallel Workflows<a class="heading-anchor" href="#1-parallel-workflows" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Point your AI assistant at the worktree directory while you continue working in the main directory. No more waiting. No conflicts.</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Terminal 1: You working in main</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> my-project</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">vim</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> src/important-file.ts</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Terminal 2: AI assistant working in worktree</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> my-project-ai-feature</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude-code</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>implement the new auth system<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
</span></code></pre>
<h3 id="2-safe-experimentation">2. Safe Experimentation<a class="heading-anchor" href="#2-safe-experimentation" aria-hidden="true" tabindex="-1">#</a></h3>
<p>AI assistants can make sweeping changes. With worktrees, those changes are isolated. If the AI creates a mess, you simply:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree remove ../my-project-ai-feature</span>
</span></code></pre>
<p>Your main directory remains untouched.</p>
<h3 id="3-easy-comparison">3. Easy Comparison<a class="heading-anchor" href="#3-easy-comparison" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Want to see what the AI did vs. your approach? Both directories exist simultaneously:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">diff</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>r</span> my-project/src my-project-ai-feature/src</span>
</span></code></pre>
<p>Or open both in your IDE side-by-side.</p>
<h3 id="4-multiple-ai-agents">4. Multiple AI Agents<a class="heading-anchor" href="#4-multiple-ai-agents" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Running multiple AI sessions on different tasks? Create multiple worktrees:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree add ../project-auth auth-feature</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree add ../project-api api-refactor</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree add ../project-tests test-coverage</span>
</span></code></pre>
<p>Each AI agent works independently without stepping on each other.</p>
<h3 id="5-shared-git-history">5. Shared Git History<a class="heading-anchor" href="#5-shared-git-history" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Unlike separate clones, worktrees share the git database. Commits made in any worktree are immediately visible to all others. Merging AI work back into main is seamless.</p>
<h2 id="practical-workflow">Practical Workflow<a class="heading-anchor" href="#practical-workflow" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Here’s a typical workflow I use:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 1. Create a feature branch and worktree for AI</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> checkout<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>b</span> feature/ai-implementation</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree add ../project-ai feature/ai-implementation</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 2. Point AI assistant at the worktree</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> ../project-ai</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Start AI coding session here</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 3. Continue your own work in main</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> ../my-project</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> checkout main</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Work normally</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 4. Review AI changes</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> ../project-ai</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> diff main</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Run tests, review code</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span>
<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 5. Merge when satisfied</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-support hl-function hl-cd hl-shell">cd</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> ../my-project</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> merge feature/ai-implementation</span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> 6. Clean up</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree remove ../project-ai</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> branch<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>d</span> feature/ai-implementation</span>
</span></code></pre>
<h2 id="tips-and-tricks">Tips and Tricks<a class="heading-anchor" href="#tips-and-tricks" aria-hidden="true" tabindex="-1">#</a></h2>
<p><strong>List all worktrees:</strong></p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree list</span>
</span></code></pre>
<p><strong>Prune stale worktrees:</strong></p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree prune</span>
</span></code></pre>
<p><strong>Lock a worktree to prevent accidental deletion:</strong></p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> worktree lock ../project-ai</span>
</span></code></pre>
<h2 id="the-bottom-line">The Bottom Line<a class="heading-anchor" href="#the-bottom-line" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Git worktree transforms how you can work with AI coding assistants. Instead of a sequential, blocking workflow, you get parallel, isolated development environments that share the same repository.</p>
<p>The AI can experiment freely. You can work uninterrupted. And when it’s time to integrate, everything is already in the same git repo.</p>
<p>If you’re serious about AI-assisted development, git worktree isn’t just nice to have—it’s essential.</p>
<hr />
<p><em>Start small: next time you’re about to let an AI assistant make changes, create a worktree first. You’ll wonder how you ever worked without it.</em></p>
]]></content:encoded>
</item>
<item>
<title>How I Added Feedback Widgets to My Ghost Blog</title>
<link>https://oxidt.com/blog/how-i-added-feedback-widgets-to-my-ghost-blog</link>
<guid isPermaLink="true">https://oxidt.com/blog/how-i-added-feedback-widgets-to-my-ghost-blog</guid>
<pubDate>Sun, 14 Dec 2025 20:10:48 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>Writing blog posts is only half the job. The other half? Understanding if the content actually helps readers. For months, I had no idea whether my posts were useful or if readers were running into issues—broken links, confusing…</description>
<content:encoded><![CDATA[<p>Writing blog posts is only half the job. The other half? Understanding if the content actually helps readers. For months, I had no idea whether my posts were useful or if readers were running into issues—broken links, confusing explanations, or missing context.</p>
<p>I needed a lightweight way to collect feedback without adding comment sections or analytics bloat. That’s when I built SeggWat’s embeddable widgets and integrated them into this blog.</p>
<p>Here’s exactly how I did it.</p>
<h2 id="the-problem-with-traditional-feedback">The Problem with Traditional Feedback<a class="heading-anchor" href="#the-problem-with-traditional-feedback" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Most developer blogs either have no feedback mechanism or rely on:</p>
<ul>
<li><strong>Comments</strong> — Great for discussions, but noisy and require moderation</li>
<li><strong>Contact forms</strong> — High friction, most readers won’t bother</li>
<li><strong>Analytics</strong> — Tells you <em>what</em> people read, not <em>if</em> it helped</li>
</ul>
<p>I wanted something simpler: a quick way for readers to say “this helped” or “I found an issue” without leaving the page.</p>
<h2 id="the-solution-two-widgets">The Solution: Two Widgets<a class="heading-anchor" href="#the-solution-two-widgets" aria-hidden="true" tabindex="-1">#</a></h2>
<p>SeggWat provides two embeddable widgets that solve different problems:</p>
<ol>
<li><strong>Feedback Button</strong> — A floating button for detailed feedback (bug reports, suggestions, questions)</li>
<li><strong>Helpful Rating</strong> — A simple thumbs up/down for “Was this page helpful?”</li>
</ol>
<p>Both are vanilla JavaScript, load asynchronously, and require zero dependencies.</p>
<h2 id="step-1-create-a-seggwat-project">Step 1: Create a SeggWat Project<a class="heading-anchor" href="#step-1-create-a-seggwat-project" aria-hidden="true" tabindex="-1">#</a></h2>
<p>First, I created a project in the <a href="https://seggwat.com/dashboard?ref=oxidt.com">SeggWat dashboard</a> specifically for this blog. This keeps feedback organized and separate from other projects.</p>
<p>The dashboard gives you a unique project key—a UUID that identifies your project when feedback is submitted.</p>
<h2 id="step-2-add-the-feedback-button-via-ghost-code-injection">Step 2: Add the Feedback Button via Ghost Code Injection<a class="heading-anchor" href="#step-2-add-the-feedback-button-via-ghost-code-injection" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Ghost has a built-in Code Injection feature that makes this trivial. No theme editing required.</p>
<ol>
<li>Log into Ghost admin at <code>yourblog.com/ghost</code></li>
<li>Navigate to <strong>Settings</strong> → <strong>Code injection</strong></li>
<li>Paste the script in <strong>Site Header</strong>:</li>
</ol>
<pre><code class="language-html"><span class="hl-text hl-html hl-basic"><span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span></span><span class="hl-meta hl-tag hl-script hl-begin hl-html"> <span class="hl-entity hl-other hl-attribute-name hl-html">defer</span>
  <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">src</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://seggwat.com/static/widgets/v1/seggwat-feedback.js<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span>
  <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-project-key</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>your-project-key-here<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span>
  <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-button-color</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>#3eb0ef<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span>
  <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-button-position</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>bottom-right<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span>
  <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-enable-screenshots</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>true<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span></span><span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-source hl-js hl-embedded hl-html"><span class="hl-source hl-js"></span></span><span class="hl-meta hl-tag hl-script hl-end hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
</span></code></pre>
<ol start="4">
<li>Click <strong>Save</strong></li>
</ol>
<p>That’s it. A feedback button now appears on every page.</p>
<h3 id="configuration-options">Configuration Options<a class="heading-anchor" href="#configuration-options" aria-hidden="true" tabindex="-1">#</a></h3>
<p>I customized a few things:</p>
<ul>
<li><strong><code>data-button-color</code></strong> — Matches Ghost’s default accent blue (<code>#3eb0ef</code>)</li>
<li><strong><code>data-button-position</code></strong> — <code>bottom-right</code> keeps it out of the way but accessible</li>
<li><strong><code>data-enable-screenshots</code></strong> — Lets readers annotate screenshots when reporting bugs</li>
</ul>
<p>Other positions available:</p>
<ul>
<li><code>right-side</code> — Vertical button on the right edge</li>
<li><code>icon-only</code> — Compact round button (good for mobile-heavy sites)</li>
</ul>
<h2 id="step-3-add-was-this-helpful-to-posts">Step 3: Add “Was This Helpful?” to Posts<a class="heading-anchor" href="#step-3-add-was-this-helpful-to-posts" aria-hidden="true" tabindex="-1">#</a></h2>
<p>For the helpful rating widget, I wanted it inline at the bottom of each post rather than floating. Ghost’s theme templating makes this possible.</p>
<p>The <code>data-show-comment-link="true"</code> option shows a “Leave a comment” link after a negative rating, which opens the feedback modal for more context.</p>
<p>If you want the rating on every post automatically, modify your Ghost theme’s <code>post.hbs</code>:</p>
<pre><code class="language-handlebars">{{!-- At the bottom of post.hbs, before closing &lt;/article&gt; --}}
&lt;div class=&quot;post-helpful-rating&quot;&gt;
  &lt;script
    src=&quot;https://seggwat.com/static/widgets/v1/seggwat-helpful.js&quot;
    data-project-key=&quot;your-project-key-here&quot;
    data-question=&quot;Was this post helpful?&quot;&gt;
  &lt;/script&gt;
&lt;/div&gt;
</code></pre>
<p>Then upload the modified theme via <strong>Settings</strong> → <strong>Design</strong> → <strong>Change theme</strong> → <strong>Upload theme</strong>.</p>
<h2 id="what-i-learned-from-the-feedback">What I Learned from the Feedback<a class="heading-anchor" href="#what-i-learned-from-the-feedback" aria-hidden="true" tabindex="-1">#</a></h2>
<p>After a few weeks of collecting feedback, patterns emerged:</p>
<ol>
<li><strong>Broken links were common</strong> — External links rot faster than expected</li>
<li><strong>Code examples needed context</strong> — Readers wanted to know which versions of tools I was using</li>
<li><strong>Some posts were genuinely helpful</strong> — The positive feedback on certain posts guided what to write next</li>
</ol>
<p>The per-page helpful ratings showed which posts were actually useful vs. which ones were just getting traffic.</p>
<h2 id="technical-details">Technical Details<a class="heading-anchor" href="#technical-details" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Both widgets:</p>
<ul>
<li>Load asynchronously with <code>defer</code> (no render blocking)</li>
<li>Auto-detect the page URL for context</li>
<li>Support multiple languages (English, German, Swedish)</li>
<li>Work on mobile with touch-optimized interfaces</li>
<li>Require no cookies or tracking</li>
</ul>
<p>The screenshot annotation feature uses SnapDOM for capturing and includes tools for arrows, rectangles, text, and redaction.</p>
<h2 id="api-for-power-users">API for Power Users<a class="heading-anchor" href="#api-for-power-users" aria-hidden="true" tabindex="-1">#</a></h2>
<p>If you want to go further, SeggWat has a REST API for programmatic access:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> List all feedback for a project</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">curl</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>X</span> GET <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>https://seggwat.com/api/v1/projects/YOUR_PROJECT_ID/feedback<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span> <span class="hl-punctuation hl-separator hl-continuation hl-line hl-shell">\
</span><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell">  -</span>H</span> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>X-API-Key: <span class="hl-meta hl-group hl-expansion hl-parameter hl-shell"><span class="hl-punctuation hl-definition hl-variable hl-shell">$</span><span class="hl-variable hl-other hl-readwrite hl-shell">SEGGWAT_API_KEY</span></span><span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>

<span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Get rating statistics</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">curl</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>X</span> GET <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>https://seggwat.com/api/v1/projects/YOUR_PROJECT_ID/ratings/stats<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span> <span class="hl-punctuation hl-separator hl-continuation hl-line hl-shell">\
</span><span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell">  -</span>H</span> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>X-API-Key: <span class="hl-meta hl-group hl-expansion hl-parameter hl-shell"><span class="hl-punctuation hl-definition hl-variable hl-shell">$</span><span class="hl-variable hl-other hl-readwrite hl-shell">SEGGWAT_API_KEY</span></span><span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
</span></code></pre>
<p>I use this to pull weekly summaries into my workflow via n8n.</p>
<h2 id="conclusion">Conclusion<a class="heading-anchor" href="#conclusion" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Adding feedback to a Ghost blog takes about 5 minutes with code injection. The insights are worth far more than the setup time.</p>
<p>If you’re running a developer blog and wondering whether your content actually helps people—add a feedback mechanism. You might be surprised what you learn.</p>
<hr />
<p><strong>Resources:</strong></p>
<ul>
<li><a href="https://docs.seggwat.com/?ref=oxidt.com">SeggWat Documentation</a></li>
<li><a href="https://docs.seggwat.com/guides/ghost?ref=oxidt.com">Ghost Integration Guide</a></li>
<li><a href="https://docs.seggwat.com/general/customization?ref=oxidt.com">Widget Customization</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Developing inside a container</title>
<link>https://oxidt.com/blog/developing-inside-a-container</link>
<guid isPermaLink="true">https://oxidt.com/blog/developing-inside-a-container</guid>
<pubDate>Sun, 6 Apr 2025 16:57:01 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>As a developer, I am constantly discovering new repositories. I am always glad if they offer a devcontainer to spin up the dev environment.</description>
<content:encoded><![CDATA[<p>As a developer, I am constantly discovering new repositories. I am always glad if they offer a devcontainer to spin up the dev environment.</p>
<h2 id="what-is-a-devcontainer-you-may-ask">What is a devcontainer, you may ask?<a class="heading-anchor" href="#what-is-a-devcontainer-you-may-ask" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The Dev Containers extension for Visual Studio Code lets you use a docker container as a full-featured development environment.</p>
<p>This is a great way to get started with a project without having to worry about the setup. It is also a great way to ensure that the code runs in a consistent environment, regardless of the developer’s local setup.</p>
<p><img src="https://eu2.contabostorage.com/b2945292a6234856a92150aa7c9418d4:oxidt-blog/media/2eab24f4-4473-4b97-a247-1d9641bd05de.webp" alt="worksonmymacine.png" fetchpriority="high" decoding="async" /></p>
<p>The well-known phrase “<em>it works on my machine</em>” best describes why you should use a devcontainer. But there are several other benefits of using it.</p>
<h2 id="the-benefits">The benefits<a class="heading-anchor" href="#the-benefits" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li>🚀 <strong>Easy setup</strong>: Ensures a developer can quickly set up a development environment without having to install all the dependencies and tools on your local machine.</li>
<li>🔄 <strong>Consistency</strong>: Ensures that all developers are using the same environment.</li>
<li>🧩 <strong>Reproducibility</strong>: It allows you to easily reproduce the same environment on different machines.</li>
<li>📦 <strong>Portability</strong>: Also allows to share the container with other developers, making it easy to onboard new team members.</li>
<li>📝 <strong>Version control</strong>: You can version control the devcontainer configuration, which makes it easy to track changes and roll back to a previous version if needed.</li>
<li>🔒 <strong>Isolation</strong>: A devcontainer allows you to isolate your development environment from your local machine, which can help prevent conflicts with other projects or dependencies.</li>
<li>⚙️ <strong>Customization</strong>: You can customize the devcontainer to suit your needs, including installing additional tools and dependencies.</li>
<li>🛠️ <strong>Integration with CI/CD</strong>: You can use the same container in your continuous integration and continuous deployment (CI/CD) pipeline, which can help ensure that the code is tested and deployed in a consistent environment.</li>
</ul>
<h2 id="how-to-use-it">How to use it?<a class="heading-anchor" href="#how-to-use-it" aria-hidden="true" tabindex="-1">#</a></h2>
<h3 id="installation">Installation<a class="heading-anchor" href="#installation" aria-hidden="true" tabindex="-1">#</a></h3>
<p>First, install the <strong>Dev Containers</strong> extension in VSCode:</p>
<ol>
<li>Open VSCode</li>
<li>Go to Extensions (Ctrl+Shift+X)</li>
<li>Search for “Dev Containers”</li>
<li>Install the extension from Microsoft</li>
</ol>
<h3 id="creating-your-first-devcontainer">Creating Your First Devcontainer<a class="heading-anchor" href="#creating-your-first-devcontainer" aria-hidden="true" tabindex="-1">#</a></h3>
<p>All the dependencies and tools needed to run the code are defined in a single file called <code>devcontainer.json</code>, located in the <code>.devcontainer</code> folder of your repository.</p>
<p>Here’s a minimal example for a Rust project:</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>name<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>Rust Development<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>image<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>mcr.microsoft.com/devcontainers/rust:latest<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>customizations<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>vscode<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>extensions<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-array hl-json"><span class="hl-punctuation hl-section hl-array hl-begin hl-json">[</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>rust-lang.rust-analyzer<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-array hl-json">,</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>tamasfe.even-better-toml<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> <span class="hl-punctuation hl-section hl-array hl-end hl-json">]</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>postCreateCommand<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>cargo --version<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> 
</span></code></pre>
<h3 id="opening-your-project-in-a-container">Opening Your Project in a Container<a class="heading-anchor" href="#opening-your-project-in-a-container" aria-hidden="true" tabindex="-1">#</a></h3>
<ol>
<li>Open your project in VSCode</li>
<li>Press <code>F1</code> and select <strong>“Dev Containers: Reopen in Container”</strong></li>
<li>VSCode will build the container and reopen your workspace inside it</li>
<li>That’s it! You’re now developing inside the container</li>
</ol>
<h3 id="advanced-configuration">Advanced Configuration<a class="heading-anchor" href="#advanced-configuration" aria-hidden="true" tabindex="-1">#</a></h3>
<p>For more complex projects, you might want to customize further:</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>name<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>Rust Full Stack<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>image<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>mcr.microsoft.com/devcontainers/rust:latest<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>features<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>ghcr.io/devcontainers/features/docker-in-docker:2<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>ghcr.io/devcontainers/features/node:1<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>customizations<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>vscode<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>extensions<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-array hl-json"><span class="hl-punctuation hl-section hl-array hl-begin hl-json">[</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>rust-lang.rust-analyzer<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-array hl-json">,</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>tamasfe.even-better-toml<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-array hl-json">,</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>vadimcn.vscode-lldb<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> <span class="hl-punctuation hl-section hl-array hl-end hl-json">]</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>settings<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>rust-analyzer.checkOnSave.command<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>clippy<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>forwardPorts<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-array hl-json"><span class="hl-punctuation hl-section hl-array hl-begin hl-json">[</span><span class="hl-constant hl-numeric hl-json">3000</span><span class="hl-punctuation hl-separator hl-array hl-json">,</span> <span class="hl-constant hl-numeric hl-json">8080</span><span class="hl-punctuation hl-section hl-array hl-end hl-json">]</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>postCreateCommand<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>cargo build<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>remoteUser<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>vscode<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> 
</span></code></pre>
<p>This configuration:</p>
<ul>
<li>🐳 Enables Docker-in-Docker for building containers inside your devcontainer</li>
<li>📦 Includes Node.js for frontend tooling</li>
<li>🔧 Pre-installs essential Rust extensions</li>
<li>🚪 Forwards ports 3000 and 8080 for web development</li>
<li>⚙️ Runs <code>cargo build</code> after container creation</li>
</ul>
<h3 id="using-docker-compose">Using Docker Compose<a class="heading-anchor" href="#using-docker-compose" aria-hidden="true" tabindex="-1">#</a></h3>
<p>For projects with multiple services (database, cache, etc.), you can use Docker Compose:</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>name<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>Rust with PostgreSQL<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>dockerComposeFile<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>docker-compose.yml<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>service<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>app<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span> <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>workspaceFolder<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>/workspace<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span> </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span> 
</span></code></pre>
<p>And your <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">version</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-quoted hl-single hl-yaml"><span class="hl-punctuation hl-definition hl-string hl-begin hl-yaml">&#39;</span>3.8<span class="hl-punctuation hl-definition hl-string hl-end hl-yaml">&#39;</span></span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">services</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">app</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">build</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">. volumes</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">.:/workspace:cached command</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">sleep infinity postgres</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">image</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">postgres:15 environment</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">POSTGRES_PASSWORD</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">postgres POSTGRES_DB</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">devdb ports</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-quoted hl-double hl-yaml"><span class="hl-punctuation hl-definition hl-string hl-begin hl-yaml">&quot;</span>5432:5432<span class="hl-punctuation hl-definition hl-string hl-end hl-yaml">&quot;</span></span> 
</span></code></pre>
<h2 id="conclusion">Conclusion<a class="heading-anchor" href="#conclusion" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Devcontainers eliminate the “it works on my machine” problem and streamline team onboarding. By committing your <code>.devcontainer</code> configuration to version control, you ensure every developer works in an identical environment.</p>
<p>Ready to try it? Create a <code>.devcontainer/devcontainer.json</code> in your next project and experience the difference!</p>
]]></content:encoded>
</item>
<item>
<title>Github Pipeline for Rust</title>
<link>https://oxidt.com/blog/github-pipeline-for-rust</link>
<guid isPermaLink="true">https://oxidt.com/blog/github-pipeline-for-rust</guid>
<pubDate>Sun, 6 Apr 2025 12:00:51 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>GitHub Actions automate your Rust development workflow. This post covers essential workflows for maintaining code quality and catching issues early.</description>
<content:encoded><![CDATA[<p>GitHub Actions automate your Rust development workflow. This post covers essential workflows for maintaining code quality and catching issues early.</p>
<h2 id="code-formatting">Code Formatting<a class="heading-anchor" href="#code-formatting" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Enforces consistent code style across your project. Fails if code isn’t formatted according to rustfmt rules.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">rustfmt</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Check format</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo fmt --check</span>
</span></code></pre>
<h2 id="unused-dependencies">Unused Dependencies<a class="heading-anchor" href="#unused-dependencies" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Identifies dependencies in Cargo.toml that your code doesn’t actually use. Keeps your dependency tree lean and reduces build times.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">unused-dependencies</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Install cargo-machete</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">taiki-e/install-action@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">tool</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo-machete</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Check for unused dependencies</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo machete</span>
</span></code></pre>
<h2 id="security-audit">Security Audit<a class="heading-anchor" href="#security-audit" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Scans your dependencies for known security vulnerabilities using the RustSec Advisory Database.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">security-audit</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Install cargo-audit</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">taiki-e/install-action@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">tool</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo-audit</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Run security audit</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo audit</span>
</span></code></pre>
<h2 id="license-and-dependency-policy">License and Dependency Policy<a class="heading-anchor" href="#license-and-dependency-policy" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Validates that all dependencies meet your licensing requirements and aren’t banned. Useful for ensuring compliance in commercial projects.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">cargo-deny</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Install cargo-deny</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">taiki-e/install-action@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">tool</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo-deny</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Check licenses and advisories</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo deny check</span>
</span></code></pre>
<h2 id="static-analysis">Static Analysis<a class="heading-anchor" href="#static-analysis" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Runs Clippy lints to catch common mistakes and non-idiomatic code. The <code>-D warnings</code> flag treats warnings as errors, forcing you to address them.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">clippy</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Setup Rust cache</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Swatinem/rust-cache@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">cache-on-failure</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-constant hl-language hl-boolean hl-yaml">true</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Check for clippy warnings</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo clippy --all-targets --all-features --color always – -D warnings</span>
</span></code></pre>
<h2 id="unit-tests">Unit Tests<a class="heading-anchor" href="#unit-tests" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Runs tests defined alongside your code in lib files. Quick feedback on core logic.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">unit-tests</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Setup Rust cache</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Swatinem/rust-cache@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">cache-on-failure</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-constant hl-language hl-boolean hl-yaml">true</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Run unit tests</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo test --lib</span>
</span></code></pre>
<h2 id="integration-tests">Integration Tests<a class="heading-anchor" href="#integration-tests" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Runs tests from the <code>tests/</code> directory that verify multiple components working together.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">integration-tests</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Setup Rust cache</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Swatinem/rust-cache@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">cache-on-failure</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-constant hl-language hl-boolean hl-yaml">true</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Run integration tests</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo test --test integration_test</span>
</span></code></pre>
<h2 id="release-build">Release Build<a class="heading-anchor" href="#release-build" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Verifies that your code compiles with optimizations enabled. Catches issues that only appear in release mode.</p>
<pre><code class="language-yaml"><span class="hl-source hl-yaml"><span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">build</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">runs-on</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">ubuntu-latest</span>
  <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">steps</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">actions/checkout@v4</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">dtolnay/rust-toolchain@stable</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Setup Rust cache</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">uses</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Swatinem/rust-cache@v2</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">with</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span>
        <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">cache-on-failure</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-constant hl-language hl-boolean hl-yaml">true</span>
    <span class="hl-punctuation hl-definition hl-block hl-sequence hl-item hl-yaml">-</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">name</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">Release build</span>
      <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml"><span class="hl-entity hl-name hl-tag hl-yaml">run</span></span><span class="hl-punctuation hl-separator hl-key-value hl-mapping hl-yaml">:</span> <span class="hl-string hl-unquoted hl-plain hl-out hl-yaml">cargo build --release --verbose</span>
</span></code></pre>
<h2 id="caching-strategy">Caching Strategy<a class="heading-anchor" href="#caching-strategy" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The <code>Swatinem/rust-cache</code> action caches compilation artifacts between workflow runs. This includes:</p>
<ul>
<li>Compiled dependencies in <code>target/</code></li>
<li>Cargo registry and git checkouts</li>
<li>Incremental compilation data</li>
</ul>
<p>The <code>cache-on-failure: true</code> option saves the cache even when the build fails. This is useful because:</p>
<ul>
<li>Your code might fail tests, but dependencies still compiled successfully</li>
<li>Next run will be faster even if this one failed</li>
<li>Particularly helpful when fixing compilation errors incrementally</li>
</ul>
<p>Without caching, each workflow run compiles all dependencies from scratch. With caching, only your changed code recompiles, cutting workflow time from minutes to seconds.</p>
<h2 id="putting-it-all-together">Putting It All Together<a class="heading-anchor" href="#putting-it-all-together" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Check out the complete workflow configuration on <a href="https://github.com/oxidt/github-actions-rs?ref=oxidt.com">GitHub</a>.</p>
]]></content:encoded>
</item>
<item>
<title>VSCode REST Client</title>
<link>https://oxidt.com/blog/vscode-rest-client</link>
<guid isPermaLink="true">https://oxidt.com/blog/vscode-rest-client</guid>
<pubDate>Thu, 17 Oct 2024 20:15:02 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>Still using Postman or Insomnia for API testing? Both are powerful tools for API testing, but they have drawbacks. Being standalone apps, they require constant window switching, which disrupts workflow, and lack integration with code…</description>
<category>VSCode</category>
<content:encoded><![CDATA[<p>Still using Postman or Insomnia for API testing? Both are powerful tools for API testing, but they have drawbacks. Being standalone apps, they require constant window switching, which disrupts workflow, and lack integration with code editors, making consistency between API requests and codebase challenging. For this reason, developers are switching to alternative solutions.</p>
<h2 id="vscode-rest-client">⚙️ VSCode REST Client<a class="heading-anchor" href="#vscode-rest-client" aria-hidden="true" tabindex="-1">#</a></h2>
<p>The VSCode REST Client Extension offers a lightweight, integrated solution, running directly within your code editor. For developers seeking a seamless and efficient API testing experience, it’s an ideal alternative to traditional tools, minimizing workflow interruptions.</p>
<p>Key Features:</p>
<ul>
<li>Request Definition: Define HTTP requests using a Markdown-like syntax in .http or .rest files.</li>
<li>Variable Support: Use variables to parameterize requests and responses.</li>
<li>Environment Variables: Set environment variables for requests and responses.</li>
<li>Version Control: Track changes to API requests over time using text-based files.</li>
<li>Integration: Seamlessly integrate with VS Code, reducing window switching and maintaining consistency between requests and codebase.</li>
</ul>
<h2 id="installation">🛠 Installation<a class="heading-anchor" href="#installation" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li>Open VSCode and navigate to the Extensions panel.</li>
<li>Search for <code>REST Client</code> and install the extension.</li>
</ul>
<h2 id="getting-started">🚀 Getting Started<a class="heading-anchor" href="#getting-started" aria-hidden="true" tabindex="-1">#</a></h2>
<p>To get started with the VSCode REST Client, follow these steps:</p>
<ol>
<li>Create a new file with a .http or .rest extension.</li>
<li>Run the request by clicking the <code>Send Request</code> button.</li>
<li>View the response data in the separate panel.</li>
</ol>
<p>Define your request using the Markdown-like syntax.</p>
<pre><code class="language-markdown"><span class="hl-text hl-html hl-markdown"><span class="hl-meta hl-block-level hl-markdown"><span class="hl-markup hl-heading hl-markdown"><span class="hl-punctuation hl-definition hl-heading hl-begin hl-markdown">###</span> </span><span class="hl-markup hl-heading hl-markdown"><span class="hl-entity hl-name hl-section hl-markdown">Simple GET request</span><span class="hl-meta hl-whitespace hl-newline hl-markdown">
</span></span></span>
<span class="hl-meta hl-paragraph hl-markdown">GET <span class="hl-markup hl-underline hl-link hl-markdown">http://httpbin.org</span><span class="hl-markup hl-underline hl-link hl-markdown">/get</span>

</span><span class="hl-meta hl-block-level hl-markdown"><span class="hl-markup hl-heading hl-markdown"><span class="hl-punctuation hl-definition hl-heading hl-begin hl-markdown">###</span> </span><span class="hl-markup hl-heading hl-markdown"><span class="hl-entity hl-name hl-section hl-markdown">Simple POST request</span><span class="hl-meta hl-whitespace hl-newline hl-markdown">
</span></span></span>
<span class="hl-meta hl-paragraph hl-markdown">POST <span class="hl-markup hl-underline hl-link hl-markdown">http://httpbin.org</span><span class="hl-markup hl-underline hl-link hl-markdown">/post</span>
Content-Type: application/json

</span><span class="hl-meta hl-paragraph hl-markdown">{
    &quot;msg&quot;: &quot;Hello World&quot;
}
</span></span></code></pre>
<ol>
<li>Create a new file with a .http or .rest extension.</li>
<li>Define your request using the Markdown-like syntax.</li>
</ol>
<pre><code class="language-markdown"><span class="hl-text hl-html hl-markdown"><span class="hl-meta hl-block-level hl-markdown"><span class="hl-markup hl-heading hl-markdown"><span class="hl-punctuation hl-definition hl-heading hl-begin hl-markdown">###</span> </span><span class="hl-markup hl-heading hl-markdown"><span class="hl-entity hl-name hl-section hl-markdown">Simple GET request</span><span class="hl-meta hl-whitespace hl-newline hl-markdown">
</span></span></span>
<span class="hl-meta hl-paragraph hl-markdown">GET <span class="hl-markup hl-underline hl-link hl-markdown">http://httpbin.org</span><span class="hl-markup hl-underline hl-link hl-markdown">/get</span>

</span><span class="hl-meta hl-block-level hl-markdown"><span class="hl-markup hl-heading hl-markdown"><span class="hl-punctuation hl-definition hl-heading hl-begin hl-markdown">###</span> </span><span class="hl-markup hl-heading hl-markdown"><span class="hl-entity hl-name hl-section hl-markdown">Simple POST request</span><span class="hl-meta hl-whitespace hl-newline hl-markdown">
</span></span></span>
<span class="hl-meta hl-paragraph hl-markdown">POST <span class="hl-markup hl-underline hl-link hl-markdown">http://httpbin.org</span><span class="hl-markup hl-underline hl-link hl-markdown">/post</span>
Content-Type: application/json

</span><span class="hl-meta hl-paragraph hl-markdown">{
    &quot;msg&quot;: &quot;Hello World&quot;
}
</span></span></code></pre>
<ol start="3">
<li>Run the request by clicking the <code>Send Request</code> button.</li>
<li>View the response data in the separate panel.</li>
</ol>
<h2 id="benefits">⭐️ Benefits<a class="heading-anchor" href="#benefits" aria-hidden="true" tabindex="-1">#</a></h2>
<h3 id="seamless-integration-in-vs-code"><strong>Seamless Integration in VS Code</strong><a class="heading-anchor" href="#seamless-integration-in-vs-code" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Switching between applications can disrupt your workflow. The VS Code REST Client keeps everything in one place, letting you send HTTP requests and view responses directly in your code editor. By staying within VS Code, you reduce the friction of hopping between windows, which is especially beneficial if you’re working on complex projects where every saved second counts.</p>
<p><img src="https://eu2.contabostorage.com/b2945292a6234856a92150aa7c9418d4:oxidt-blog/media/221c91c6-1d01-4d4a-9b5d-1264bb478309.webp" alt="" fetchpriority="high" decoding="async" /></p>
<h3 id="variable-support"><strong>Variable support</strong><a class="heading-anchor" href="#variable-support" aria-hidden="true" tabindex="-1">#</a></h3>
<p>Support for variables, which can be defined in the .http or .rest files. Variables are defined using the <code>@</code> symbol and can be referenced using the <code>{{variable}}</code> syntax. This allows you to parameterize requests and responses, making your API testing more dynamic and flexible. Environment variables</p>
<pre><code class="language-markdown"><span class="hl-text hl-html hl-markdown"><span class="hl-meta hl-paragraph hl-markdown">@hostname = api.example.com
@port = 8080
@host = {{hostname}}:{{port}}
@env_var = {{$dotenv [%]variableName}}

</span><span class="hl-meta hl-block-level hl-markdown">#</span>##

<span class="hl-meta hl-paragraph hl-markdown">@name = Strunk <span class="hl-meta hl-other hl-valid-ampersand hl-markdown">&amp;</span> White

</span><span class="hl-meta hl-paragraph hl-markdown">GET https://{{host}}/authors/{{%name}} HTTP/1.1
</span></span></code></pre>
<p>The extension also supports environment variables, which can be defined in the settings.json file. This allows you to set up different environments (e.g., local, staging, production) and switch between them easily. Environment variables can be shared across environments, making it easy to reuse values like tokens, URLs, and other constants.</p>
<pre><code class="language-json"><span class="hl-source hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>rest-client.environmentVariables<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>: <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
    <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>$shared<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>version<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>v1<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>prodToken<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>foo<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>nonProdToken<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>bar<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>
    </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
    <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>local<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>version<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>v2<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>host<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>localhost<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>token<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>{{$shared nonProdToken}}<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>secretKey<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>devSecret<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>
    </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
    <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>production<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-meta hl-structure hl-dictionary hl-json"><span class="hl-punctuation hl-section hl-dictionary hl-begin hl-json">{</span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>host<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>example.com<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>token<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span><span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>{{$shared prodToken}}<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span><span class="hl-punctuation hl-separator hl-dictionary hl-pair hl-json">,</span></span>
        <span class="hl-meta hl-structure hl-dictionary hl-key hl-json"><span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>secretKey<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span></span> <span class="hl-meta hl-structure hl-dictionary hl-value hl-json"><span class="hl-punctuation hl-separator hl-dictionary hl-key-value hl-json">:</span> <span class="hl-string hl-quoted hl-double hl-json"><span class="hl-punctuation hl-definition hl-string hl-begin hl-json">&quot;</span>prodSecret<span class="hl-punctuation hl-definition hl-string hl-end hl-json">&quot;</span></span>
    </span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span><span class="hl-punctuation hl-section hl-dictionary hl-end hl-json">}</span></span>
</span></code></pre>
]]></content:encoded>
</item>
<item>
<title>Build your own BioLink page</title>
<link>https://oxidt.com/blog/build-your-own-biolink-page-2</link>
<guid isPermaLink="true">https://oxidt.com/blog/build-your-own-biolink-page-2</guid>
<pubDate>Thu, 17 Oct 2024 11:39:57 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>BioLink pages like Linktree charge monthly fees for features you can build yourself in an afternoon. This guide shows you how to create your own custom BioLink page and host it for free on GitHub Pages.</description>
<content:encoded><![CDATA[<p>BioLink pages like Linktree charge monthly fees for features you can build yourself in an afternoon. This guide shows you how to create your own custom BioLink page and host it for free on GitHub Pages.</p>
<h2 id="what-you-ll-build">What You’ll Build<a class="heading-anchor" href="#what-you-ll-build" aria-hidden="true" tabindex="-1">#</a></h2>
<p>A simple, responsive landing page with:</p>
<ul>
<li>Profile photo and bio</li>
<li>Links to your social profiles and projects</li>
<li>Custom styling with Tailwind CSS and DaisyUI</li>
<li>Free hosting via GitHub Pages</li>
</ul>
<h2 id="prerequisites">Prerequisites<a class="heading-anchor" href="#prerequisites" aria-hidden="true" tabindex="-1">#</a></h2>
<ul>
<li>GitHub account</li>
<li>Claude Code (or any text editor)</li>
<li>Basic HTML knowledge (helpful but not required)</li>
</ul>
<h2 id="step-1-create-the-html-structure">Step 1: Create the HTML Structure<a class="heading-anchor" href="#step-1-create-the-html-structure" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Create an <code>index.html</code> file with this basic structure:</p>
<pre><code class="language-html"><span class="hl-text hl-html hl-basic"><span class="hl-meta hl-tag hl-sgml hl-html"><span class="hl-punctuation hl-definition hl-tag hl-html">&lt;!</span><span class="hl-meta hl-tag hl-sgml hl-doctype hl-html"><span class="hl-entity hl-name hl-tag hl-doctype hl-html">DOCTYPE</span> html</span><span class="hl-punctuation hl-definition hl-tag hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">html</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">lang</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>en<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-theme</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>dark<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">head</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">meta</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">charset</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>UTF-8<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">meta</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">name</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>viewport<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">content</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>width=device-width, initial-scale=1.0<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">title</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>Your Name - Links<span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">title</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    
    <span class="hl-meta hl-tag hl-sgml hl-html"><span class="hl-punctuation hl-definition hl-tag hl-html">&lt;!</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">– </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">T</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">a</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">l</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">w</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">n</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">d </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">C</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">S</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">S </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">+ </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">D</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">a</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">s</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">y</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">U</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">I </span>-<span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">-</span><span class="hl-punctuation hl-definition hl-tag hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">link</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">href</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">rel</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>stylesheet<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span></span><span class="hl-meta hl-tag hl-script hl-begin hl-html"> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">src</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://cdn.tailwindcss.com<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span></span><span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span><span class="hl-source hl-js hl-embedded hl-html"></span><span class="hl-meta hl-tag hl-script hl-end hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">head</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">body</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">min-h-screen bg-base-200</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">container mx-auto px-4 py-8 max-w-2xl</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
        <span class="hl-meta hl-tag hl-sgml hl-html"><span class="hl-punctuation hl-definition hl-tag hl-html">&lt;!</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">– </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">P</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">r</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">o</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">f</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">l</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">e </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">S</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">e</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">c</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">t</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">o</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">n </span>-<span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">-</span><span class="hl-punctuation hl-definition hl-tag hl-html">&gt;</span></span>
        <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">flex flex-col items-center mb-8</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">avatar mb-4</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">w-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                    <span class="hl-meta hl-tag hl-inline hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-any hl-html">img</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">src</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>your-photo.jpg<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">alt</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>Profile<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html"> /&gt;</span></span>
                <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">h1</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">text-3xl font-bold mb-2</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>Your Name<span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">h1</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">p</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">text-base-content/70 text-center</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>Software Developer | Open Source Enthusiast<span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">p</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
        <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>

        <span class="hl-meta hl-tag hl-sgml hl-html"><span class="hl-punctuation hl-definition hl-tag hl-html">&lt;!</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">– </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">L</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">n</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">k</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">s </span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">S</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">e</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">c</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">t</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">i</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">o</span><span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">n </span>-<span class="hl-invalid hl-illegal hl-bad-comments-or-CDATA hl-html">-</span><span class="hl-punctuation hl-definition hl-tag hl-html">&gt;</span></span>
        <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span> <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">space-y-4</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">href</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://github.com/yourusername<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> 
               <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">btn btn-lg btn-block btn-primary</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                GitHub
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">href</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://twitter.com/yourusername<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> 
               <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">btn btn-lg btn-block btn-secondary</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                Twitter
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">href</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>https://your-blog.com<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> 
               <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">btn btn-lg btn-block btn-accent</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                Blog
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
            
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">href</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>mailto:you@example.com<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> 
               <span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-entity hl-other hl-attribute-name hl-class hl-html">class</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span></span></span><span class="hl-meta hl-attribute-with-value hl-class hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-meta hl-class-name hl-html">btn btn-lg btn-block btn-ghost</span><span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
                Email
            <span class="hl-meta hl-tag hl-inline hl-a hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-inline hl-a hl-html">a</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
        <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
    <span class="hl-meta hl-tag hl-block hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-block hl-any hl-html">div</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">body</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">html</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
</span></code></pre>
<h2 id="step-2-customize-with-claude-code">Step 2: Customize with Claude Code<a class="heading-anchor" href="#step-2-customize-with-claude-code" aria-hidden="true" tabindex="-1">#</a></h2>
<p>You can use Claude Code to generate variations or add features:</p>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-comment hl-line hl-number-sign hl-shell"><span class="hl-punctuation hl-definition hl-comment hl-begin hl-shell">#</span></span><span class="hl-comment hl-line hl-number-sign hl-shell"> Ask Claude Code to customize your page</span><span class="hl-comment hl-line hl-number-sign hl-shell">
</span><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>Add a projects section with cards showing my GitHub repos<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>Change the theme to light mode with custom colors<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">claude</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>Add icons from Heroicons to each link button<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
</span></code></pre>
<p>Claude Code can help you:</p>
<ul>
<li>Add custom sections (projects, skills, testimonials)</li>
<li>Implement dark/light theme toggle</li>
<li>Add animations or transitions</li>
<li>Integrate analytics</li>
</ul>
<h2 id="step-3-customize-styling">Step 3: Customize Styling<a class="heading-anchor" href="#step-3-customize-styling" aria-hidden="true" tabindex="-1">#</a></h2>
<p>DaisyUI provides several built-in themes. Change the <code>data-theme</code> attribute:</p>
<pre><code class="language-html"><span class="hl-text hl-html hl-basic"><span class="hl-meta hl-tag hl-structure hl-any hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-structure hl-any hl-html">html</span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">lang</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>en<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span> <span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-entity hl-other hl-attribute-name hl-html">data-theme</span><span class="hl-punctuation hl-separator hl-key-value hl-html">=</span></span><span class="hl-meta hl-attribute-with-value hl-html"><span class="hl-string hl-quoted hl-double hl-html"><span class="hl-punctuation hl-definition hl-string hl-begin hl-html">&quot;</span>cyberpunk<span class="hl-punctuation hl-definition hl-string hl-end hl-html">&quot;</span></span></span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
</span></code></pre>
<p>Available themes: light, dark, cupcake, cyberpunk, valentine, synthwave, retro, and many more.</p>
<p>You can also create custom color schemes by adding a config:</p>
<pre><code class="language-html"><span class="hl-text hl-html hl-basic"><span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span></span><span class="hl-meta hl-tag hl-script hl-begin hl-html"><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
<span class="hl-source hl-js hl-embedded hl-html"><span class="hl-source hl-js">    <span class="hl-variable hl-other hl-object hl-js">tailwind</span><span class="hl-punctuation hl-accessor hl-js">.</span><span class="hl-meta hl-property hl-object hl-js">config</span> <span class="hl-keyword hl-operator hl-assignment hl-js">=</span> <span class="hl-meta hl-object-literal hl-js"><span class="hl-punctuation hl-section hl-block hl-js">{</span>
        <span class="hl-meta hl-object-literal hl-key hl-js">daisyui</span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-meta hl-object-literal hl-js"><span class="hl-punctuation hl-section hl-block hl-js">{</span>
            <span class="hl-meta hl-object-literal hl-key hl-js">themes</span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-meta hl-sequence hl-js"><span class="hl-punctuation hl-section hl-brackets hl-js">[</span>
                <span class="hl-meta hl-object-literal hl-js"><span class="hl-punctuation hl-section hl-block hl-js">{</span>
                    <span class="hl-meta hl-object-literal hl-key hl-js">mytheme</span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-meta hl-object-literal hl-js"><span class="hl-punctuation hl-section hl-block hl-js">{</span>
                        <span class="hl-meta hl-object-literal hl-key hl-js"><span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>primary<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>#your-color<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
                        <span class="hl-meta hl-object-literal hl-key hl-js"><span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>secondary<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>#your-color<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
                        <span class="hl-meta hl-object-literal hl-key hl-js"><span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>accent<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span><span class="hl-punctuation hl-separator hl-key-value hl-js">:</span> <span class="hl-string hl-quoted hl-double hl-js"><span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>#your-color<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
                    <span class="hl-punctuation hl-section hl-block hl-js">}</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
                <span class="hl-punctuation hl-section hl-block hl-js">}</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
            <span class="hl-punctuation hl-section hl-brackets hl-js">]</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
        <span class="hl-punctuation hl-section hl-block hl-js">}</span></span><span class="hl-punctuation hl-separator hl-comma hl-js">,</span>
    <span class="hl-punctuation hl-section hl-block hl-js">}</span></span>
</span></span><span class="hl-meta hl-tag hl-script hl-end hl-html"><span class="hl-punctuation hl-definition hl-tag hl-begin hl-html">&lt;/</span><span class="hl-entity hl-name hl-tag hl-script hl-html">script</span><span class="hl-punctuation hl-definition hl-tag hl-end hl-html">&gt;</span></span>
</span></code></pre>
<h2 id="step-4-setup-github-repository">Step 4: Setup GitHub Repository<a class="heading-anchor" href="#step-4-setup-github-repository" aria-hidden="true" tabindex="-1">#</a></h2>
<ol>
<li>Create a new repository named <code>username.github.io</code> (replace with your GitHub username)</li>
<li>Clone the repository locally</li>
<li>Add your <code>index.html</code> file</li>
<li>Commit and push:</li>
</ol>
<pre><code class="language-bash"><span class="hl-source hl-shell hl-bash"><span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> add .</span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> commit<span class="hl-variable hl-parameter hl-option hl-shell"><span class="hl-punctuation hl-definition hl-parameter hl-shell"> -</span>m</span> <span class="hl-string hl-quoted hl-double hl-shell"><span class="hl-punctuation hl-definition hl-string hl-begin hl-shell">&quot;</span>Initial biolink page<span class="hl-punctuation hl-definition hl-string hl-end hl-shell">&quot;</span></span></span>
<span class="hl-meta hl-function-call hl-shell"><span class="hl-variable hl-function hl-shell">git</span></span><span class="hl-meta hl-function-call hl-arguments hl-shell"> push origin main</span>
</span></code></pre>
<h2 id="step-5-enable-github-pages">Step 5: Enable GitHub Pages<a class="heading-anchor" href="#step-5-enable-github-pages" aria-hidden="true" tabindex="-1">#</a></h2>
<ol>
<li>Go to your repository on GitHub</li>
<li>Click Settings → Pages</li>
<li>Under “Source”, select “Deploy from a branch”</li>
<li>Select “main” branch and “/ (root)” folder</li>
<li>Click Save</li>
</ol>
<p>Your site will be live at <code>https://username.github.io</code> within a few minutes.</p>
<h2 id="step-6-add-custom-domain-optional">Step 6: Add Custom Domain (Optional)<a class="heading-anchor" href="#step-6-add-custom-domain-optional" aria-hidden="true" tabindex="-1">#</a></h2>
<p>If you own a domain:</p>
<ol>
<li>In your repository, create a file named <code>CNAME</code> with your domain:</li>
</ol>
<p> </p>
<pre><code>yourdomain.com
</code></pre>
<ol start="2">
<li>Add DNS records at your domain provider:</li>
</ol>
<p> </p>
<pre><code>Type: A
Name: @
Value: 185.199.108.153

Type: A
Name: @
Value: 185.199.109.153

Type: A
Name: @
Value: 185.199.110.153

Type: A
Name: @
Value: 185.199.111.153
</code></pre>
<ol start="3">
<li>In GitHub Pages settings, enter your custom domain and enable HTTPS</li>
</ol>
<h2 id="advanced-features-you-can-add">Advanced Features You Can Add<a class="heading-anchor" href="#advanced-features-you-can-add" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Use Claude Code to implement:</p>
<ul>
<li><strong>Analytics</strong>: Track visitors with Plausible or Google Analytics</li>
<li><strong>Dynamic content</strong>: Fetch GitHub stars or latest blog posts via API</li>
<li><strong>Contact form</strong>: Use Formspree or Web3Forms for free form handling</li>
<li><strong>SEO optimization</strong>: Meta tags for better social media sharing</li>
<li><strong>QR code</strong>: Generate a QR code linking to your page</li>
</ul>
<h2 id="example-implementation">Example Implementation<a class="heading-anchor" href="#example-implementation" aria-hidden="true" tabindex="-1">#</a></h2>
<p>Check out a complete example on <a href="https://github.com/hauju/biolink?ref=oxidt.com">GitHub</a>.</p>
<h2 id="conclusion">Conclusion<a class="heading-anchor" href="#conclusion" aria-hidden="true" tabindex="-1">#</a></h2>
<p>You now have a completely free, customizable BioLink page that you control. No monthly fees, no feature limits, and you can modify it however you want. Update it by editing the HTML and pushing to GitHub - changes go live in seconds.</p>
]]></content:encoded>
</item>
<item>
<title>Integrating SCD4x CO2 Sensors with Rust</title>
<link>https://oxidt.com/blog/scd-4x</link>
<guid isPermaLink="true">https://oxidt.com/blog/scd-4x</guid>
<pubDate>Sun, 13 Oct 2024 11:42:22 +0000</pubDate>
<dc:creator>Hauke Jung</dc:creator>
<description>For developers working with embedded systems, the new Rust crate, scd4x, offers a powerful tool to interface with SCD4x series CO2 sensors. This crate provides a streamlined way to access sensor data, making it easier to integrate…</description>
<category>Embedded</category>
<category>rust</category>
<content:encoded><![CDATA[<p>For developers working with embedded systems, the new Rust crate, <code>scd4x</code>, offers a powerful tool to interface with SCD4x series CO2 sensors. This crate provides a streamlined way to access sensor data, making it easier to integrate environmental monitoring capabilities into your projects. With support for both SCD40 and SCD41 models, it simplifies tasks like reading CO2 levels, temperature, and humidity. Explore this crate to enhance your Rust-based IoT applications with reliable air quality sensing.</p>
<p>Learn more about <code>scd4x</code> <a href="https://crates.io/crates/scd4x?ref=oxidt.com">here</a>.</p>
]]></content:encoded>
</item>
</channel>
</rss>
