<?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: Piotr Zielinski</title>
    <description>The latest articles on DEV Community by Piotr Zielinski (@p-zielinski).</description>
    <link>https://dev.to/p-zielinski</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%2F3963384%2Fe6e6c08f-a49b-4e2f-b3c9-ed425e9a1e1c.jpg</url>
      <title>DEV Community: Piotr Zielinski</title>
      <link>https://dev.to/p-zielinski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/p-zielinski"/>
    <language>en</language>
    <item>
      <title>Stop Hardcoding 301s: How I Built a Redirect Engine That Doesn't Break at 2 A.M.</title>
      <dc:creator>Piotr Zielinski</dc:creator>
      <pubDate>Thu, 04 Jun 2026 09:57:43 +0000</pubDate>
      <link>https://dev.to/p-zielinski/stop-hardcoding-301s-how-i-built-a-redirect-engine-that-doesnt-break-at-2-am-2938</link>
      <guid>https://dev.to/p-zielinski/stop-hardcoding-301s-how-i-built-a-redirect-engine-that-doesnt-break-at-2-am-2938</guid>
      <description>&lt;p&gt;Marketing wants an A/B landing page by Friday. Product wants to gracefully deprecate a legacy API without breaking old mobile clients. Growth wants ten thousand short links, and Ops &lt;em&gt;does not&lt;/em&gt; want ten thousand Nginx edits.&lt;/p&gt;

&lt;p&gt;At some point, a single &lt;code&gt;return 301&lt;/code&gt; in your CDN stops being a configuration problem and becomes a &lt;strong&gt;routing product&lt;/strong&gt;. Someone has to answer, on every HTTP request: &lt;em&gt;Given this host, path, query, and method—where does this visitor go, and with which status code?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built that answer as a pipeline at &lt;strong&gt;&lt;a href="https://linkshift.app" rel="noopener noreferrer"&gt;LinkShift&lt;/a&gt;&lt;/strong&gt;. Not a pile of special cases, but a &lt;strong&gt;fixed sequence of steps&lt;/strong&gt; that runs the exact same way for real visitors and for the tools you use to test rules before rollout.&lt;/p&gt;

&lt;p&gt;Here is how I designed a deterministic redirect engine, the pipeline that powers it, and the edge cases that kept me up at night so they don't have to keep you up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Usually Works" Is Not Enough
&lt;/h2&gt;

&lt;p&gt;A redirect engine fails quietly. The browser follows a broken &lt;code&gt;302&lt;/code&gt; and nobody files a ticket. The damage shows up days later in analytics: wrong campaign, wrong locale, or an infinite loop that only appears when two rules on the same host point at each other.&lt;/p&gt;

&lt;p&gt;What I wanted early on was boring, bulletproof reliability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same inputs → same decision.&lt;/strong&gt; Two engineers simulating the same request against the same rule set should get the same target URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same resolution logic everywhere.&lt;/strong&gt; Matching and destination resolution must not diverge between the "Test Rule" button in the dashboard and an actual click on a custom domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guards before cleverness.&lt;/strong&gt; Rate limits and access checks run &lt;em&gt;before&lt;/em&gt; anyone evaluates a ternary conditional in a destination string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expressiveness is easy. &lt;strong&gt;Ordering is what saves you in production.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pipeline, Told as a Story
&lt;/h2&gt;

&lt;p&gt;Picture a request hitting a hostname. Before the engine asks "which rule wins?", the request walks through a strict corridor of gates. Only then does it enter the rule loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbofuymold5qstbiagqu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbofuymold5qstbiagqu.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 1: The Host Has to Exist
&lt;/h3&gt;

&lt;p&gt;If the hostname does not resolve to a domain or LinkShift subdomain in a &lt;strong&gt;domain group&lt;/strong&gt;, the pipeline stops with a &lt;strong&gt;404&lt;/strong&gt;. There is no rule list to evaluate—the request never entered the tenant’s world.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 2: Protection Runs First
&lt;/h3&gt;

