<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bracketly</title>
    <description>The latest articles on DEV Community by Bracketly (@ethan_5b3022150e2c07a4030).</description>
    <link>https://dev.to/ethan_5b3022150e2c07a4030</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4033478%2F8e37796d-86e3-4623-91c8-7d484dca518a.png</url>
      <title>DEV Community: Bracketly</title>
      <link>https://dev.to/ethan_5b3022150e2c07a4030</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethan_5b3022150e2c07a4030"/>
    <language>en</language>
    <item>
      <title>Five AI Companies Just Agreed on a Plugin Format — Here's a Free Validator for It</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Sun, 09 Aug 2026 07:26:43 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/five-ai-companies-just-agreed-on-a-plugin-format-heres-a-free-validator-for-it-91a</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/five-ai-companies-just-agreed-on-a-plugin-format-heres-a-free-validator-for-it-91a</guid>
      <description>&lt;h1&gt;
  
  
  Five AI Companies Just Agreed on a Plugin Format — Here's a Free Validator for It
&lt;/h1&gt;

&lt;p&gt;On August 6th, OpenAI, AWS, Cursor, Microsoft, and Vercel published Agent Plugins 1.0.0 — a shared packaging standard so a single plugin folder can install into ChatGPT, Codex, GitHub Copilot, VS Code, Cursor, and AWS's Kiro without rewriting setup instructions for each one. It's not a new format competing with Agent Skills (&lt;code&gt;SKILL.md&lt;/code&gt;) or MCP — it's the layer that wraps both: a &lt;code&gt;plugin.json&lt;/code&gt; manifest, an optional &lt;code&gt;skills/&lt;/code&gt; folder, and an optional &lt;code&gt;mcp.json&lt;/code&gt; for MCP server configs, all in one portable directory.&lt;/p&gt;

&lt;p&gt;The catch with a spec this fresh: almost nobody has actually validated a manifest against the real schema yet, because almost nobody's had the chance to. The &lt;code&gt;plugin.json&lt;/code&gt; format is deliberately narrow — exactly ten permitted top-level fields, a &lt;code&gt;name&lt;/code&gt; field with a strict character-and-length pattern, a closed &lt;code&gt;author&lt;/code&gt; object — and most people writing their first one right now are working from blog posts and a single canonical example, not the JSON Schema itself. It's easy to add a field that gets silently dropped, or write a plugin name that looks fine and fails validation outright.&lt;/p&gt;

&lt;p&gt;I added a free &lt;a href="https://bracketly.pages.dev/tools/agent-plugin-validator/" rel="noopener noreferrer"&gt;Agent Plugins Manifest Validator&lt;/a&gt; to Bracketly, my free client-side dev tools site. Paste a &lt;code&gt;plugin.json&lt;/code&gt; and it checks it against the published 1.0.0 schema: the exact &lt;code&gt;$schema&lt;/code&gt; URL, the name pattern (lowercase alphanumeric plus hyphens and periods, no doubled &lt;code&gt;--&lt;/code&gt; or &lt;code&gt;..&lt;/code&gt;, must start and end alphanumeric), and correct types on every optional field. It also knows the difference between what the spec treats as fatal versus not — an unrecognized top-level field or a non-object &lt;code&gt;extensions&lt;/code&gt; value is explicitly non-fatal per spec (clients report and ignore it, then keep loading the plugin), so those show as warnings rather than errors, matching how a real client actually behaves instead of over-flagging. Paste an optional &lt;code&gt;mcp.json&lt;/code&gt; too, and it validates the three MCP transport types — including a rule that's easy to miss by hand: a stdio server's &lt;code&gt;command&lt;/code&gt; has to be a single executable token, not a full command line with embedded arguments.&lt;/p&gt;

&lt;p&gt;Every check is built directly from the published JSON Schema and spec text at agent-plugins.org, not a paraphrase of a summary post, and verified against more than three dozen hand-constructed test cases — valid and invalid — before shipping.&lt;/p&gt;

&lt;p&gt;Like everything on the site, it's 100% client-side. Nothing you paste is sent anywhere.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>MCP Just Broke Its Own Handshake — Here's a Free Checker for the 2026-07-28 Spec</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:24:51 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/mcp-just-broke-its-own-handshake-heres-a-free-checker-for-the-2026-07-28-spec-83j</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/mcp-just-broke-its-own-handshake-heres-a-free-checker-for-the-2026-07-28-spec-83j</guid>
      <description>&lt;h1&gt;
  
  
  MCP Just Broke Its Own Handshake — Here's a Free Checker for the 2026-07-28 Spec
&lt;/h1&gt;

&lt;p&gt;On July 28th, the Model Context Protocol spec shipped its biggest breaking change since the protocol launched: the &lt;code&gt;initialize&lt;/code&gt;/&lt;code&gt;initialized&lt;/code&gt; handshake and the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header — the thing every MCP server and client has been built around since day one — are gone. In their place, every single request now has to carry its own protocol version and capabilities in a &lt;code&gt;_meta&lt;/code&gt; object, on every call, with no more one-time negotiation. There's also new header-based routing (&lt;code&gt;Mcp-Method&lt;/code&gt;, &lt;code&gt;Mcp-Name&lt;/code&gt;, &lt;code&gt;MCP-Protocol-Version&lt;/code&gt;), a &lt;code&gt;resultType&lt;/code&gt; field that's now mandatory on every successful result, and a Multi Round-Trip Requests mechanism that replaces server-initiated streams for mid-call user input.&lt;/p&gt;

&lt;p&gt;All four Tier 1 SDKs — TypeScript, Python, Go, C# — already speak the new spec. Which means if you've got a hand-rolled MCP server, a custom transport layer, or anything that isn't just calling an official SDK's helpers, it can look completely fine in isolation and still fail the moment it talks to an updated client or gateway, with nothing more informative than a routing failure or a silently-dropped request.&lt;/p&gt;

&lt;p&gt;I added a free &lt;a href="https://bracketly.pages.dev/tools/mcp-migration-checker/" rel="noopener noreferrer"&gt;MCP 2026-07-28 Migration Checker&lt;/a&gt; to Bracketly, my free client-side dev tools site, for exactly this. Paste a JSON-RPC message and it flags the deprecated &lt;code&gt;initialize&lt;/code&gt; handshake if it's still there, checks whether &lt;code&gt;params._meta&lt;/code&gt; has the required &lt;code&gt;io.modelcontextprotocol/protocolVersion&lt;/code&gt; and &lt;code&gt;clientCapabilities&lt;/code&gt; fields, and validates that responses declare &lt;code&gt;resultType&lt;/code&gt; and that &lt;code&gt;input_required&lt;/code&gt; results carry the &lt;code&gt;inputRequests&lt;/code&gt;/&lt;code&gt;requestState&lt;/code&gt; fields the new Multi Round-Trip flow needs. If you paste a full raw HTTP block instead of just the JSON body, it also checks the new routing headers — including flagging a lingering &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header as a sign you're still on the old stateful model.&lt;/p&gt;

&lt;p&gt;Building this meant going straight to the published TypeScript schema for the 2026-07-28 revision rather than trusting summaries, since a wire-protocol checker that's subtly wrong about field names is worse than no checker at all — so every check is tuned to how confident I actually am in it: the required-field checks are strict (spec violations), while corners of the spec I'm less certain are fully nailed down, like the exact shape of individual Multi Round-Trip entries, are flagged as gentler notes instead of hard failures.&lt;/p&gt;

&lt;p&gt;Like every tool on the site, it runs entirely in your browser — nothing you paste is sent anywhere. If you're migrating an MCP server or client right now, it's a quick first pass before you go digging through logs.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your Custom AI SDK Backend Returns 200 OK and the Chat UI Still Shows Nothing</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:37:10 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/your-custom-ai-sdk-backend-returns-200-ok-and-the-chat-ui-still-shows-nothing-3pfp</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/your-custom-ai-sdk-backend-returns-200-ok-and-the-chat-ui-still-shows-nothing-3pfp</guid>
      <description>&lt;h1&gt;
  
  
  Your Custom AI SDK Backend Returns 200 OK and the Chat UI Still Shows Nothing
&lt;/h1&gt;

&lt;p&gt;Vercel's AI SDK makes streaming a chat response trivial — as long as your backend is also written in JS and calls &lt;code&gt;streamText().toUIMessageStreamResponse()&lt;/code&gt;. The moment you're not in that world — a Go, Python, or Rust backend, a queue that re-serializes the stream, a proxy sitting between your LLM call and the client — you have to hand-produce the exact Server-Sent Events format &lt;code&gt;useChat&lt;/code&gt; expects, and there's no error message when you get it wrong. The request succeeds, the status is 200, and the UI just renders an empty message or silently stalls.&lt;/p&gt;

&lt;p&gt;I hit this building a small side project and ended up staring at raw response bytes in a terminal for longer than I'd like to admit, so I added an &lt;a href="https://bracketly.pages.dev/tools/ai-sdk-stream-debugger/" rel="noopener noreferrer"&gt;AI SDK Stream Protocol Validator &amp;amp; Debugger&lt;/a&gt; to Bracketly.&lt;/p&gt;

&lt;p&gt;The protocol itself is a sequence of &lt;code&gt;data: {...}&lt;/code&gt; JSON lines, each with a &lt;code&gt;type&lt;/code&gt; field — &lt;code&gt;text-start&lt;/code&gt;, &lt;code&gt;text-delta&lt;/code&gt;, &lt;code&gt;text-end&lt;/code&gt; for streamed text; &lt;code&gt;tool-input-start&lt;/code&gt;/&lt;code&gt;tool-input-delta&lt;/code&gt;/&lt;code&gt;tool-input-available&lt;/code&gt; for tool calls; &lt;code&gt;finish&lt;/code&gt; and a closing &lt;code&gt;data: [DONE]&lt;/code&gt; to end the stream. It reads as simple until you're generating it by hand and discover how many ways there are to get it subtly wrong: a &lt;code&gt;text-delta&lt;/code&gt; referencing an &lt;code&gt;id&lt;/code&gt; you never opened with &lt;code&gt;text-start&lt;/code&gt;, a &lt;code&gt;text-start&lt;/code&gt; that never gets a matching &lt;code&gt;text-end&lt;/code&gt;, a missing &lt;code&gt;messageId&lt;/code&gt; on the initial &lt;code&gt;start&lt;/code&gt; event, or just forgetting the trailing &lt;code&gt;[DONE]&lt;/code&gt; so the client waits forever for a stream that already finished.&lt;/p&gt;