&lt;p&gt;Next comes the organization &lt;strong&gt;redirect rate limit&lt;/strong&gt;. If the plan’s per-minute cap is exceeded, the engine returns &lt;strong&gt;429&lt;/strong&gt; and does not redirect. Abuse and accidental traffic spikes should not become a DDoS against your customers’ destinations.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;organization access&lt;/strong&gt; runs: suspended accounts or plan limits yield a &lt;strong&gt;402&lt;/strong&gt;. This check also applies when you &lt;strong&gt;simulate&lt;/strong&gt; rules from the API. A suspended org should not get useful routing previews that imply production would work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 3: The &lt;code&gt;robots.txt&lt;/code&gt; Exception
&lt;/h3&gt;

&lt;p&gt;This path is not a redirect rule. If the domain group serves a robots policy, it returns &lt;strong&gt;200&lt;/strong&gt; with the configured body. Rate limiting and access checks already ran; crawlers see consistent policy without competing with marketing redirects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 4: The Rule Loop Begins
&lt;/h3&gt;

&lt;p&gt;The engine loads redirect rules that are not deleted and not blocked, sorted in a strict order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;priority&lt;/code&gt; descending&lt;/strong&gt; — Higher number wins the race to be considered first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;createdAt&lt;/code&gt; descending&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;id&lt;/code&gt; descending&lt;/strong&gt; — A stable tie-breaker, not something you should design around.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each rule, in that order, it asks a chain of questions. &lt;strong&gt;The first rule that produces a redirect target wins.&lt;/strong&gt; The loop stops. Everyone else is ignored.&lt;/p&gt;