&lt;p&gt;The tool parses each event, checks it against the documented part types and their required fields, and tracks state across the whole stream so it can catch the ordering bugs a plain JSON validator would miss — an orphaned delta, an unclosed span, a tool-output event referencing a &lt;code&gt;toolCallId&lt;/code&gt; that was never introduced. It also reconstructs the assembled message text from the deltas, so you can see in one glance what the client would actually have rendered from your stream. Paste the raw response body — straight from &lt;code&gt;curl -N&lt;/code&gt; or your browser's network tab — and it runs entirely in your browser tab; nothing gets sent anywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bracketly.pages.dev/tools/ai-sdk-stream-debugger/" rel="noopener noreferrer"&gt;Try it here&lt;/a&gt; — feedback and edge cases welcome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your Truncated Embedding Vector Is Silently the Wrong Length — Here's the One-Line Fix</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:20:00 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/your-truncated-embedding-vector-is-silently-the-wrong-length-heres-the-one-line-fix-3k40</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/your-truncated-embedding-vector-is-silently-the-wrong-length-heres-the-one-line-fix-3k40</guid>
      <description>&lt;h1&gt;
  
  
  Your Truncated Embedding Vector Is Silently the Wrong Length — Here's the One-Line Fix
&lt;/h1&gt;

&lt;p&gt;Most major embedding APIs — OpenAI's text-embedding-3 family, Gemini's gemini-embedding models, Cohere embed-v4, Nomic Embed v1.5 — are now trained with Matryoshka Representation Learning, which means you can safely shorten an embedding after the fact to save storage and search cost, with only a small, predictable accuracy hit. That's a genuinely useful trick. It's also got a one-line gotcha that's easy to miss the first time, so I added an &lt;a href="https://bracketly.pages.dev/tools/embedding-truncator/" rel="noopener noreferrer"&gt;Embedding Vector Truncator &amp;amp; Renormalizer&lt;/a&gt; to Bracketly, my free client-side dev tools site.&lt;/p&gt;

&lt;p&gt;Here's the gotcha: embeddings from these APIs are L2-normalized to unit length by default — that's what makes cosine similarity and dot-product search interchangeable, and it's part of why the math downstream (nearest-neighbor search, reranking, thresholding) behaves predictably. Slice off the last N dimensions of a unit vector and the result is &lt;em&gt;not&lt;/em&gt; unit length anymore — you've changed its magnitude without touching its direction. If you then compare that truncated vector against others with cosine similarity or dot product without renormalizing first, you get numbers that are subtly, silently wrong. Nothing throws an error. Your search results just get quietly worse, and it's the kind of bug that's genuinely hard to notice unless you're specifically looking for it, because the vectors still &lt;em&gt;look&lt;/em&gt; like embeddings — same shape, plausible-looking floats.&lt;/p&gt;

&lt;p&gt;Providers that expose a native truncation parameter (OpenAI's &lt;code&gt;dimensions&lt;/code&gt; argument, for instance) handle this for you automatically. The gap is when you're truncating a vector yourself — a value you already stored at full size and want to shrink for a cheaper index, or a self-hosted Matryoshka-trained model where you're slicing the array by hand. In that case the fix really is one line: take the L2 norm of the truncated slice (square root of the sum of its squared values) and divide every value by it. The tool does exactly that — paste a vector, pick a target dimension, get back the renormalized result plus its norm before and after, so you can confirm it's actually unit length again.&lt;/p&gt;

&lt;p&gt;I also added an optional second-vector field so you can paste a comparison vector and see cosine similarity computed two ways: on the original full-length vectors, and on both truncated-and-renormalized. The two numbers won't be identical — you did genuinely discard information, so a small shift is the expected accuracy/size tradeoff Matryoshka truncation is built around — but seeing the actual delta for your own real vectors is a lot more convincing than trusting a blog post's abstract claim that "it should still work reasonably well." Everything runs as plain array arithmetic in your browser tab; nothing you paste is sent anywhere.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your MCP Tool Definition Can Be 100% Spec-Compliant and Still Not Work</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Sat, 01 Aug 2026 07:15:58 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/your-mcp-tool-definition-can-be-100-spec-compliant-and-still-not-work-f8j</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/your-mcp-tool-definition-can-be-100-spec-compliant-and-still-not-work-f8j</guid>
      <description>&lt;h1&gt;
  
  
  Your MCP Tool Definition Can Be 100% Spec-Compliant and Still Not Work
&lt;/h1&gt;

&lt;p&gt;I've been building a few free browser-based tools for people working with LLM APIs — a GPT token counter, a Citations API debugger, a Gemini thought-signature checker. This week I added one for the Model Context Protocol: a schema validator for MCP tool definitions.&lt;/p&gt;

&lt;p&gt;What sent me down this path was how small the actual MCP spec is. A &lt;code&gt;Tool&lt;/code&gt; object only has two required fields: &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;inputSchema&lt;/code&gt;. That's it. No character restrictions on the name, no length cap, nothing that says &lt;code&gt;inputSchema&lt;/code&gt; has to describe an object at its root rather than, say, a string or an array. Read the raw JSON Schema for &lt;code&gt;Tool&lt;/code&gt; and you'd reasonably conclude almost anything goes.&lt;/p&gt;

&lt;p&gt;Then you actually wire a server up to a real client and watch tools silently fail to load, or get called with arguments that don't match what you expected, and none of it was a JSON syntax error — the JSON was fine the whole time.&lt;/p&gt;

&lt;p&gt;The gap is between what the spec &lt;em&gt;requires&lt;/em&gt; and what every real client &lt;em&gt;assumes&lt;/em&gt;. Tool names get used as function identifiers, so a name with a space or a slash in it works fine against the raw protocol and then gets rejected the moment it passes through an OpenAI-compatible bridge, because OpenAI's function-calling format does constrain names and the bridge inherits that constraint whether or not MCP itself does. &lt;code&gt;inputSchema&lt;/code&gt; without &lt;code&gt;"type": "object"&lt;/code&gt; at the root parses as valid JSON Schema and then breaks argument validation, because every SDK assumes tool arguments arrive as a JSON object of named parameters, spec text or not. A &lt;code&gt;required&lt;/code&gt; array listing a property that doesn't exist in &lt;code&gt;properties&lt;/code&gt; is a typo the spec has no opinion on, but it'll confuse the client's own JSON Schema validator in a way that's hard to trace back to that one line.&lt;/p&gt;

&lt;p&gt;None of these are things a JSON validator catches, and none of them are exactly the MCP spec's problem either — they're the difference between "technically valid" and "what actually works," which is exactly the gap a linter is for.&lt;/p&gt;

&lt;p&gt;So the tool checks both: the hard spec requirements (missing &lt;code&gt;name&lt;/code&gt;, missing &lt;code&gt;inputSchema&lt;/code&gt;, a &lt;code&gt;required&lt;/code&gt; entry that doesn't exist) as errors, and the practical client conventions (name character set, 64-character length caps, &lt;code&gt;$ref&lt;/code&gt; support varying by client) as warnings, labeled clearly as which is which. Paste a single tool, an array, or a full &lt;code&gt;tools/list&lt;/code&gt; response — it detects the shape and validates every tool in it. Free, runs entirely client-side, no server ever sees your tool definitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bracketly.pages.dev/tools/mcp-tool-validator/" rel="noopener noreferrer"&gt;Try it here&lt;/a&gt; — feedback and edge cases welcome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Gemini 3's Function Calling Has a Signature Rule That Only Bites You Once You Add a Second Tool Call</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:20:16 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/gemini-3s-function-calling-has-a-signature-rule-that-only-bites-you-once-you-add-a-second-tool-call-31mm</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/gemini-3s-function-calling-has-a-signature-rule-that-only-bites-you-once-you-add-a-second-tool-call-31mm</guid>
      <description>&lt;h1&gt;
  
  
  Gemini 3's Function Calling Has a Signature Rule That Only Bites You Once You Add a Second Tool Call
&lt;/h1&gt;

&lt;p&gt;Gemini 3 introduced &lt;code&gt;thoughtSignature&lt;/code&gt; — an encrypted blob attached to a &lt;code&gt;functionCall&lt;/code&gt; part that preserves the model's reasoning state across a multi-turn tool-use conversation. Drop it when replaying history back to the API and you get a 400. Simple enough, until you actually build something that calls more than one tool, so I added a &lt;a href="https://bracketly.pages.dev/tools/gemini-signature-validator/" rel="noopener noreferrer"&gt;Gemini Thought Signature Validator&lt;/a&gt; to Bracketly, my free client-side dev tools site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that's easy to get backwards
&lt;/h2&gt;

&lt;p&gt;There are two distinct cases, and they require opposite handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel function calls&lt;/strong&gt; (one response, several tool calls at once — "check the weather in Tokyo, Mumbai, and São Paulo"): only the &lt;em&gt;first&lt;/em&gt; &lt;code&gt;functionCall&lt;/code&gt; part in that response carries a &lt;code&gt;thoughtSignature&lt;/code&gt;. The rest legitimately have none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential function calls&lt;/strong&gt; (separate turns, one call each, across multiple steps of a longer task): &lt;em&gt;every&lt;/em&gt; turn's &lt;code&gt;functionCall&lt;/code&gt; needs its own signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write code that handles one case correctly and you'll ship something that works fine in testing — most people's first tool-calling demo is a single function — and then breaks the moment a prompt fans out into multiple calls, or the model chains several tool steps together. Worse, if you're serializing conversation history yourself (a custom agent loop, a stored session you replay later) it's easy to accidentally treat "no signature on this part" as a bug and strip or regenerate something that was never supposed to have one, which breaks the &lt;em&gt;correctly&lt;/em&gt; signed request instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool actually checks
&lt;/h2&gt;

&lt;p&gt;Paste a request body, a bare &lt;code&gt;contents&lt;/code&gt; array, or a raw &lt;code&gt;generateContent&lt;/code&gt; response, and it walks every turn checking exactly the things that cause real failures: whether the first &lt;code&gt;functionCall&lt;/code&gt; in each model turn has a signature, whether the following turn's &lt;code&gt;functionResponse&lt;/code&gt; parts exist, match in count, and are in the same order as their calls, and whether Gemini 3's per-call &lt;code&gt;id&lt;/code&gt; field gets echoed back correctly. It can't decode what's inside a signature — nobody outside Gemini can — but it doesn't need to; the failures that actually happen are structural, not cryptographic.&lt;/p&gt;

&lt;p&gt;Built and verified against the documented request/response shapes with hand-written test vectors before touching a line of UI code — parallel-call signing, sequential-call signing, missing-response detection, id mismatches, malformed input, all covered. 100% client-side, nothing you paste leaves your browser: &lt;a href="https://bracketly.pages.dev/tools/gemini-signature-validator/" rel="noopener noreferrer"&gt;bracketly.pages.dev/tools/gemini-signature-validator&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A Citation Can Look Correct and Still Be Silently Wrong — So I Built a Debugger for It</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Wed, 29 Jul 2026 07:26:55 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/a-citation-can-look-correct-and-still-be-silently-wrong-so-i-built-a-debugger-for-it-n0a</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/a-citation-can-look-correct-and-still-be-silently-wrong-so-i-built-a-debugger-for-it-n0a</guid>
      <description>&lt;h1&gt;
  
  
  A Citation Can Look Correct and Still Be Silently Wrong — So I Built a Debugger for It
&lt;/h1&gt;

&lt;p&gt;Anthropic's Claude API has a Citations feature: turn it on for a document you send in, and Claude's response comes back with each claim traceable to the exact passage that backs it — a &lt;code&gt;cited_text&lt;/code&gt; string, which document it came from, and a location (character range, page number, or content block index). It's genuinely useful for RAG apps where "trust me" isn't good enough. But building against it surfaced a failure mode I hadn't planned for, so I added a &lt;a href="https://bracketly.pages.dev/tools/citations-viewer/" rel="noopener noreferrer"&gt;Citations API Response Viewer&lt;/a&gt; to Bracketly, my free client-side dev tools site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;The first surprise is structural: with citations on, Claude doesn't return one text block with inline footnote markers. It returns &lt;em&gt;multiple&lt;/em&gt; text blocks, and only the blocks backed by a source carry a &lt;code&gt;citations&lt;/code&gt; array at all. If your code assumes &lt;code&gt;response.content&lt;/code&gt; is always a single chunk of prose, that assumption breaks the moment citations are enabled — you have to stitch the blocks back together yourself to get the readable answer.&lt;/p&gt;

&lt;p&gt;The second surprise is worse, because it fails quietly. Each citation gives you &lt;code&gt;start_char_index&lt;/code&gt; / &lt;code&gt;end_char_index&lt;/code&gt; into &lt;em&gt;your&lt;/em&gt; document — not Claude's copy of it, yours. If the document you show your user, or use for a fact-check pass, isn't byte-for-byte the exact string you sent in the API request — different line endings, a re-export, a "fixed" typo, even different whitespace — those indices point at the wrong span. The &lt;code&gt;cited_text&lt;/code&gt; field still reads correctly on its own, so nothing &lt;em&gt;looks&lt;/em&gt; broken. You only find out when someone clicks a citation and it highlights the wrong sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;Paste a response (the full message, its &lt;code&gt;content&lt;/code&gt; array, or a single block — it auto-detects), and it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reassembles the answer text with each cited passage highlighted and numbered&lt;/li&gt;
&lt;li&gt;Lists every citation with its source document, location, and quoted text, cross-linked back to the passage&lt;/li&gt;
&lt;li&gt;Lets you paste your actual source document per citation and checks, character-by-character, whether &lt;code&gt;source.slice(start, end)&lt;/code&gt; really equals &lt;code&gt;cited_text&lt;/code&gt; — flagging a mismatch with the exact (wrong) span it found instead of just saying "nope"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part is the one I actually needed and couldn't find anywhere: a way to verify my own document handling was consistent, not just a pretty renderer for the JSON.&lt;/p&gt;

&lt;p&gt;100% client-side, nothing you paste leaves your browser: &lt;a href="https://bracketly.pages.dev/tools/citations-viewer/" rel="noopener noreferrer"&gt;bracketly.pages.dev/tools/citations-viewer&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Added a GPT Token Counter, and Learned Tiktoken's Own npm Package Ships the Vocab Files Twice</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:19:37 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/i-added-a-gpt-token-counter-and-learned-tiktokens-own-npm-package-ships-the-vocab-files-twice-3ae0</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/i-added-a-gpt-token-counter-and-learned-tiktokens-own-npm-package-ships-the-vocab-files-twice-3ae0</guid>
      <description>&lt;h1&gt;
  
  
  I Added a GPT Token Counter, and Learned Tiktoken's Own npm Package Ships the Vocab Files Twice
&lt;/h1&gt;

&lt;p&gt;I use OpenAI's API often enough that I'm regularly guessing at token counts before I hit send — "is this prompt going to blow the context window" is a dumb thing to find out from an error response. So I added a &lt;a href="https://bracketly.pages.dev/tools/token-counter/" rel="noopener noreferrer"&gt;GPT Token Counter&lt;/a&gt; to Bracketly, my free client-side dev tools site, and it turned into a smaller, more interesting problem than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tokenizer, not an approximation
&lt;/h2&gt;

&lt;p&gt;The obvious wrong answer is "estimate ~4 characters per token." The obvious right answer is &lt;code&gt;js-tiktoken&lt;/code&gt;, OpenAI's own pure-JS port of tiktoken — same byte-pair-encoding vocabulary the API actually bills against, not a guess. GPT-5.5, GPT-5.4, and GPT-4o all still tokenize with the &lt;code&gt;o200k_base&lt;/code&gt; encoding (confirmed against OpenAI's own tiktoken repo — nothing newer has shipped a different one yet), while GPT-4 and GPT-3.5 Turbo use the older &lt;code&gt;cl100k_base&lt;/code&gt;. Two vocab files cover essentially every model anyone's actually calling right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that made me look twice
&lt;/h2&gt;

&lt;p&gt;Those vocab files aren't small — &lt;code&gt;o200k_base&lt;/code&gt; alone is about 2.3MB unminified, because it's a literal table of ~200,000 byte-pair merge ranks, not code. Bundling that into every page on the site would tax everyone visiting a JSON formatter to benefit nobody. So the tool imports &lt;code&gt;js-tiktoken/lite&lt;/code&gt; (just the encoder logic, no vocab) and dynamically &lt;code&gt;import()&lt;/code&gt;s only the one rank file the selected model actually needs — Vite splits each into its own chunk automatically, so it only downloads the first time you type something, cached from then on.&lt;/p&gt;

&lt;p&gt;I didn't trust "it compiled" as proof it worked. I ran the actual built-and-bundled tool in a real headless browser against known tiktoken outputs — &lt;code&gt;"Hello, world!"&lt;/code&gt; is 4 tokens in both encodings, &lt;code&gt;"The quick brown fox jumps over the lazy dog."&lt;/code&gt; is 10 — and only shipped it once the live page matched exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't cover Claude or Gemini — their tokenizers aren't tiktoken-compatible, and I'd rather leave a model out than fake a number for it. It also doesn't add the small fixed per-message overhead a chat-format API request tacks on for role/name framing — it counts raw text tokens only, which is the number you actually want when you're sizing a prompt before you've decided how to structure the request.&lt;/p&gt;

&lt;p&gt;Free, no signup, runs entirely in your browser: &lt;a href="https://bracketly.pages.dev/tools/token-counter/" rel="noopener noreferrer"&gt;bracketly.pages.dev/tools/token-counter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Finally Gave Bracketly a Logo That Means Something</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:05:30 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/i-finally-gave-bracketly-a-logo-that-means-something-4np3</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/i-finally-gave-bracketly-a-logo-that-means-something-4np3</guid>
      <description>&lt;h1&gt;
  
  
  I Finally Gave Bracketly a Logo That Means Something
&lt;/h1&gt;

&lt;p&gt;Bracketly has had a favicon since day one — a vaguely hood-shaped abstract mark that, looking back, had nothing to do with the name. It just sat there in the browser tab looking like a placeholder nobody replaced. Today I replaced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new mark
&lt;/h2&gt;

&lt;p&gt;It's just &lt;code&gt;[ ]&lt;/code&gt; — two brackets, rendered as rounded stroke paths rather than sharp geometric corners, to match the soft-radius design language already used everywhere else on the site (cards, buttons, pills). Literal, obvious in hindsight, and the kind of thing that should've been there from the start for a site called &lt;em&gt;Bracketly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It ships in two forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A monochrome version for the favicon — theme-aware, so it switches between black and white depending on whether your OS is in light or dark mode, same as the rest of the site already does for text and backgrounds.&lt;/li&gt;
&lt;li&gt;A full-color version using the site's existing accent gradient (blue → purple → pink) for the header and anywhere else a full-color mark makes sense.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;Nothing fancier than two &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; elements with &lt;code&gt;stroke-linecap="round"&lt;/code&gt; and &lt;code&gt;stroke-linejoin="round"&lt;/code&gt; — the rounded bracket ends come for free from the stroke rendering, no manual arc math needed. The tricky part wasn't the shape, it was the spacing: my first attempt had the two brackets' arms overlapping in the middle, which at a glance just looked like a solid rounded rectangle instead of two distinct brackets. Rendered a preview at actual favicon size (32×32) before shipping anything, since a mark that reads fine at 512px can easily turn into mush at the size it actually gets used.&lt;/p&gt;

&lt;p&gt;All the PNG variants (32×32, 512×512, and a 180×180 Apple touch icon for anyone who adds the site to their home screen) are rasterized directly from that one SVG source rather than hand-exported, so there's exactly one file to ever update if the mark changes again.&lt;/p&gt;

&lt;p&gt;Small change, but it's the kind of detail that either quietly signals "someone's paying attention here" or quietly signals the opposite. &lt;a href="https://bracketly.pages.dev/" rel="noopener noreferrer"&gt;Come take a look&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Built a Cron Expression Parser Because I Kept Getting the "Day" Fields Wrong</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:34:37 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/i-built-a-cron-expression-parser-because-i-kept-getting-the-day-fields-wrong-14d0</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/i-built-a-cron-expression-parser-because-i-kept-getting-the-day-fields-wrong-14d0</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Cron Expression Parser Because I Kept Getting the "Day" Fields Wrong
&lt;/h1&gt;

&lt;p&gt;Cron syntax has five fields — minute, hour, day-of-month, month, day-of-week — and most people can read the first three just fine. &lt;code&gt;0 9 * * *&lt;/code&gt; is obviously "9am, every day." It's the interaction between the last two fields that trips people up, and it tripped me up enough times that I added a &lt;a href="https://bracketly.pages.dev/tools/cron-parser/" rel="noopener noreferrer"&gt;Cron Expression Parser&lt;/a&gt; to Bracketly, my free client-side dev tools site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha: it's OR, not AND
&lt;/h2&gt;

&lt;p&gt;Here's the thing that isn't obvious from staring at &lt;code&gt;0 0 1 * 1&lt;/code&gt;: if you read it left to right, you might assume it means "midnight, on the 1st of the month, AND on a Monday" — i.e. only fires when the 1st happens to land on a Monday.&lt;/p&gt;

&lt;p&gt;That's wrong. Standard cron semantics say: when &lt;em&gt;both&lt;/em&gt; day-of-month and day-of-week are restricted (neither is a bare &lt;code&gt;*&lt;/code&gt;), a date matches if it satisfies &lt;em&gt;either&lt;/em&gt; one. So &lt;code&gt;0 0 1 * 1&lt;/code&gt; actually fires at midnight on the 1st of every month, &lt;em&gt;and separately&lt;/em&gt; every Monday. If you wanted "only when the 1st is a Monday," cron can't directly express that at all.&lt;/p&gt;

&lt;p&gt;This isn't a bug anywhere — it's just how POSIX cron has always worked — but it's exactly the kind of thing you forget between the few times a year you write a cron expression, and get wrong silently until a job fires on a day you didn't expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;Paste a cron expression (or one of the &lt;code&gt;@daily&lt;/code&gt; / &lt;code&gt;@hourly&lt;/code&gt; / &lt;code&gt;@weekly&lt;/code&gt; shortcuts) and it gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A plain-English explanation, written to make the OR-not-AND behavior explicit rather than paper over it&lt;/li&gt;
&lt;li&gt;The next 5 times the schedule will actually fire, computed from your current time&lt;/li&gt;
&lt;li&gt;Inline validation — a field like &lt;code&gt;99 * * * *&lt;/code&gt; gets a specific "value out of range" error instead of just silently doing something unexpected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like every tool on Bracketly, it's 100% client-side — the parsing and date math happen in your browser, nothing is sent anywhere. It supports the standard field syntax (&lt;code&gt;*&lt;/code&gt;, ranges like &lt;code&gt;1-5&lt;/code&gt;, steps like &lt;code&gt;*/15&lt;/code&gt;, comma lists like &lt;code&gt;1,15&lt;/code&gt;), though numeric fields only for now — no &lt;code&gt;MON&lt;/code&gt;/&lt;code&gt;JAN&lt;/code&gt;-style names yet.&lt;/p&gt;

&lt;p&gt;If you've ever second-guessed whether your deploy schedule actually means what you think it means, it's free and takes about ten seconds to check: &lt;a href="https://bracketly.pages.dev/tools/cron-parser/" rel="noopener noreferrer"&gt;bracketly.pages.dev/tools/cron-parser&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Cloudflare's "Autoconfig" Bot Quietly Broke My Static Site's Deploy — Here's What Happened</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Mon, 20 Jul 2026 06:08:39 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/cloudflares-autoconfig-bot-quietly-broke-my-static-sites-deploy-heres-what-happened-28e1</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/cloudflares-autoconfig-bot-quietly-broke-my-static-sites-deploy-heres-what-happened-28e1</guid>
      <description>&lt;h1&gt;
  
  
  Cloudflare's "Autoconfig" Bot Quietly Broke My Static Site's Deploy — Here's What Happened
&lt;/h1&gt;

&lt;p&gt;A few days ago, Cloudflare's &lt;code&gt;cloudflare-workers-and-pages[bot]&lt;/code&gt; opened a pull request on &lt;a href="https://bracketly.pages.dev" rel="noopener noreferrer"&gt;Bracketly&lt;/a&gt;'s repo, titled "Add Cloudflare Workers configuration." It sat there unmerged while I worked on other things. Today I merged it without thinking too hard about it — it's from Cloudflare's own bot, on a project already hosted on Cloudflare Pages, seemed safe enough.&lt;/p&gt;

&lt;p&gt;It took the site down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the PR actually did
&lt;/h2&gt;

&lt;p&gt;It added the &lt;code&gt;@astrojs/cloudflare&lt;/code&gt; adapter to &lt;code&gt;astro.config.mjs&lt;/code&gt;, generated a &lt;code&gt;wrangler.jsonc&lt;/code&gt;, and updated &lt;code&gt;package.json&lt;/code&gt;. The framing in the PR description is "configures your project for Cloudflare Workers deployment," which sounds like a reasonable modernization — Workers is Cloudflare's newer, more capable runtime.&lt;/p&gt;

&lt;p&gt;The problem: Bracketly is a 100% static, client-side site. Every "tool" on it (JSON formatter, Base64 encoder, hash generator, etc.) runs entirely in the browser — there was never a server-rendering need to justify a Workers adapter in the first place.&lt;/p&gt;

&lt;p&gt;Adding the adapter restructured the build output. Before: a flat &lt;code&gt;dist/&lt;/code&gt; with &lt;code&gt;index.html&lt;/code&gt; at the root, exactly what a plain static-file deploy expects. After: &lt;code&gt;dist/client/&lt;/code&gt; (the actual site) and &lt;code&gt;dist/server/&lt;/code&gt; (a Workers runtime bundle), because the adapter assumes you want on-demand server rendering alongside static assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it "succeeded" while actually being broken
&lt;/h2&gt;

&lt;p&gt;My existing GitHub Actions deploy step ran &lt;code&gt;wrangler pages deploy dist&lt;/code&gt; — the same command that had worked for weeks. It reported success. The build succeeded. The deploy step reported "Deployment complete." Every automated signal said everything was fine.&lt;/p&gt;

&lt;p&gt;Wrangler had actually detected the new &lt;code&gt;dist/client/wrangler.json&lt;/code&gt; and silently redirected the deploy target from &lt;code&gt;dist&lt;/code&gt; to &lt;code&gt;dist/client&lt;/code&gt; on its own — a "helpful" auto-detection feature that meant the command exit code was misleading. It genuinely uploaded files and genuinely returned success. It just wasn't obviously communicating that the actual site paths (&lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/tools/whatever&lt;/code&gt;, the sitemap) were now 404ing.&lt;/p&gt;

&lt;p&gt;I only caught it because I happened to run a manual &lt;code&gt;curl&lt;/code&gt; against the live homepage right after the merge and got nothing back where a title tag should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git revert -m 1 &amp;lt;merge-commit&amp;gt;&lt;/code&gt;, rebuild locally to confirm &lt;code&gt;dist/index.html&lt;/code&gt; exists again (not nested), push. Total downtime: a few minutes, only because I was actively watching for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Automated "helpful" configuration bots — especially ones with commit/PR access to your deploy pipeline — don't know your project's actual architecture. This one assumed "Cloudflare Pages project" implies "wants Workers/SSR," which is a reasonable &lt;em&gt;default&lt;/em&gt; guess but was flatly wrong for a purely static site. A green checkmark on a CI deploy step is not the same thing as "the site actually works" — it just means the command didn't throw a non-zero exit code. Worth an actual &lt;code&gt;curl&lt;/code&gt; against production after any deploy-pipeline change, automated or not, regardless of how official-looking the bot that proposed it is.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Silent Cloudflare Pages Bug That Was Quietly Hurting My SEO</title>
      <dc:creator>Bracketly</dc:creator>
      <pubDate>Mon, 20 Jul 2026 05:55:33 +0000</pubDate>
      <link>https://dev.to/ethan_5b3022150e2c07a4030/the-silent-cloudflare-pages-bug-that-was-quietly-hurting-my-seo-3oi7</link>
      <guid>https://dev.to/ethan_5b3022150e2c07a4030/the-silent-cloudflare-pages-bug-that-was-quietly-hurting-my-seo-3oi7</guid>
      <description>&lt;h1&gt;
  
  
  The Silent Cloudflare Pages Bug That Was Quietly Hurting My SEO
&lt;/h1&gt;

&lt;p&gt;While building a broken-link checker for &lt;a href="https://bracketly.pages.dev" rel="noopener noreferrer"&gt;Bracketly&lt;/a&gt; (a small free dev-tools site), I found something I wasn't expecting: every nonexistent URL on the site — &lt;code&gt;/literally-anything-i-typed&lt;/code&gt;, &lt;code&gt;/tools/does-not-exist&lt;/code&gt;, whatever — was returning &lt;strong&gt;HTTP 200&lt;/strong&gt;, with the homepage's content, instead of a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Cloudflare Pages has a fallback behavior for unmatched routes: if your deployed site has no &lt;code&gt;404.html&lt;/code&gt;, it serves &lt;code&gt;index.html&lt;/code&gt; for any path it can't otherwise resolve — but critically, it does this with a &lt;strong&gt;200 status code&lt;/strong&gt;, not 404. This is the classic SPA-router pattern (makes sense for a client-side-routed React/Vue app where you &lt;em&gt;want&lt;/em&gt; every path to load &lt;code&gt;index.html&lt;/code&gt; and let JS take over routing). It's the wrong default for a plain static multi-page site, and if you never explicitly add a &lt;code&gt;404.html&lt;/code&gt;, you get it silently, with zero warning.&lt;/p&gt;

&lt;p&gt;I only noticed because I was building a script to curl every URL in my sitemap and flag non-200 responses — and a deliberately-broken test URL came back green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's worse than it looks
&lt;/h2&gt;

&lt;p&gt;This is what's sometimes called a "soft 404" — a page that &lt;em&gt;should&lt;/em&gt; be an error but returns success. Search engines treat this specially, and not kindly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google's crawler explicitly detects soft-404 patterns (repeated identical content served at different URLs with a 200 status) and can flag the pattern site-wide, not just the one URL&lt;/li&gt;
&lt;li&gt;Every broken/mistyped/old link pointing at your site becomes a duplicate-content page in Google's eyes, all serving the same homepage&lt;/li&gt;
&lt;li&gt;It dilutes whatever ranking signal your actual homepage has, since Google now sees N copies of it at different URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a site that had literally zero custom 404 handling, this meant &lt;em&gt;any&lt;/em&gt; crawled dead link — a typo'd URL, an old removed page, a bot probing random paths — was quietly generating homepage duplicates in the index.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Trivial once you know to look for it. In Astro (or basically any static site generator), you just need a page that generates &lt;code&gt;404.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/404.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
&amp;lt;BaseLayout title="Page Not Found" description="..."&amp;gt;
  &amp;lt;h1&amp;gt;404&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;This page doesn't exist.&amp;lt;/p&amp;gt;
&amp;lt;/BaseLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro (and most SSGs) automatically emits this as &lt;code&gt;dist/404.html&lt;/code&gt;, and Cloudflare Pages automatically serves it with a genuine 404 status for any unmatched route once it exists — no config needed, just the file being present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://example.com/nonexistent-page
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;before: 200
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;after:  404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;If you're deploying a static site to Cloudflare Pages, Vercel, Netlify, or similar — check right now whether a random broken URL on your site returns 404 or 200. It's a two-second check and a five-minute fix, but it's exactly the kind of thing that never shows up unless you go looking, since the page &lt;em&gt;looks&lt;/em&gt; completely normal (it's literally rendering your homepage) — there's no visible error to notice.&lt;/p&gt;

&lt;p&gt;Source for the actual fix: &lt;a href="https://github.com/GRimkiller360/bracketly" rel="noopener noreferrer"&gt;https://github.com/GRimkiller360/bracketly&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>cloudflare</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