&lt;p&gt;Here is the mental model your team has to internalize: &lt;em&gt;First rule that produces a target wins&lt;/em&gt;—not &lt;em&gt;"first rule whose path matched."&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the HTTP method match?&lt;/strong&gt; (e.g., &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the source match?&lt;/strong&gt; Plain paths respect &lt;code&gt;pathMatch&lt;/code&gt; and &lt;code&gt;queryMatch&lt;/code&gt;. Regex sources apply against the path or the full original URL. Wildcards (&lt;code&gt;*&lt;/code&gt;) catch everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is this a link map or dynamic rule?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Link maps:&lt;/em&gt; Match a prefix (&lt;code&gt;/go&lt;/code&gt;), extract a key, look up static HTTPS URL. Miss? &lt;em&gt;Continue to next rule.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Dynamic rules:&lt;/em&gt; Resolve regex capture groups (&lt;code&gt;$1&lt;/code&gt;), placeholders, and ternaries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is the target safe?&lt;/strong&gt; Absolute URLs go through a &lt;strong&gt;destination blacklist&lt;/strong&gt;. Blocked host → &lt;strong&gt;403&lt;/strong&gt;. Blacklist infrastructure failed? Fail closed → &lt;strong&gt;503&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Redirect.&lt;/strong&gt; Issue the status code (&lt;code&gt;301&lt;/code&gt;, &lt;code&gt;302&lt;/code&gt;, &lt;code&gt;307&lt;/code&gt;, or &lt;code&gt;308&lt;/code&gt;) and the resolved target.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If no rule produces a target, the request ends in &lt;strong&gt;404&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5 Edge Cases That Look Like Bugs (But Aren't)
&lt;/h2&gt;

&lt;p&gt;These are the lessons I wish I had read before I debugged my first "impossible" redirect.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "Matched" is not the same as "Redirected"
&lt;/h3&gt;

&lt;p&gt;A link map miss without a fallback skips to the next rule. A dynamic rule can match the path and method but still fail resolution in ways that yield no target. Design explicit catch-alls at lower priority instead of assuming "no match" and "match but no URL" behave identically.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Priority is part of your API contract
&lt;/h3&gt;

&lt;p&gt;Never build business logic on database tie-breakers (&lt;code&gt;createdAt&lt;/code&gt;). If two rules must never compete ambiguously, give them different &lt;strong&gt;&lt;code&gt;priority&lt;/code&gt;&lt;/strong&gt; values. Catch-alls (&lt;code&gt;source: "*"&lt;/code&gt;) belong at the bottom unless you enjoy explaining why a new experiment stopped all traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;queryMatch&lt;/code&gt; is where campaigns go to die
&lt;/h3&gt;

&lt;p&gt;For a short-link prefix like &lt;code&gt;/go&lt;/code&gt;, you almost always want &lt;strong&gt;&lt;code&gt;queryMatch: ignore&lt;/code&gt;&lt;/strong&gt; so &lt;code&gt;/go/summer?utm=email&lt;/code&gt; still resolves the key &lt;code&gt;summer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But for a regex rewriting &lt;code&gt;www&lt;/code&gt; to apex that &lt;em&gt;must&lt;/em&gt; keep the query string, &lt;strong&gt;&lt;code&gt;ignore&lt;/code&gt;&lt;/strong&gt; can strip the very data you needed. The pipeline is deterministic; your configuration has to explicitly state what "match" means for query strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The destination string is a small program
&lt;/h3&gt;

&lt;p&gt;Resolution order is fixed: &lt;strong&gt;&lt;code&gt;$N&lt;/code&gt; substitution → placeholders → conditionals.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"'{accept-language.primary:to_lower_case}' includes 'pl' ? /pl : /en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queryMatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This power buys you language-based routing or A/B testing without deploys. It also buys you footguns. If a conditional branch resolves to an &lt;strong&gt;empty string&lt;/strong&gt;, the rule still &lt;strong&gt;wins&lt;/strong&gt;. The engine &lt;em&gt;does not&lt;/em&gt; fall through to the next rule. The client just gets a broken redirect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfk09kf6crb9e4qg5wlx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfk09kf6crb9e4qg5wlx.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Loops are multi-request, not single-hop
&lt;/h3&gt;

&lt;p&gt;The engine returns exactly &lt;strong&gt;one&lt;/strong&gt; redirect per request. If the target points to the same host with another matching path, the &lt;strong&gt;browser&lt;/strong&gt; makes a new request and the full pipeline runs again. The engine will not detect that loop in a single hop. Multi-hop debugging belongs in trace tools, not in wishful thinking about the redirect service being psychic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Link Maps vs. Dynamic Rules: Keep the Hot Path Dumb
&lt;/h2&gt;

&lt;p&gt;Scale changes design. Ten thousand short links do not mean ten thousand redirect rules. They mean &lt;strong&gt;one prefix rule&lt;/strong&gt; and a fast database table mapping keys to HTTPS URLs.&lt;/p&gt;

&lt;p&gt;This split keeps the hot path predictable. The rule loop does something cheap (prefix match, key extract, map lookup). The heavy editorial work lives in the map. Mixing models—like trying to template map rows—would break the invariant that map lookups are static and instantly auditable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validate at Save, Guard at Runtime
&lt;/h2&gt;

&lt;p&gt;I split safety across two phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At create/update:&lt;/strong&gt; A validator rejects rules that cannot work. Regex &lt;code&gt;$N&lt;/code&gt; without a regex source? Rejected. Conditional depth over the limit? Rejected. Destinational URLs are scanned for safety before they ever hit the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At request time:&lt;/strong&gt; Rate limits and blacklist checks protect the platform. Blacklist infrastructure failure is &lt;strong&gt;fail-closed&lt;/strong&gt; (no redirect).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation keeps bad logic out of the database. Runtime guards keep bad traffic from becoming someone else’s problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simulate vs. Live: What Stays the Same?
&lt;/h2&gt;

&lt;p&gt;The feature my customers rely on most is &lt;strong&gt;Simulate&lt;/strong&gt;: sending sample requests against the &lt;em&gt;current live rule set&lt;/em&gt; without counting as production traffic.&lt;/p&gt;

&lt;p&gt;Simulate answers: &lt;em&gt;"What URL would this request get right now?"&lt;/em&gt;&lt;br&gt;
It does not answer: &lt;em&gt;"Will we survive a traffic spike?"&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Live Traffic&lt;/th&gt;
&lt;th&gt;Simulate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rule match &amp;amp; resolution&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organization access (&lt;code&gt;402&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect rate limit (&lt;code&gt;429&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination blacklist&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Simulate is for routing correctness; production guards are for survival. That distinction prevented a massive class of false confidence in LinkShift's CI/CD.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;If you are about to build this, sequence the pipeline on paper before you write the DSL. Decide sort order, decide what "winning" means, decide fail-open vs. fail-closed for safety dependencies. The ternary syntax and placeholders are the fun part; &lt;strong&gt;the corridor of gates and the rule loop semantics&lt;/strong&gt; are what your users will actually rely on.&lt;/p&gt;

&lt;p&gt;I run this pipeline every day at &lt;strong&gt;&lt;a href="https://linkshift.app" rel="noopener noreferrer"&gt;LinkShift&lt;/a&gt;&lt;/strong&gt; to power programmable redirects on custom domains and managed subdomains. The same engine drives analytics, stored redirect tests, and batch simulations.&lt;/p&gt;

&lt;p&gt;If you want the full step-by-step diagram and operator reference, the public docs walk through the live pipeline and edge cases in detail: &lt;strong&gt;&lt;a href="https://linkshift.app/docs" rel="noopener noreferrer"&gt;Read the LinkShift Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you have war stories—empty ternary branches, query string surprises, redirect loops—I’d genuinely like to hear them in the comments. A predictable pipeline is only boring until one rule at priority &lt;code&gt;10&lt;/code&gt; proves you wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build programmable redirects at &lt;a href="https://linkshift.app" rel="noopener noreferrer"&gt;LinkShift.app&lt;/a&gt;. If your team has outgrown static config files, come check it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>architecture</category>
      <category>node</category>
    </item>
    <item>
      <title>One Markdown File, Two Worlds: How to Build Docs for Both Humans and AI Assistants</title>
      <dc:creator>Piotr Zielinski</dc:creator>
      <pubDate>Wed, 03 Jun 2026 21:19:17 +0000</pubDate>
      <link>https://dev.to/p-zielinski/one-markdown-file-two-worlds-how-to-build-docs-for-both-humans-and-ai-assistants-52fi</link>
      <guid>https://dev.to/p-zielinski/one-markdown-file-two-worlds-how-to-build-docs-for-both-humans-and-ai-assistants-52fi</guid>
      <description>&lt;p&gt;When building a modern SaaS product with an AI assistant ("Ask our docs"), you quickly hit a brutal realization about documentation design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Humans&lt;/strong&gt; want brevity, beautiful diagrams, clean UI, and zero noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLMs&lt;/strong&gt; want absolutely everything. Edge cases, exact dashboard click paths, API endpoint sequences, and hidden technical constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic approach? Maintain two separate knowledge bases. One for humans (a beautiful public CMS) and one for AI (prompts, vector databases, or markdown files tailored for the bot). &lt;/p&gt;

&lt;p&gt;The result? Both sources drift apart within exactly one week.&lt;/p&gt;

&lt;p&gt;In our project, we took a different route. &lt;strong&gt;We write exactly one Markdown file&lt;/strong&gt;, but our rendering pipeline processes it differently depending on whether it’s being read by a human in a browser or ingested by an AI agent into our RAG (Retrieval-Augmented Generation) context.&lt;/p&gt;

&lt;p&gt;Here is how we built the simplest possible Single Source of Truth (SSoT) for both humans and AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 The Context: Why We Needed This
&lt;/h2&gt;

&lt;p&gt;We are building &lt;a href="https://linkshift.app/" rel="noopener noreferrer"&gt;LinkShift.app&lt;/a&gt; — a programmable redirect platform. Think of it as a smart layer on the edge where you define traffic routing using rules, dynamic link maps, and regex, instead of hard-coding messy redirects in your app or CDN. &lt;/p&gt;

&lt;p&gt;For a developer tool like this, documentation isn't just an afterthought; it's a core feature. Our users build complex routing logic, so the UI needs to be crystal clear. At the same time, our AI assistant must understand the deep mechanics behind our rules engine, batch simulations, and fallback scenarios. We simply couldn't afford a knowledge gap between what the developer reads and what our AI knows.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The Architecture: One File, Two Views
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;MarkdownRendererComponent&lt;/code&gt; (built with Angular and the &lt;code&gt;marked&lt;/code&gt; parser) is not just a dumb Markdown-to-HTML translator. Before the text ever hits the screen, it goes through a lightweight pre-processing pass.&lt;/p&gt;

&lt;p&gt;The entire process is controlled using simple, custom block directives (&lt;code&gt;:::block-name&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Here is how the roles are split depending on who the audience is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document Element&lt;/th&gt;
&lt;th&gt;Human Reader (Docs UI)&lt;/th&gt;
&lt;th&gt;AI Assistant (RAG Context)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Markdown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (Rendered to HTML)&lt;/td&gt;
&lt;td&gt;✅ Yes (Raw text)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infoboxes (&lt;code&gt;:::info&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (Styled callouts)&lt;/td&gt;
&lt;td&gt;✅ Yes (Textual context)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mermaid Diagrams&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (Graphical render)&lt;/td&gt;
&lt;td&gt;✅ Yes (Raw text flow syntax)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hidden from Humans (&lt;code&gt;:::ai-only&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Hidden (CSS &lt;code&gt;display: none&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ Yes (Full text for LLM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Author Scratchpad (&lt;code&gt;:::hidden-on-purpose&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Stripped entirely&lt;/td&gt;
&lt;td&gt;❌ Stripped entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📝 How It Looks in Practice (Syntax)
&lt;/h2&gt;

&lt;p&gt;For the technical writer or developer, it’s still just a standard &lt;code&gt;.md&lt;/code&gt; file in the Git repository. However, they get to use a few "superpowers."&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AI-Only Blocks (&lt;code&gt;:::ai-only&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the secret sauce. This is where we drop extra context that would feel repetitive or bloated to a human engineer, but is absolute gold for an LLM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### API Integration&lt;/span&gt;

To import domains, you need to hit the POST &lt;span class="sb"&gt;`/domains`&lt;/span&gt; endpoint.

:::ai-only
⚠️ Context for the bot: Remember to instruct the user that before calling 
POST &lt;span class="sb"&gt;`/domains`&lt;/span&gt;, they MUST first generate an organization token via 
&lt;span class="sb"&gt;`/auth/org-token`&lt;/span&gt;, otherwise they will receive a 403. Humans have this covered 
in the "Quickstart" section, but remind them in the chat if they ask about 403 errors.
:::

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For Humans:&lt;/strong&gt; The pre-processor wraps this in a &lt;code&gt;&amp;lt;div class="docs-ai-hidden" aria-hidden="true"&amp;gt;&lt;/code&gt;. A simple CSS rule applies &lt;code&gt;display: none&lt;/code&gt;. The text is physically there in the DOM (so if someone opens DevTools, they can see it—this is not for security!), but it doesn't ruin the page's readability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the AI:&lt;/strong&gt; The backend loader reads the raw Markdown file directly into the vector store or prompt context, &lt;strong&gt;completely ignoring&lt;/strong&gt; the CSS layer. The bot sees the instructions loud and clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Mermaid Diagrams for Everyone
&lt;/h3&gt;

&lt;p&gt;Instead of uploading static images (which LLMs cannot read out of the box in a standard text-based RAG pipeline), we stick to Mermaid code blocks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We use standard Markdown code fences specifying &lt;code&gt;mermaid&lt;/code&gt; as the language to draw flowcharts. (Dev.to’s parser currently struggles with rendering nested Mermaid blocks within examples, so I'm omitting the exact snippet here to keep this post readable!).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When our pipeline reads that Mermaid block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In the browser:&lt;/strong&gt; &lt;code&gt;mermaid.run()&lt;/code&gt; executes on hydration, rendering a gorgeous visual chart for the human reader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the AI:&lt;/strong&gt; The agent receives the raw text-based Mermaid code. Because modern LLMs are fantastic at understanding graph-based text syntax, they can flawlessly explain the flowchart to a user in chat or even generate an updated version of the chart in their response.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The Author Scratchpad (&lt;code&gt;:::hidden-on-purpose&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Sometimes, documentation writers need to leave notes for themselves or the team (e.g., "TODO: update this after the v2.4 release").&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;:::hidden-on-purpose
Leaving this here because we are changing the edge engine logic in June 
and this entire paragraph will need a rewrite. ~Mark
:::

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A utility function called &lt;code&gt;stripHiddenOnPurposeMarkdown()&lt;/code&gt; runs a regex pass that nukes these blocks &lt;strong&gt;both before rendering the UI and before sending data to the bot&lt;/strong&gt;. It is a private playground that stays strictly in the codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why This Boosts Developer Experience (DX)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Knowledge Fragmentation:&lt;/strong&gt; Everything related to a feature (the user-facing copy, the diagrams, and the hyper-technical AI hints) lives in &lt;strong&gt;one single file&lt;/strong&gt;. A code change requires exactly one documentation update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git as the Source of Truth:&lt;/strong&gt; Version control, Pull Requests, peer reviews—the entire documentation workflow follows your standard development pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead Simple Deployment:&lt;/strong&gt; A single sync script (&lt;code&gt;npm run docs:sync&lt;/code&gt;) builds the frontend bundle and simultaneously refreshes the AI bot's knowledge base in our backend.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ⚠️ Lessons from Production: What to Watch Out For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is NOT a security boundary:&lt;/strong&gt; The &lt;code&gt;ai-only&lt;/code&gt; blocks are hidden via client-side CSS. Do not put API keys, passwords, or highly sensitive internal business strategies there. For truly confidential data (like internal infrastructure runbooks), we use a completely separate, private directory that never gets bundled into the public documentation repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep Your Parsers in Sync:&lt;/strong&gt; The text stripping utilities must behave identically on both the frontend and the backend. We highly recommend keeping these parser utilities in a shared workspace folder (e.g., &lt;code&gt;shared/utils/&lt;/code&gt;), so a regex adjustment in one place doesn't accidentally break formatting in the other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You don't need to adopt heavy, complex headless CMS platforms or enterprise-grade documentation frameworks just to survive the AI era. Sometimes, all it takes is taking good old Markdown, writing a dozen lines of pre-processing code, and elegantly decoupling your data layer from your presentation layer.&lt;/p&gt;

&lt;p&gt;By doing this, our users get a clean, minimalist reading experience, while our AI assistant handles highly specific technical edge cases like an absolute expert. Everybody wins.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try It Out!
&lt;/h3&gt;

&lt;p&gt;Curious to see how this dual-view system looks in production? Head over to the &lt;a href="https://linkshift.app/docs" rel="noopener noreferrer"&gt;LinkShift Documentation&lt;/a&gt; and try asking the AI agent a technical question about our programmable redirects. See if you can spot the difference between the visual docs and the bot's deep context! 😉&lt;/p&gt;

&lt;p&gt;If you are dealing with similar RAG vs. UI documentation challenges, let me know in the comments how you solved them!&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>llm</category>
      <category>dx</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Cheat LLM Context: A Lightweight AI Doc Assistant Architecture</title>
      <dc:creator>Piotr Zielinski</dc:creator>
      <pubDate>Tue, 02 Jun 2026 19:48:40 +0000</pubDate>
      <link>https://dev.to/p-zielinski/how-to-cheat-llm-context-a-lightweight-ai-doc-assistant-architecture-3hl1</link>
      <guid>https://dev.to/p-zielinski/how-to-cheat-llm-context-a-lightweight-ai-doc-assistant-architecture-3hl1</guid>
      <description>&lt;p&gt;Dropping your entire Markdown documentation folder into an LLM prompt sounds easy - until you see the API bill. Large contexts mean large costs, especially when users ask repetitive or highly specific questions.&lt;/p&gt;

&lt;p&gt;When building the documentation assistant for my project, &lt;strong&gt;&lt;a href="https://linkshift.app/" rel="noopener noreferrer"&gt;LinkShift.app&lt;/a&gt;&lt;/strong&gt; (a programmable redirect and link-mapping platform running on the edge), I knew the learning curve would be steep for users dealing with Regex, Liquid templates, and edge routing rules. Instead of taking the easy route and watching my API budget melt, I designed a multi-tier, ultra-low-cost AI agent architecture.&lt;/p&gt;

&lt;p&gt;Here is how I solved token bloat and kept response times blazing fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tiered Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;Instead of throwing a massive model at the full chat history and documentation for every single query, the system filters the request through three distinct phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request -&amp;gt; [1. Receptionist (gpt-5.4-nano)] -&amp;gt; Intent Filtering &amp;amp; File Routing
                                                                  |
                                                                  v
User Response &amp;lt;- [3. Response Gen (gpt-5.4-mini)] &amp;lt;- [2. Inject Relevant Files (Usually 3-6)]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 1: Smart Data Ingestion (Preprocessing)
&lt;/h3&gt;

&lt;p&gt;Feeding raw Markdown files dynamically to an LLM is incredibly inefficient.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All 28 Markdown files of my documentation were pre-processed and &lt;strong&gt;summarized&lt;/strong&gt; beforehand using a tiny &lt;code&gt;gpt-5.4-nano&lt;/code&gt; model.&lt;/li&gt;
&lt;li&gt;For the OpenAPI/API Reference, I split the main schema by tags (endpoints). Each section got its own highly compressed summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: The "AI Receptionist" Guardrail
&lt;/h3&gt;

&lt;p&gt;When a user asks a question, it doesn't touch the main, more expensive LLM right away. The first line of defense is a &lt;code&gt;gpt-5.4-nano&lt;/code&gt; model acting as a "receptionist." It handles two critical tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Intent Validation:&lt;/strong&gt; It verifies if the query is actually relevant to &lt;a href="https://linkshift.app/" rel="noopener noreferrer"&gt;LinkShift&lt;/a&gt;. This ensures no one is using my API budget to do their computer science homework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Routing:&lt;/strong&gt; It scans the pre-made lightweight summaries and pinpoints the exact documentation files needed to answer the question. I set a safe upper limit of 10 files, but the model usually dynamically selects just 3-6 highly relevant ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result? We only pass a fraction of the total documentation into the next stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Precise Generation &amp;amp; The "Low-Token Context Hack"
&lt;/h3&gt;

&lt;p&gt;Only now does the slightly heavier model, &lt;code&gt;gpt-5.4-mini&lt;/code&gt;, enter the scene. It ingests the user's query and only the specific files isolated by the receptionist to compile a high-quality, hallucination-free answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Chat History Hack:&lt;/strong&gt;&lt;br&gt;
Keeping full chat logs in memory quickly bloats the context window. To bypass this, every time &lt;code&gt;gpt-5.4-mini&lt;/code&gt; responds, it also generates a single-sentence micro-summary of the conversation so far. On the next turn, I inject &lt;em&gt;only&lt;/em&gt; this micro-summary instead of the entire chat history.&lt;/p&gt;

&lt;p&gt;This keeps the context perfectly intact, answers lightning-fast, and the API bill down to literally pennies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Over-Engineering Syndrome
&lt;/h2&gt;

&lt;p&gt;The best part about this whole setup? I spent days obsessing over this architecture, refining prompts, and stress-testing edge cases - despite currently having exactly &lt;strong&gt;zero users&lt;/strong&gt; (free or paid).&lt;/p&gt;

&lt;p&gt;It’s the classic indie hacker / software engineer trap: building a hyper-optimized, infinitely scalable infrastructure for massive traffic before making a single dollar.&lt;/p&gt;

&lt;p&gt;On the bright side, the system is bulletproof, safe from wallet-draining exploits, and ready for whatever comes next.&lt;/p&gt;

&lt;p&gt;If you want to test it out, try to break the receptionist guardrail, or just see how it handles technical queries, feel free to play with it here: &lt;a href="https://linkshift.app/docs" rel="noopener noreferrer"&gt;linkshift.app/docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle context costs in your own LLM projects? Do you use a similar routing system, or do you prefer standard vector databases (RAG)? Let’s discuss in the comments!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz39cju0mlsnmv18n2bn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz39cju0mlsnmv18n2bn.png" alt=" " width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
