<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/"/>
    <language>en</language>
    <item>
      <title>I built a zero-dependency CLI to audit hreflang (international SEO)</title>
      <dc:creator>Dr. Reza Madahzadeh</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:19:32 +0000</pubDate>
      <link>https://dev.to/madahzadeh/i-built-a-zero-dependency-cli-to-audit-hreflang-international-seo-47bm</link>
      <guid>https://dev.to/madahzadeh/i-built-a-zero-dependency-cli-to-audit-hreflang-international-seo-47bm</guid>
      <description>&lt;p&gt;If your site serves multiple languages, hreflang is where SEO quietly breaks: page A links B, B never links back; someone writes &lt;code&gt;en_US&lt;/code&gt; instead of &lt;code&gt;en-US&lt;/code&gt;; an alternate points at a redirect; the target canonicalizes somewhere else. None of it is visible in the browser — all of it costs you in the SERPs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;hreflang-audit&lt;/strong&gt; is a single-file Node.js 20+ CLI with zero npm dependencies. It crawls your site (or parses your XML sitemap, sitemap-index included), reads &lt;code&gt;&amp;lt;link rel="alternate" hreflang&amp;gt;&lt;/code&gt; elements, HTTP &lt;code&gt;Link:&lt;/code&gt; headers, and &lt;code&gt;xhtml:link&lt;/code&gt; sitemap entries, and runs 11 checks — from invalid ISO codes (with hints like &lt;code&gt;en-UK&lt;/code&gt; → &lt;code&gt;en-GB&lt;/code&gt;) to missing return links and canonical conflicts.&lt;/p&gt;

&lt;p&gt;Every finding comes with a fix hint. Output is a human-readable report, or &lt;code&gt;--json&lt;/code&gt; for machines, with exit codes designed for CI. There's also a composite GitHub Action so you can run it on a weekly schedule and fail the build when hreflang regresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in under a minute (no npm install)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/madahzadeh/hreflang-audit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hreflang-audit
npm run demo   &lt;span class="c"&gt;# offline demo with seeded issues&lt;/span&gt;
node hreflang-audit.mjs https://example.com/   &lt;span class="c"&gt;# audit a real site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E001&lt;/td&gt;
&lt;td&gt;invalid ISO language/script/region codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E002&lt;/td&gt;
&lt;td&gt;underscore locales (&lt;code&gt;en_US&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E003&lt;/td&gt;
&lt;td&gt;relative hreflang URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E004&lt;/td&gt;
&lt;td&gt;missing return links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E005&lt;/td&gt;
&lt;td&gt;conflicting entries (same lang, two URLs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E006&lt;/td&gt;
&lt;td&gt;broken target URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W101–W105&lt;/td&gt;
&lt;td&gt;missing self-reference / x-default, redirected targets, canonical mismatches, duplicates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;HTML is parsed with tolerant regexes rather than a full DOM, JavaScript-rendered pages are not executed, and it complements — not replaces — Search Console's reports.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/madahzadeh/hreflang-audit" rel="noopener noreferrer"&gt;github.com/madahzadeh/hreflang-audit&lt;/a&gt; — MIT licensed. Feedback and issues welcome; sitemap/HTML mismatch detection is next on my list.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>node</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Designing a Zero-Trust Authorization Layer for Collection Actions</title>
      <dc:creator>MATT ROSE</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:14:05 +0000</pubDate>
      <link>https://dev.to/matt_rose_9d0fe88d3533a4f/designing-a-zero-trust-authorization-layer-for-collection-actions-599f</link>
      <guid>https://dev.to/matt_rose_9d0fe88d3533a4f/designing-a-zero-trust-authorization-layer-for-collection-actions-599f</guid>
      <description>&lt;p&gt;Most recovery technology is built for execution throughput: dialing numbers, streaming SMS, rendering demand letters,&lt;br&gt;
syndicating portfolios, reporting tradelines, or dispatching field agents. The harder architectural problem is distributed across multiple autonomous systems:&lt;/p&gt;

&lt;p&gt;Is this specific actor authorized to perform this specific action on this account across this channel right now?&lt;/p&gt;

&lt;p&gt;An account eligible during Monday's batch pipeline is not necessarily eligible when the worker thread fires. A payment&lt;br&gt;
clears in the core ledger. Consent is revoked via SMS. A formal dispute is received by outside counsel. A bankruptcy docket&lt;br&gt;
update lands in servicing. When downstream tools act on stale snapshots, the failure is not a lack of rules—it is just-in-time&lt;br&gt;
authorization over fragmented, asynchronous state.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treating Recovery Actions as Zero-Trust Resources
Mapping NIST SP 800-207 Zero Trust Architecture directly onto recovery operations separates decision logic from
execution logic:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;• Policy Information Points (PIP): Creditor ledgers, servicing platforms, payment gateways, dispute queues, court&lt;br&gt;
scrapers, and consent registries supplying timestamped facts.&lt;br&gt;
• Policy Decision Point (PDP): The deterministic adjudication engine that evaluates normalized facts against immutable,&lt;br&gt;
versioned policy packs.&lt;br&gt;
• Policy Enforcement Point (PEP): The dialer daemon, SMS proxy, CRA reporting worker, or case management gateway&lt;br&gt;
that strictly fails closed without a valid permit.&lt;br&gt;
• Policy Administrator (PA): The service managing real-time cryptographic permit issuance, verification, and revocation.&lt;br&gt;
The core operational axiom: No current, valid authorization → No execution.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Universal Action Request Contract
The request contract remains invariant across communication channels and legal actions. It uses privacy-preserving
pseudonymous references (HMACs) rather than raw PII payloads:
&lt;/li&gt;
&lt;/ol&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;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_01J9X8K2M1N7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issuer_corp_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"organization_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"agency_partner_44"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"system_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dialer_cluster_east"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent_772"&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;span class="nl"&gt;"subject_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"subj_hmac_8f9a2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"account_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acct_hmac_3c1b7..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CALL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VOICE"&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_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dest_hmac_9e4d1..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requested_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-26T15:42:19Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"COLLECTION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US-FL"&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;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;ol&gt;
&lt;li&gt;Fact Provenance &amp;amp; The Four Epistemic States
Incoming evidence cannot be accepted without cryptographic and temporal provenance. Facts carry verification tier,
observation timestamps, and explicit TTL windows. The resolver must strictly classify evidence into four states:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;1. MISSING: A mandatory statutory or policy fact is absent from the graph.&lt;/li&gt;
&lt;li&gt;2. STALE: The fact exists but exceeds its maximum allowed freshness window.&lt;/li&gt;
&lt;li&gt;3. CONFLICTING: Two independent authoritative sources contradict each other (e.g., active consent vs. opt-out&lt;/li&gt;
&lt;li&gt;timestamp).&lt;/li&gt;
&lt;li&gt;4. INVALID: The value violates schema, physical reality, or statutory preconditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a vital boundary: BLOCK means the system possesses verifiable evidence that an action is unlawful or&lt;br&gt;
suppressed. NO_USABLE_RESULT means the system cannot guarantee safety and must fail closed. Conflating the two&lt;br&gt;
invites unmeasured fallback risk.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deterministic Decision Engines vs. Probabilistic LLMs
LLMs are suited for extracting facts from unstructured legal notices, reconciling dirty records, and preparing human review
summaries. LLMs must never make the final authorization decision. The decision function must remain pure and
deterministic: f(Normalized Facts, Policy Version, Timestamp) → Decision Packet.
&lt;/li&gt;
&lt;/ol&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;"decision_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dec_01J9X8K9Z4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BLOCK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requested_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CALL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason_codes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CONSENT_REVOKED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPT_OUT_NEWER_THAN_CAMPAIGN"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"pack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US_CONSUMER_CONTACT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"2026.08.24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:7c9..."&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;span class="nl"&gt;"facts_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:4f1a2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"decided_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"2026-08-26T15:42:19.012Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"evidence_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ev_01J9X8K9Z4"&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;ol&gt;
&lt;li&gt;Sender-Constrained Permits &amp;amp; Durable Receipts
Conventional bearer JWTs are unsafe for high-consequence operations. An Action Permit must be sender-constrained via
cryptographic proof-of-possession (RFC 9449 DPoP), audience-restricted, single-use, and short-lived (e.g., 30-second
TTL). The architecture cleanly splits authorization into two artifacts:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;• Action Permit: Minimal, ephemeral credential verified by the PEP at runtime.&lt;br&gt;
• Decision Receipt: Append-only immutable record stored with full evidence lineage for audits.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Closing the Loop: Execution Receipts&lt;br&gt;
Authorization without verified execution creates an unobservable gap. The PEP must return an Execution Receipt&lt;br&gt;
confirming when and how the action occurred, forming a complete cryptographic chain: Request → Decision → Permit →&lt;br&gt;
Execution → Receipt → Audit Graph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-Time Revocation &amp;amp; State Synchronization&lt;br&gt;
Short TTLs reduce exposure, but critical events (cures, bankruptcy filings, attorney notices) happen asynchronously. The&lt;br&gt;
architecture requires a sub-second revocation bus. A system advertising 5ms decisions is unsafe if an account recall takes&lt;br&gt;
15 minutes to reach the dialer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Regulatory exposure in collections stems from broken state synchronization across distributed systems. Building an&lt;br&gt;
independent zero-trust authorization plane solves this failure mode permanently.&lt;/p&gt;

&lt;p&gt;Your systems execute. Quorum CAS-1 decides whether the action is authorized.&lt;/p&gt;

&lt;p&gt;References&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NIST SP 800-207: Zero Trust Architecture

RFC 9700: OAuth 2.0 Security Best Current Practice

RFC 9449: Demonstrating Proof of Possession (DPoP)

CFPB FDCPA Annual Report 2025
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>security</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Configurar Graphify para usarlo con Claude cli</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:13:09 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/configurar-graphify-para-usarlo-con-claude-cli-k86</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/configurar-graphify-para-usarlo-con-claude-cli-k86</guid>
      <description>&lt;h2&gt;
  
  
  Instalación y uso de Graphify (Linux / WSL)
&lt;/h2&gt;

&lt;p&gt;(Contenido creado con IA)&lt;/p&gt;

&lt;p&gt;Guía paso a paso para instalar &lt;a href="https://github.com/safishamsi/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt; una vez en el&lt;br&gt;
entorno (máquina/WSL) y luego activarlo en cada proyecto. Basada en la instalación real hecha en&lt;br&gt;
&lt;code&gt;drupal&lt;/code&gt; (ver sección 8 para las particularidades de ese proyecto).&lt;/p&gt;

&lt;p&gt;Graphify convierte una carpeta de código/config/docs en un grafo de conocimiento consultable&lt;br&gt;
(&lt;code&gt;graph.json&lt;/code&gt;, &lt;code&gt;graph.html&lt;/code&gt;, &lt;code&gt;GRAPH_REPORT.md&lt;/code&gt;) y puede integrarse con Claude Code para que&lt;br&gt;
consulte ese grafo antes de rastrear todo el código.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Qué es global y qué es por-proyecto
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Elemento&lt;/th&gt;
&lt;th&gt;Alcance&lt;/th&gt;
&lt;th&gt;Cuándo se instala&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;uv&lt;/code&gt; (gestor de Python)&lt;/td&gt;
&lt;td&gt;Global, una vez por máquina&lt;/td&gt;
&lt;td&gt;Sección 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;graphifyy&lt;/code&gt; (el CLI &lt;code&gt;graphify&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Global, una vez por máquina&lt;/td&gt;
&lt;td&gt;Sección 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key del backend LLM&lt;/td&gt;
&lt;td&gt;Global (variable de entorno / &lt;code&gt;.env&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Sección 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;graphify-out/&lt;/code&gt; (graph.json, graph.html, GRAPH_REPORT.md)&lt;/td&gt;
&lt;td&gt;Por proyecto — es el dato generado a partir de ESE código&lt;/td&gt;
&lt;td&gt;Sección 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; (hook), &lt;code&gt;.claude/skills/graphify/&lt;/code&gt;, sección en &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Por proyecto&lt;/td&gt;
&lt;td&gt;Sección 6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No hace falta reinstalar &lt;code&gt;uv&lt;/code&gt;/&lt;code&gt;graphify&lt;/code&gt; en cada repo. Lo único que se repite por proyecto es&lt;br&gt;
generar su grafo y activar la integración con Claude Code.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Instalar Graphify en el entorno (una vez)
&lt;/h2&gt;

&lt;p&gt;Requisitos: Python 3.10+ (ya viene en la mayoría de distros/WSL) y &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Instalar uv (gestiona su propio Python, no depende del venv del sistema)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin/env"&lt;/span&gt;   &lt;span class="c"&gt;# o reinicia la shell; añade ~/.local/bin al PATH&lt;/span&gt;

&lt;span class="c"&gt;# 2. Instalar graphify como herramienta global&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;graphifyy

&lt;span class="c"&gt;# 3. Comprobar&lt;/span&gt;
graphify &lt;span class="nt"&gt;--version&lt;/span&gt;
graphify &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nota WSL/Debian/Ubuntu&lt;/strong&gt;: si en algún momento usas &lt;code&gt;python3 -m venv&lt;/code&gt; directamente y falla con&lt;br&gt;
&lt;code&gt;No module named ensurepip&lt;/code&gt; pidiendo &lt;code&gt;apt install python3.X-venv&lt;/code&gt; (requiere sudo), &lt;strong&gt;no lo&lt;br&gt;
instales para esto&lt;/strong&gt;: usa &lt;code&gt;uv&lt;/code&gt;, que no depende del módulo &lt;code&gt;venv&lt;/code&gt; del sistema. Así es como lo&lt;br&gt;
resolvimos en &lt;code&gt;drupal&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.1 Instalar el soporte del backend LLM que vayas a usar
&lt;/h3&gt;

&lt;p&gt;El extractor semántico necesita un paquete de cliente extra según el backend. Instálalo con el&lt;br&gt;
extra correspondiente (si no sabes cuál usarás, &lt;code&gt;all&lt;/code&gt; los trae todos):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt;     &lt;span class="c"&gt;# Google Gemini&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[anthropic]"&lt;/span&gt;  &lt;span class="c"&gt;# Claude&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[openai]"&lt;/span&gt;     &lt;span class="c"&gt;# OpenAI&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[ollama]"&lt;/span&gt;     &lt;span class="c"&gt;# modelo local, sin API key&lt;/span&gt;
&lt;span class="c"&gt;# o directamente:&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[all]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si lo instalaste sin el extra y &lt;code&gt;graphify extract&lt;/code&gt; falla con &lt;code&gt;the 'openai'/'anthropic' package is&lt;br&gt;
required for this backend&lt;/code&gt;, reinstala con &lt;code&gt;--force&lt;/code&gt; y el extra correcto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Esto nos pasó la primera vez: el backend &lt;code&gt;gemini&lt;/code&gt; usa internamente el cliente &lt;code&gt;openai&lt;/code&gt;, y no se&lt;br&gt;
instala por defecto.)&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Configurar la API key
&lt;/h2&gt;

&lt;p&gt;Solo hace falta si vas a extraer contenido semántico (docs, config, PDFs, imágenes). El parsing de&lt;br&gt;
código puro (&lt;code&gt;--code-only&lt;/code&gt;) no requiere key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Variable de entorno&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GEMINI_API_KEY&lt;/code&gt; o &lt;code&gt;GOOGLE_API_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MOONSHOT_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;ninguna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock&lt;/td&gt;
&lt;td&gt;credenciales AWS/IAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tu-clave"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cuidado con las comillas&lt;/strong&gt;: si la clave está en un &lt;code&gt;.env&lt;/code&gt; como &lt;code&gt;GEMINI_API_KEY="AQ.xxx"&lt;/code&gt;, un&lt;br&gt;
&lt;code&gt;cut -d'=' -f2-&lt;/code&gt; se lleva las comillas literales dentro del valor y Gemini responde&lt;br&gt;
&lt;code&gt;Please pass a valid API key&lt;/code&gt;. Al extraer el valor de un &lt;code&gt;.env&lt;/code&gt;, quítalas explícitamente:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^GEMINI_API_KEY='&lt;/span&gt; .env | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="p"&gt;%\&lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;; val=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="p"&gt;#\&lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;
export GEMINI_API_KEY=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$val&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Límites del tier gratuito&lt;/strong&gt;: el tier gratuito de Gemini (&lt;code&gt;gemini-3-flash&lt;/code&gt;) tiene límites bajos&lt;br&gt;
(5 peticiones/min, 250k tokens/min). Si &lt;code&gt;graphify extract&lt;/code&gt; falla con errores &lt;code&gt;429&lt;br&gt;
RESOURCE_EXHAUSTED&lt;/code&gt;, repite con &lt;code&gt;--max-concurrency 1&lt;/code&gt; (más lento pero no satura la cuota). El&lt;br&gt;
caché interno evita reprocesar lo que ya se extrajo con éxito.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  4. Decidir el alcance del grafo en un proyecto
&lt;/h2&gt;

&lt;p&gt;Dos formas de usarlo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A) Graficar todo el repo&lt;/strong&gt; (proyectos pequeños, sin &lt;code&gt;vendor/&lt;/code&gt;, &lt;code&gt;node_modules/&lt;/code&gt;, dumps de BD, etc.&lt;br&gt;
mezclados con el código):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mi-proyecto
graphify extract &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B) Graficar solo unas rutas concretas&lt;/strong&gt; (recomendado en proyectos con dependencias de terceros,&lt;br&gt;
builds, backups SQL, &lt;code&gt;sites/default/files&lt;/code&gt;, etc. que no aportan nada al grafo y disparan coste/&lt;br&gt;
ruido). Se crea una copia física de solo esas rutas y se extrae sobre la copia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mi-proyecto
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; graphify/source/&amp;lt;misma-estructura-de-carpetas&amp;gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ruta/a/incluir-1 graphify/source/ruta/a/incluir-1
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ruta/a/incluir-2 graphify/source/ruta/a/incluir-2
&lt;span class="c"&gt;# ... una copia por cada ruta a incluir, preservando la ruta relativa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Añade &lt;code&gt;graphify/&lt;/code&gt; a &lt;code&gt;.gitignore&lt;/code&gt; para que esta copia y las herramientas no se comiteen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/graphify/'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto es lo que se hizo en &lt;code&gt;drupal&lt;/code&gt;: ver sección 8 para el ejemplo completo con&lt;br&gt;
&lt;code&gt;config/sync&lt;/code&gt;, módulos y temas custom/contrib concretos.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Generar el grafo
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extracción completa (AST + semántica vía LLM)&lt;/span&gt;
graphify extract &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini

&lt;span class="c"&gt;# Si el backend gratuito da 429, repetir más despacio:&lt;/span&gt;
graphify extract &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1

&lt;span class="c"&gt;# Generar/regenerar GRAPH_REPORT.md + graph.html con nombres de comunidad&lt;/span&gt;
graphify cluster-only &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Flags útiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--no-gitignore&lt;/code&gt;: procesa también archivos que estén en &lt;code&gt;.gitignore&lt;/code&gt; (útil si &lt;code&gt;&amp;lt;ruta&amp;gt;&lt;/code&gt; es una
copia aislada como en el método B, o si hay config versionada que normalmente se ignora).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--code-only&lt;/code&gt;: solo AST determinista, sin LLM, sin API key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--force&lt;/code&gt;: fuerza re-extracción completa ignorando caché.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resultado en &lt;code&gt;&amp;lt;ruta&amp;gt;/graphify-out/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;graph.json&lt;/code&gt; — datos completos del grafo (nodos, edges, comunidades)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graph.html&lt;/code&gt; — visualización interactiva&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GRAPH_REPORT.md&lt;/code&gt; — resumen en lenguaje natural + preguntas sugeridas&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Es normal que algunos archivos de configuración muy simples (YAML triviales, toggles) no&lt;br&gt;
generen nodo semántico propio — el modelo decide que no aportan nada nuevo más allá del AST. No&lt;br&gt;
es un fallo; el resto del grafo (código, relaciones) sigue siendo válido.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  6. Integrar con Claude Code en el proyecto
&lt;/h2&gt;

&lt;p&gt;Si &lt;code&gt;&amp;lt;ruta&amp;gt;&lt;/code&gt; del paso anterior es la raíz del proyecto, el grafo ya queda en&lt;br&gt;
&lt;code&gt;./graphify-out/graph.json&lt;/code&gt;, la ubicación por defecto que espera Graphify (hook, skill y comandos&lt;br&gt;
sin &lt;code&gt;--graph&lt;/code&gt; explícito).&lt;/p&gt;

&lt;p&gt;Si usaste el método B (copia aislada, p. ej. &lt;code&gt;graphify/source/&lt;/code&gt;), el grafo se genera por defecto&lt;br&gt;
dentro de &lt;code&gt;graphify/source/graphify-out/&lt;/code&gt;, no en la raíz. Muévelo una vez a la raíz (ubicación&lt;br&gt;
que espera el resto de la integración):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;graphify/source/graphify-out ./graphify-out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partir de ahí, cualquier &lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; que vuelvas a lanzar sobre &lt;code&gt;graphify/source&lt;/code&gt; debe&lt;br&gt;
llevar la variable &lt;code&gt;GRAPHIFY_OUT&lt;/code&gt; apuntando a esa ruta absoluta, o volverá a escribir dentro de&lt;br&gt;
&lt;code&gt;graphify/source/graphify-out&lt;/code&gt; (duplicando el grafo en vez de actualizar el de la raíz):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out"&lt;/span&gt;
graphify update graphify/source        &lt;span class="c"&gt;# o extract, con --backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Una alternativa más simple si no te importa el symlink es dejar &lt;code&gt;graphify-out&lt;/code&gt; como&lt;br&gt;
&lt;code&gt;ln -s graphify/source/graphify-out graphify-out&lt;/code&gt;: las lecturas (&lt;code&gt;query&lt;/code&gt;, el hook, &lt;code&gt;graph.html&lt;/code&gt;)&lt;br&gt;
y las siguientes escrituras de &lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update graphify/source&lt;/code&gt; (que escriben en&lt;br&gt;
&lt;code&gt;graphify/source/graphify-out&lt;/code&gt;, el destino real del enlace) siguen funcionando igual sin tocar&lt;br&gt;
&lt;code&gt;GRAPHIFY_OUT&lt;/code&gt;. Mover el directorio de verdad a la raíz (como se hizo arriba) es preferible por&lt;br&gt;
tenerlo en la ubicación canónica sin depender de un enlace, pero ambas opciones son válidas.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instala la integración oficial (hook + skill + nota en &lt;code&gt;CLAUDE.md&lt;/code&gt;), sin &lt;code&gt;--strict&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify claude &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto crea/actualiza:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; — hooks &lt;code&gt;PreToolUse&lt;/code&gt; (advisory, nunca bloqueante sin &lt;code&gt;--strict&lt;/code&gt;): avisan
antes de un &lt;code&gt;grep&lt;/code&gt;/&lt;code&gt;Bash&lt;/code&gt;/&lt;code&gt;Grep&lt;/code&gt; o de leer un archivo de código, sugiriendo &lt;code&gt;graphify query&lt;/code&gt;
primero.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/skills/graphify/&lt;/code&gt; — skill del comando &lt;code&gt;/graphify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; — sección &lt;code&gt;## graphify&lt;/code&gt; con las reglas de cuándo consultar el grafo.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Revisa la sección añadida a &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt;: por defecto dice &lt;code&gt;graphify update .&lt;/code&gt; para&lt;br&gt;
refrescar el grafo tras cambios de código. Si usaste el método B (copia aislada), eso es&lt;br&gt;
&lt;strong&gt;incorrecto&lt;/strong&gt; — &lt;code&gt;.&lt;/code&gt; es la raíz real del proyecto, no la copia, y lanzaría una extracción sin&lt;br&gt;
acotar sobre todo el repo. Corrígelo a mano por &lt;code&gt;graphify update graphify/source&lt;/code&gt; (o la ruta de&lt;br&gt;
tu copia).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--strict&lt;/code&gt; es opcional y no se activó por defecto&lt;/strong&gt;: bloquearía la primera lectura de código&lt;br&gt;
por sesión hasta consultar el grafo. Con un grafo que no cubre todo el repo (método B), eso&lt;br&gt;
puede bloquear tareas legítimas fuera del alcance graficado — se dejó desactivado.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  7. Mantenimiento
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tras cambiar código dentro de las rutas graficadas&lt;/strong&gt;: &lt;code&gt;graphify update &amp;lt;ruta&amp;gt;&lt;/code&gt; (solo AST, sin
coste de API). Si usaste copia aislada, primero hay que reflejar el cambio dentro de
&lt;code&gt;graphify/source/&lt;/code&gt; (copiando el archivo modificado) y luego correr &lt;code&gt;update&lt;/code&gt; sobre esa copia con
&lt;code&gt;GRAPHIFY_OUT&lt;/code&gt; apuntando al &lt;code&gt;graphify-out/&lt;/code&gt; real (ver sección 6):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out"&lt;/span&gt;
  graphify update graphify/source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-extracción semántica completa&lt;/strong&gt; (por ejemplo tras añadir muchas rutas nuevas):
&lt;code&gt;graphify extract &amp;lt;ruta&amp;gt; --backend &amp;lt;backend&amp;gt; --force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desinstalar de un proyecto&lt;/strong&gt;: &lt;code&gt;graphify uninstall --project&lt;/code&gt; (quita hook, skill y sección de
&lt;code&gt;CLAUDE.md&lt;/code&gt;; con &lt;code&gt;--purge&lt;/code&gt; borra también &lt;code&gt;graphify-out/&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Limitaciones conocidas (comprobadas en la práctica)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cobertura parcial&lt;/strong&gt;: el grafo solo "sabe" de las rutas que se graficaron. Para cualquier otra
parte del proyecto, hay que seguir buscando en el código directamente — decídselo explícitamente
a Claude Code en &lt;code&gt;CLAUDE.md&lt;/code&gt; si usas el método B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El hook de lectura (&lt;code&gt;Read&lt;/code&gt;/&lt;code&gt;Glob&lt;/code&gt;) no se dispara para cualquier extensión&lt;/strong&gt;: solo para un
listado fijo de extensiones "genéricas" (&lt;code&gt;.php&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, etc. — ver
&lt;code&gt;_HOOK_SOURCE_EXTS&lt;/code&gt; en el código de Graphify). En un proyecto Drupal, por ejemplo, &lt;strong&gt;no&lt;/strong&gt; se
dispara para &lt;code&gt;.module&lt;/code&gt;, &lt;code&gt;.install&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.theme&lt;/code&gt; ni &lt;code&gt;.twig&lt;/code&gt;, que es donde vive buena parte
del código/config. El aviso de búsqueda (&lt;code&gt;Bash|Grep&lt;/code&gt;) sí se dispara siempre que exista el grafo,
independientemente de la extensión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coste y cuotas&lt;/strong&gt;: la extracción semántica consume tokens del backend elegido. El tier
gratuito de Gemini es fácil de agotar en proyectos con muchos archivos de config; usar
&lt;code&gt;--max-concurrency 1&lt;/code&gt; lo evita a costa de tardar más.&lt;/li&gt;
&lt;li&gt;El grafo es un punto de partida para orientarse, no la fuente de verdad — conviene verificar
siempre en el código real antes de editar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Ejemplo real: &lt;code&gt;drupal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;En este proyecto se usó el método B (copia aislada) porque el repo mezcla Drupal core, &lt;code&gt;vendor/&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;sites/default/files&lt;/code&gt;, dumps SQL, etc. junto al código propio. Solo interesaba graficar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="n"&gt;config/sync/**&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/modules/custom/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/modules/contrib/quiz&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/themes/custom/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/themes/contrib/bootstrap_barrio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pasos aplicados (resumen; ver &lt;code&gt;CLAUDE.md&lt;/code&gt; del proyecto para el resultado final):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 0. Instalación global (una vez en la máquina/WSL)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt;     &lt;span class="c"&gt;# ~/.local/bin ya estaba en el PATH&lt;/span&gt;

&lt;span class="c"&gt;# 1. Copia aislada de solo las rutas a graficar&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; graphify/source/config graphify/source/web/modules/custom &lt;span class="se"&gt;\&lt;/span&gt;
         graphify/source/web/modules/contrib graphify/source/web/themes/custom &lt;span class="se"&gt;\&lt;/span&gt;
         graphify/source/web/themes/contrib
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; config/sync graphify/source/config/sync
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/modules/custom/. graphify/source/web/modules/custom/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/modules/contrib/quiz graphify/source/web/modules/contrib/quiz
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/themes/custom/. graphify/source/web/themes/custom/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/themes/contrib/bootstrap_barrio graphify/source/web/themes/contrib/bootstrap_barrio

&lt;span class="c"&gt;# 2. Extracción (config/sync son YAML "contenido/conocimiento", no solo código -&amp;gt; --no-gitignore)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
graphify extract graphify/source &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--no-gitignore&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1
graphify cluster-only graphify/source &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 100

&lt;span class="c"&gt;# 3. Mover el grafo a la raíz (ubicación canónica) e integrar con Claude Code&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;graphify/source/graphify-out ./graphify-out
graphify claude &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;
&lt;span class="c"&gt;# (y corregir a mano el "graphify update ." -&amp;gt; ver sección 6/7, con GRAPHIFY_OUT, en CLAUDE.md)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resultado final: &lt;code&gt;./graphify-out/graph.json&lt;/code&gt; (directorio real en la raíz, ~1600 nodos / ~2850&lt;br&gt;
edges / ~296 comunidades), &lt;code&gt;graphify&lt;/code&gt; instalado en global (&lt;code&gt;~/.local/bin/graphify&lt;/code&gt;), copia&lt;br&gt;
aislada del código en &lt;code&gt;graphify/source/&lt;/code&gt; (usada solo como entrada para futuras&lt;br&gt;
&lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt;), y el hook + la nota en &lt;code&gt;CLAUDE.md&lt;/code&gt; activos para ese subconjunto de rutas.&lt;/p&gt;

</description>
      <category>graphify</category>
      <category>claude</category>
      <category>drupal</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Deploying an Independence Day Web Game with Tencent EdgeOne Makers: A Developer’s Guide</title>
      <dc:creator>Poundrafasya</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:12:44 +0000</pubDate>
      <link>https://dev.to/poundra/deploying-an-independence-day-web-game-with-tencent-edgeone-makers-a-developers-guide-3c61</link>
      <guid>https://dev.to/poundra/deploying-an-independence-day-web-game-with-tencent-edgeone-makers-a-developers-guide-3c61</guid>
      <description>&lt;p&gt;Transitioning a local development project into as live, globally accessible application is a critical milestone for any developer. Recently, I developed a responsive web game to celebrate Indonesian Independence Day. The game features interactive mechanics like a double-jump system and collectible kelapa muda (young coconuts) items. While the game ran flawlessly on a local server, the challenge was finding a deployment platform that could deliver static assets rapidly, handle traffic efficiently, and provide a seamless workflow without incurring high costs.&lt;/p&gt;

&lt;p&gt;Enter Tencent EdgeOne Makers. Previously known as EdgeOne Pages, this platform has evolved into a robust full-stack hosting solution for Web and AI Agents. This article serves as a technical review and a step-by-step tutorial on how to leverage EdgeOne Makers to deploy a web application.&lt;/p&gt;

&lt;p&gt;Why Tencent EdgeOne Makers?&lt;br&gt;
When evaluating platforms for a web game, asset delivery speed is the top priority. High latency can ruin the player experience, especially for timing-based mechanics like a double-jump. EdgeOne Makers solves this through its underlying infrastructure:&lt;/p&gt;

&lt;p&gt;Global Edge Computing &amp;amp; CDN: The platform caches static assets (like the HTML canvas, CSS, JavaScript files, and the kelapa muda image sprites) across global edge nodes. This ensures the game loads instantly for users regardless of their location.&lt;/p&gt;

&lt;p&gt;Seamless Git Integration: It connects directly to repositories, enabling Continuous Integration/Continuous Deployment (CI/CD). Every push to the main branch automatically triggers a new build.&lt;/p&gt;

&lt;p&gt;Built-in Security: It includes Web Application Firewall (WAF) capabilities, protecting the site from common web vulnerabilities out of the box.&lt;/p&gt;

&lt;p&gt;AI Integration Ready: For future enhancements, the platform supports AI Agents and offers generous API token allowances, making it easy to integrate intelligent features later.&lt;/p&gt;

&lt;p&gt;Step-by-Step Deployment Tutorial&lt;br&gt;
Deploying the Independence Day web game took only a few minutes. Here is the exact workflow:&lt;/p&gt;

&lt;p&gt;Step 1: Repository Preparation&lt;br&gt;
Ensure your project is pushed to a Git provider (GitHub, GitLab, or Bitbucket). For this game, the repository contained standard web files (HTML, CSS, JS) optimized for responsive rendering on both mobile and desktop screens.&lt;/p&gt;

&lt;p&gt;Step 2: Connecting to EdgeOne Makers&lt;br&gt;
Navigate to the Tencent EdgeOne Makers console. Select the option to create a new project and connect your Git account. The authorization process is straightforward. Once linked, select the specific repository containing the web game.&lt;/p&gt;

&lt;p&gt;Step 3: Build Configuration&lt;br&gt;
EdgeOne Makers automatically detects the framework used. Because this project relies on vanilla web technologies, the build settings were minimal.&lt;/p&gt;

&lt;p&gt;Framework Preset: Auto-detect or Static HTML.&lt;/p&gt;

&lt;p&gt;Build Command: Left blank (or specify npm run build if you are migrating the game to Vite or React).&lt;/p&gt;

&lt;p&gt;Output Directory: Usually . for the root directory or dist if a bundler is used.&lt;/p&gt;

&lt;p&gt;Step 4: Deployment and Global Distribution&lt;br&gt;
Click "Deploy." The platform provisions the environment, pulls the code, and distributes the assets across its edge network. Within seconds, the console provides a live URL. The deployment logs are transparent, allowing you to monitor the build process step-by-step.&lt;/p&gt;

&lt;p&gt;Technical Review &amp;amp; Best Practices&lt;br&gt;
Testing the live URL demonstrated significant performance advantages. The game assets loaded with zero noticeable delay, and the responsive UI adapted perfectly across different devices. The double-jump mechanics remained highly responsive, proving that the edge delivery effectively minimized latency.&lt;/p&gt;

&lt;p&gt;Best Practice Tip: Always optimize your image assets before deployment. While the CDN is fast, compressing sprites and background images ensures the initial payload is as small as possible, providing an optimal experience for users on mobile networks.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Tencent EdgeOne Makers provides a frictionless, enterprise-grade deployment experience that is accessible to developers at any level. It eliminates the complexities of server management and CI/CD configuration, allowing you to focus entirely on code and creativity. Whether you are deploying a simple static portfolio, a complex React application, or an interactive web game, this platform is an outstanding choice to bring your projects online securely and efficiently.&lt;/p&gt;

&lt;h1&gt;
  
  
  TencentEdgeOne #EdgeOneMakers #CODEPOLITAN #EdgeOne
&lt;/h1&gt;

</description>
      <category>codepolitan</category>
      <category>tencentedgeone</category>
      <category>edgeone</category>
      <category>edgeonemakers</category>
    </item>
    <item>
      <title>Your WordPress Search Plugin Should Return Post IDs and Nothing Else</title>
      <dc:creator>Rafał Groń</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:11:13 +0000</pubDate>
      <link>https://dev.to/gronrafal/your-wordpress-search-plugin-should-return-post-ids-and-nothing-else-51d4</link>
      <guid>https://dev.to/gronrafal/your-wordpress-search-plugin-should-return-post-ids-and-nothing-else-51d4</guid>
      <description>&lt;p&gt;In the space of a month I was asked three questions that looked completely unrelated:&lt;/p&gt;

&lt;p&gt;Can search work on a site translated with TranslatePress, where a visitor types in one language and reads in another?&lt;br&gt;
Can search hide course content the visitor hasn't bought?&lt;br&gt;
Why does search behave differently inside a premium theme's AJAX overlay?&lt;br&gt;
Different plugins, different problem domains, different people asking. All three turned out to be the same question, and the answer is an architectural decision you make once, near the start, and then live with.&lt;/p&gt;

&lt;p&gt;The choice&lt;br&gt;
If you write a plugin that replaces WordPress search, you pick one of two shapes.&lt;/p&gt;

&lt;p&gt;Shape one: own the pipeline. You build your own index, you match against it, you produce the result list, and you render it. You control everything from query string to markup.&lt;/p&gt;

&lt;p&gt;Shape two: answer with post IDs. You do your matching however you like, then hand WordPress a list of IDs in the order you want them, and let the rest of WordPress carry on as normal.&lt;/p&gt;

&lt;p&gt;Shape one is tempting because it is the only way to guarantee the output looks exactly how you intended. It is also the reason most search plugins fight with everything else on the site.&lt;/p&gt;

&lt;p&gt;Why the first shape breaks on multilingual sites&lt;br&gt;
TranslatePress stores translations in its own tables and applies them at render time. The post itself stays in the original language.&lt;/p&gt;

&lt;p&gt;Now put a search plugin next to it that maintains its own index. That index is built from the original content, because that is what is in the database. A visitor browsing in Finnish types a Finnish word. The plugin looks in its own index, finds nothing that matches, and returns nothing. The translation layer never gets a chance to help, because the plugin never handed anything to WordPress in the first place.&lt;/p&gt;

&lt;p&gt;With the second shape, the flow is different:&lt;/p&gt;

&lt;p&gt;content stored in language A&lt;br&gt;
        ↓&lt;br&gt;
plugin matches, returns post IDs&lt;br&gt;
        ↓&lt;br&gt;
WordPress builds the loop&lt;br&gt;
        ↓&lt;br&gt;
translation layer renders in language C&lt;br&gt;
The plugin does not need to know that translations exist. It never sees them, never indexes them, never has an opinion about them. It answers with IDs and gets out of the way.&lt;/p&gt;

&lt;p&gt;That is not a feature anyone built. It is what you get for free by not owning the last mile.&lt;/p&gt;

&lt;p&gt;Why it matters for access control&lt;br&gt;
The second question came from a site running membership software, where lessons belong to courses and visitors have bought some courses and not others. The request was for search results to exclude content the visitor cannot open.&lt;/p&gt;

&lt;p&gt;If your plugin renders its own results, you now have to understand somebody else's permission model. You have to know how that plugin stores entitlements, keep up as it changes, and repeat the exercise for every membership plugin your users install.&lt;/p&gt;

&lt;p&gt;If your plugin returns IDs, you add one filter:&lt;/p&gt;

&lt;p&gt;$ids = apply_filters( 'my_plugin_result_ids', $ids, $query );&lt;br&gt;
and the site decides. The membership plugin already knows who can see what. It does not need you to reimplement it, and you do not need to ship an integration per vendor.&lt;/p&gt;

&lt;p&gt;The developer who asked me this wrote that filter himself and sent it to me. It is in the plugin now. That is only possible because IDs are the interface.&lt;/p&gt;

&lt;p&gt;The failure mode nobody thinks about&lt;br&gt;
Here is the part I would most like you to take away, because it bit me and it is not obvious.&lt;/p&gt;

&lt;p&gt;To replace search, you generally do two things: you inject your results, and you remove WordPress's own LIKE clause so its keyword matching does not fight your ordering.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
add_filter( 'posts_search', function ( $search, $query ) {&lt;br&gt;
    if ( $query-&amp;gt;is_search() ) {&lt;br&gt;
        return ''; // strip the native LIKE clause&lt;br&gt;
    }&lt;br&gt;
    return $search;&lt;br&gt;
}, 10, 2 );&lt;br&gt;
Perfectly reasonable, and quietly dangerous.&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Consider what happens when your matching fails. Your API times out, or the site runs out of quota, or a network path breaks. You return no IDs. But you have already stripped the native search clause, so WordPress has nothing to fall back on either.&lt;/p&gt;

&lt;p&gt;Search does not degrade. It goes blank. The site owner finds out from a customer.&lt;/p&gt;

&lt;p&gt;The fix is to make the removal conditional on having actually succeeded:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;add_filter( 'posts_search', function ( $search, $query ) {&lt;br&gt;
    if ( $query-&amp;gt;is_search() &amp;amp;&amp;amp; $query-&amp;gt;get( 'my_plugin_used' ) ) {&lt;br&gt;
        return '';&lt;br&gt;
    }&lt;br&gt;
    return $search; // untouched when we did not produce results&lt;br&gt;
}, 10, 2 );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set that flag only after you have results in hand. Now failure means the visitor gets ordinary WordPress keyword search, which is worse than what you offer but very much better than nothing. The shop keeps selling. What is lost is relevance, not availability.&lt;/p&gt;

&lt;p&gt;This is worth more attention than it usually gets. Anything that talks to a network will fail sometimes, and the state you leave the site in when it does is part of your plugin's design, not an accident.&lt;/p&gt;

&lt;p&gt;Ordering, since it is the usual stumbling block&lt;br&gt;
Once you hand over IDs, WordPress will happily reorder them for you unless you say otherwise:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$query-&amp;gt;set( 'post__in', $ids );&lt;br&gt;
$query-&amp;gt;set( 'orderby', 'post__in' );&lt;br&gt;
$query-&amp;gt;set( 'post_type', 'any' );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;orderby =&amp;gt; post__in is the piece people miss. Without it your carefully ranked list comes back sorted by date, and you spend an afternoon convinced your ranking is broken.&lt;/p&gt;

&lt;p&gt;Worth knowing too: a theme can undo this downstream. If a template runs its own query, or a page builder widget has its own Query settings with a fixed Order By, that widget wins. The plugin is fine and the ranking is fine, and the page still shows the wrong order. If a user reports scrambled results, check the template before you check yourself. That was the third question, and the answer turned out to be a theme's own AJAX search intercepting the request before WordPress ever saw it.&lt;/p&gt;

&lt;p&gt;Where I have not finished applying my own argument&lt;br&gt;
Being honest about the limit of the pattern as I have implemented it.&lt;/p&gt;

&lt;p&gt;I hook the main query. That covers the standard search results page, which is where the overwhelming majority of searches happen. It does not cover a secondary WP_Query in a template, and three people have now asked for exactly that, wanting to build a custom results layout that still uses my ranking.&lt;/p&gt;

&lt;p&gt;The right answer is obvious in hindsight and follows directly from everything above: if IDs are the interface, they should be reachable without going through the main query at all.&lt;/p&gt;

&lt;p&gt;$ids = my_plugin_search_ids( $term, [ 'limit' =&amp;gt; 20 ] );&lt;br&gt;
Then a developer can do whatever they like with them, and I do not need to know what.&lt;/p&gt;

&lt;p&gt;It is not built yet. It is a good illustration of the general point though. The value of returning IDs is not the specific hook you use to deliver them. It is that the interface is small enough that other people can build things you did not think of, without asking your permission or waiting for your next release.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Embeddings Cannot Say No: An Intent Detector's Real Numbers</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/embeddings-cannot-say-no-an-intent-detectors-real-numbers-3pg6</link>
      <guid>https://dev.to/julesrobineau/embeddings-cannot-say-no-an-intent-detectors-real-numbers-3pg6</guid>
      <description>&lt;p&gt;My message detector worked on my test set. On messages it had never seen, it missed one real case out of two.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I built a small detector that spots messages asking for an action. It relies on embeddings, a technique that turns a sentence into numbers. It works well, except for one thing: it does not understand negation. "The outage is fixed, thanks" fires exactly like "outage". I explain why this is a flaw of nature, not of tuning, with numbers to back it. And why the right answer is to accept the flaw rather than fix it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers who want to plug in AI without paying a large model on every message. No machine learning background needed: I define everything along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;In a personal project, an AI watches a chat between users. When a message asks for an action, a badge shows up. Create a ticket, send a document.&lt;/p&gt;

&lt;p&gt;Calling an LLM on every message is expensive. In money and in response time. I already made that case here: &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;put the LLM last&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the system has two stages. Stage 1 is a small model, free to run, that sorts messages. Stage 2 is the LLM, called only when the user clicks the badge.&lt;/p&gt;

&lt;p&gt;Stage 1 uses embeddings. An embedding turns a sentence into a list of numbers. Two sentences with close meanings give close lists of numbers. That is all you need to understand for the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding with a similarity score
&lt;/h2&gt;

&lt;p&gt;How does an embedding make a decision? By measuring how much two sentences look alike. That score is called cosine similarity. Close to 1: the sentences are very similar. Lower: they have nothing in common.&lt;/p&gt;

&lt;p&gt;My detector compares each message to the available tools. "Create a ticket", "send a document". If the message looks enough like a tool, it is deemed actionable.&lt;/p&gt;

&lt;p&gt;The whole question sits inside "enough". That is where things get hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fixed bar does not work
&lt;/h2&gt;

&lt;p&gt;First instinct: set a bar. Above 0.85 similarity with a tool, the message is actionable.&lt;/p&gt;

&lt;p&gt;It does not work. My model gives scores between 0.80 and 0.92, for everything. "Hello" and "create a ticket" get almost the same score.&lt;/p&gt;

&lt;p&gt;This model sees every sentence as somewhat similar. Its scores are packed into a tiny range. And a fixed bar separates nothing inside a tiny range. Raise the bar, and you lose real cases before you lose the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare against neutral phrases, not a bar
&lt;/h2&gt;

&lt;p&gt;The solution that worked: compare, instead of measuring in the absolute.&lt;/p&gt;

&lt;p&gt;I wrote a list of neutral phrases, with no action in them. "Hello", "thanks", "ok great". I call them anchors.&lt;/p&gt;

&lt;p&gt;The rule becomes simple. A message is actionable if it looks more like a tool than like the best anchor. If it mostly looks like "thanks", it is chit-chat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The decision rule
&lt;/span&gt;&lt;span class="n"&gt;tool_score&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closest_tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;neutral_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closest_anchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;actionable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;neutral_score&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail that matters: I split long messages on punctuation. A request drowned in politeness stands out better piece by piece. The flip side: no punctuation, no splitting. Voice dictation rarely adds any.&lt;/p&gt;

&lt;h2&gt;
  
  
  One false alarm out of four
&lt;/h2&gt;

&lt;p&gt;I evaluated on 300 generated messages. Generated, not real: the project had no users yet. Keep that in mind, reality will do worse.&lt;/p&gt;

&lt;p&gt;Two measures matter, and they are simple. First: out of 100 messages that deserve an action, how many the detector catches. Mine catches 92. That is called recall, and it is a good score.&lt;/p&gt;

&lt;p&gt;Second: out of 100 harmless messages, how many it leaves alone. Mine leaves 76. In other words, 24 harmless messages out of 100 trigger the badge for nothing. That is called specificity, and it is the number that hurts.&lt;/p&gt;

&lt;p&gt;A badge that is wrong one time out of four stops being believed. Too many false alarms kill the alarm. The first number shines in the demo. The second one is paid in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embeddings cannot say no
&lt;/h2&gt;

&lt;p&gt;The worst failure mode has a name: negation. "There is an outage" asks for an action. "The outage is fixed, thanks" asks for none.&lt;/p&gt;

&lt;p&gt;To the detector, these two sentences are almost identical. Same vocabulary, so almost the same numbers. The second one triggers the badge exactly like the first.&lt;/p&gt;

&lt;p&gt;This is not a botched setting. An embedding summarizes a sentence by its topic. And both sentences have the same topic: an outage. The "it's fixed" part weighs almost nothing in the numbers.&lt;/p&gt;

&lt;p&gt;You can check this yourself in two minutes. Take your positive sentences, append "it's fixed, thanks" to each one. Then look at the scores: they barely move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mock exam caught up with me
&lt;/h2&gt;

&lt;p&gt;At first I believed I could fix negation through tuning. Twelve more anchors, like "it's fixed" and "problem solved". And a higher decision bar.&lt;/p&gt;

&lt;p&gt;On my test set, beautiful. So I gave the detector a mock exam. In jargon: a holdout. A set of fresh messages, never used during tuning. New exam questions, not the past papers.&lt;/p&gt;

&lt;p&gt;The verdict. The "it's fixed" false alarms did disappear. But the detector dropped from catching 68 real cases out of 100 to just 48. My fix missed more than one real case out of two.&lt;/p&gt;

&lt;p&gt;That is called overfitting. My tuning had memorized my examples instead of learning the problem. Like a student who recites past papers and sinks on a fresh question.&lt;/p&gt;

&lt;p&gt;Worse: on the mock exam, no bar gives both a good catch rate and few false alarms. The knob I was turning has nothing left to give. The ceiling is structural.&lt;/p&gt;

&lt;h2&gt;
  
  
  All the precision belongs to the LLM
&lt;/h2&gt;

&lt;p&gt;The numbers forced the conclusion on me. This detector will never be precise. It can, however, be exhaustive.&lt;/p&gt;

&lt;p&gt;So I picked its role: a gate, not a judge. Bar at the lowest, to catch wide. On the mock exam: 88 real cases out of 100 caught, and 31 harmless messages out of 100 let through for nothing.&lt;/p&gt;

&lt;p&gt;That noise is accepted, and written into the design. Because stage 2 judges behind it: the LLM, called on click, can read "it's fixed". It filters the negation and picks the right tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The shipped config: a gate, not a judge&lt;/span&gt;
&lt;span class="na"&gt;model     &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multilingual-e5-small&lt;/span&gt;   &lt;span class="c1"&gt;# small, on CPU, zero training&lt;/span&gt;
&lt;span class="na"&gt;threshold &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lowest&lt;/span&gt;                  &lt;span class="c1"&gt;# catch wide, on purpose&lt;/span&gt;
&lt;span class="na"&gt;anchors   &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smalltalk + resolution&lt;/span&gt;  &lt;span class="c1"&gt;# "thanks", "it's fixed"...&lt;/span&gt;
&lt;span class="na"&gt;result    &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;88 real cases out of 100 · 31 &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="s"&gt; alarms out of &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last decision, the most counter-intuitive one: do not train the model. I have no real conversations. And training on generated messages means learning the generator's quirks. The mock exam had just shown me that trap. I will train once I have a few hundred real, anonymized messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before you ship an embedding detector
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Before a similarity score goes to production, run the list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Test on messages never seen before, not on the ones used for tuning&lt;/li&gt;
&lt;li&gt;[ ] Count the false alarms, not just the good catches&lt;/li&gt;
&lt;li&gt;[ ] Try negation phrases: "it's fixed", "no need anymore, thanks"&lt;/li&gt;
&lt;li&gt;[ ] Compare against neutral phrases rather than setting an absolute bar&lt;/li&gt;
&lt;li&gt;[ ] Give the embedding stage a gate's role: catch wide, let something else judge&lt;/li&gt;
&lt;li&gt;[ ] Know how many false alarms the next stage can absorb&lt;/li&gt;
&lt;li&gt;[ ] Never train on generated data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A similarity score does not understand "no". That is not a tuning flaw, it is the nature of the tool.&lt;/p&gt;

&lt;p&gt;So measure on never-seen data, publish your ugly numbers, and put each stage where it is good. The embedding catches. The LLM understands.&lt;/p&gt;

&lt;p&gt;Building intent detection or AI routing, and the numbers do not hold in production? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://huggingface.co/intfloat/multilingual-e5-small" rel="noopener noreferrer"&gt;multilingual-e5-small (Hugging Face)&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2402.05672" rel="noopener noreferrer"&gt;Wang et al., Multilingual E5 Text Embeddings: A Technical Report&lt;/a&gt; · &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;Put the LLM Last (the previous article)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>nlp</category>
      <category>llm</category>
    </item>
    <item>
      <title>From Hype to Hard Reality: What We're Learning About Shipping AI Agents in Production</title>
      <dc:creator>Tamiz Uddin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:01:52 +0000</pubDate>
      <link>https://dev.to/tamizuddin/from-hype-to-hard-reality-what-were-learning-about-shipping-ai-agents-in-production-42il</link>
      <guid>https://dev.to/tamizuddin/from-hype-to-hard-reality-what-were-learning-about-shipping-ai-agents-in-production-42il</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tamiz.pro/insights/shipping-ai-agents-production-lessons" rel="noopener noreferrer"&gt;tamiz.pro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the better part of two years, the narrative has been seductive: build a few function calls around a reasoning model, add a tool-use loop, and you have an autonomous agent that solves hard problems. The demos are real. The benchmarks are impressive. And then you try to ship one to production — and the floor opens up beneath you.&lt;/p&gt;

&lt;p&gt;I've spent the last eighteen months building, breaking, and rebuidling AI agent systems across multiple teams and product lines. What follows isn't a critique of the technology. It's a map of the gap between what was promised and what actually works at scale, written for engineers who are currently standing in that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evaluation Illusion
&lt;/h2&gt;

&lt;p&gt;The first hard lesson is that every benchmark you see published is measuring something fundamentally different from what your production system does.&lt;/p&gt;

&lt;p&gt;SWE-Bench, GAIA, AgentBench — these are controlled evaluations on curated datasets with clean boundaries. Production is not curated. Your user says "fix the billing API" and means "the staging environment," not the one they've been working in all morning. The LLM doesn't know this. It guesses. It acts. It breaks things.&lt;/p&gt;

&lt;p&gt;The industry's early response was to build better evaluators: rubric-based scoring, LLM-as-judge, multi-hop verification pipelines. These help, but they create a second-order problem — you're now optimizing your agent for how well it scores on your evaluator, not for how well it serves the user.&lt;/p&gt;

&lt;p&gt;What actually moved the needle was simpler than most papers suggest. &lt;strong&gt;Constrain the action space.&lt;/strong&gt; Agents that operate on well-defined schemas, with typed inputs and bounded toolsets, evaluate far more reliably than open-ended ones. Think of it like type-safe programming: the compiler catches mistakes early, and the runtime is predictable. Your agent's tool interface is its API contract. Treat it like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability Isn't Optional — It's the System
&lt;/h2&gt;

&lt;p&gt;Most teams built their first AI agents without observability, assuming they could add it later. This is the "we'll put in logging later" of the AI era, and it hits just as hard.&lt;/p&gt;

&lt;p&gt;An AI agent in production is not a single request. It's a multi-step execution trace involving planning, tool calls, result parsing, error recovery, and often re-planning. When it fails — and it will — debugging a single &lt;code&gt;llm.invoke()&lt;/code&gt; call tells you nothing. You need to see the full trajectory: what the model planned, which tools it called, what it observed, and why it chose the next step.&lt;/p&gt;

&lt;p&gt;The practical standard that emerged: every agent execution needs a structured trace log with&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unique session ID&lt;/li&gt;
&lt;li&gt;a step-by-step event log (plan, tool_call, tool_result, observation)&lt;/li&gt;
&lt;li&gt;latency and token counts per step&lt;/li&gt;
&lt;li&gt;a confidence or uncertainty signal if your model provides one&lt;/li&gt;
&lt;li&gt;a final outcome classification (success, partial, failure, timeout, user_abort)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenTelemetry is the right foundation here. It gives you distributed tracing, context propagation, and a ecosystem of exporters. The mistake most teams make is treating traces as an afterthought added via SDK hooks. Build observability into the agent framework itself — the trace generation should be a first-class concern, not a cross-cutting middleware layer you bolt on afterward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tamiz.pro" rel="noopener noreferrer"&gt;See more on production AI patterns&lt;/a&gt; and &lt;a href="https://tamiz.pro/insights" rel="noopener noreferrer"&gt;Tamiz's Insights&lt;/a&gt; for deeper coverage on related engineering practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Curve Is Not Linear
&lt;/h2&gt;

&lt;p&gt;This is the lesson that keeps engineering leads awake at 2 AM.&lt;/p&gt;

&lt;p&gt;A simple RAG pipeline costs pennies per query. An agent that loops through five to fifteen tool calls, re-plans, retries, and potentially spans multiple turns? That's dollars per session, not cents.&lt;/p&gt;

&lt;p&gt;The cost isn't just the LLM calls. It's the &lt;em&gt;thinking&lt;/em&gt; overhead that compound autoregressive architectures accumulate. Each additional step adds latency, each retry multiplies it, and the user experience degrades non-linearly while costs climb linearly-per-step.&lt;/p&gt;

&lt;p&gt;The architectural response has converged on a few patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost-aware scheduling.&lt;/strong&gt; Route different agent sub-tasks to different model tiers. A cheap model handles routing and simple tool calls. An expensive model only runs when the task complexity threshold is crossed. This is the same principle as CDN caching for LLM requests: don't fly coast-to-coast for local delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget caps and early termination.&lt;/strong&gt; Set a maximum step count and token budget per execution. When you hit them, fall back to a safe default — usually escalating to a human or returning a structured error. Agents that run unbounded until they "succeed" are gambling with your margin and your SLA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching as infrastructure.&lt;/strong&gt; Conversation history, tool response bodies, and even plan fragments are often redundant across steps and sessions. Content-addressable caching of intermediate results — keyed by semantic fingerprint, not exact string match — can cut median cost by 30–50% in multi-step agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Human-in-the-Loop Problem (That Isn't Really About Humans)
&lt;/h2&gt;

&lt;p&gt;The original vision of AI agents was full autonomy. The production reality is that pure autonomy doesn't ship. Not for anything that touches money, data, or user state.&lt;/p&gt;

&lt;p&gt;But the interesting finding hasn't been that humans need to approve everything — it's that &lt;strong&gt;structured intervention points&lt;/strong&gt; improve outcomes more than raw oversight does.&lt;/p&gt;

&lt;p&gt;The pattern that works: define clear checkpoint semantics rather than vague "human review." A checkpoint should answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What decision is being made?&lt;/li&gt;
&lt;li&gt;What information is available to the human reviewer?&lt;/li&gt;
&lt;li&gt;What are the acceptable actions (approve, modify, abort, escalate)?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This turns subjective oversight into a deterministic state machine. Your agent pauses at a known point. The reviewer sees a structured summary — not the raw trace. They take one of three discrete actions. The agent resumes or terminates with clear semantics.&lt;/p&gt;

&lt;p&gt;The counter-intuitive part: well-designed checkpoints often reduce total human involvement compared to post-hoc review. You catch problems before they compound, and the structured context means reviewers spend less time understanding the situation and more time making the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hallucination in Agents Is Structural, Not Cosmetic
&lt;/h2&gt;

&lt;p&gt;In a simple QA system, hallucination looks like a wrong answer. In an agent, it looks like a cascade: the model hallucinates a tool schema, calls a non-existent endpoint, parses a malformed response as valid, and proceeds to act on fabricated data.&lt;/p&gt;

&lt;p&gt;This isn't a prompt engineering problem. It's an architectural one.&lt;/p&gt;

&lt;p&gt;The defenses that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema-enforced tool calling.&lt;/strong&gt; Never let the model construct tool invocation arguments from free text. Use JSON Schema validation on every tool call before execution. Reject calls that don't conform. This alone eliminates the most dangerous class of agent hallucinations — fabricated parameters that look plausible but are structurally invalid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observable side effects.&lt;/strong&gt; Every tool that mutates state (writes a database, sends an email, modifies a file) should have a dry-run or preview mode. The agent proposes the action, the system applies it conditionally, and the result is logged before any user-facing impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic guards around non-deterministic models.&lt;/strong&gt; Separate the planning layer (LLM-driven, creative) from the execution layer (deterministic, rule-based). The LLM decides &lt;em&gt;what to do&lt;/em&gt;. The execution layer verifies &lt;em&gt;that it can be done safely&lt;/em&gt;. This boundary is harder to draw in practice than in theory — it requires careful interface design — but it's the single most impactful architectural decision for production reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Integration Tax Is Real and Underestimated
&lt;/h2&gt;

&lt;p&gt;AI agents don't operate in a vacuum. They integrate with databases, APIs, message queues, authentication systems, and existing business logic. Every integration point is a potential failure mode, and the agent's multi-step nature amplifies each one.&lt;/p&gt;

&lt;p&gt;The pattern we've found most useful is the &lt;strong&gt;contract-first integration approach&lt;/strong&gt;: define the interface contract for every external system the agent touches, validate inputs against that contract before the agent ever sees them, and log mismatches as structured errors rather than letting the model guess at interpretations.&lt;/p&gt;

&lt;p&gt;This means spending as much engineering effort on your integration layer as on the agent itself. The agent is the visible part. The integration layer is what determines whether it survives in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works in Production Today
&lt;/h2&gt;

&lt;p&gt;If I were advising a team building an AI agent for production right now, this is the architecture I'd recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A deterministic orchestrator&lt;/strong&gt; (LangGraph, temporary state machines, or a custom implementation) that manages the execution graph, not a flat loop with a reasoning model at the center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A two-tier model strategy&lt;/strong&gt;: a cheap, fast model for routing and simple decisions; an expensive model only for complex reasoning sub-tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full OpenTelemetry tracing&lt;/strong&gt; with structured event logs, shipped to both a time-series store (for metrics) and a document store (for trace replay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Schema-validated tool interfaces&lt;/strong&gt; with no free-text argument construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget caps and timeout gates&lt;/strong&gt; at every level — per-step, per-session, and per-user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint semantics&lt;/strong&gt; for any action that crosses a state boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback path&lt;/strong&gt; that degrades gracefully to human review or simple responses when the agent exceeds its confidence thresholds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents that shipped successfully weren't the most autonomous or the most clever. They were the most &lt;em&gt;constrained&lt;/em&gt;. They had narrow scopes, well-defined failure modes, and architectures that treated uncertainty as a first-class input rather than something to paper over with better prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future Isn't Bigger Agents — It's Better Boundaries
&lt;/h2&gt;

&lt;p&gt;The hype cycle around AI agents is heading toward a correction. Not because agents don't work, but because the original vision — fully autonomous, open-ended, general-purpose digital workers — is architecturally incoherent at production scale. Uncertainty compounds. Failure modes multiply. Costs explode.&lt;/p&gt;

&lt;p&gt;The correction is already visible. Teams that are shipping are building narrower, more constrained systems with deterministic guardrails, proper observability, and honest assessments of where human judgment is required versus where the model can operate safely.&lt;/p&gt;

&lt;p&gt;The engineers who will lead this space forward aren't the ones building the most capable agents. They're the ones building the most &lt;em&gt;reliable&lt;/em&gt; ones. That distinction matters more than any benchmark score, and it's the one that will define the difference between a production system and a demo that never left the sandbox.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machine</category>
      <category>learning</category>
      <category>hype</category>
    </item>
    <item>
      <title>What I Check When I Inherit a WordPress Site I Didn't Build</title>
      <dc:creator>Noshi</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:00:20 +0000</pubDate>
      <link>https://dev.to/nokazehayato/what-i-check-when-i-inherit-a-wordpress-site-i-didnt-build-2lp4</link>
      <guid>https://dev.to/nokazehayato/what-i-check-when-i-inherit-a-wordpress-site-i-didnt-build-2lp4</guid>
      <description>&lt;p&gt;Sometimes the site was built by another developer. Sometimes by an agency. Parts of it may also have been generated or changed with AI. But the handoff problem itself is older than AI.&lt;/p&gt;

&lt;p&gt;However it was built, you're the one touching it now. You don't know what's actually configured under the hood. Not yet.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Record the current state before you change anything
&lt;/h2&gt;

&lt;p&gt;Before you fix, add, or "clean up" anything, write down what you found. No one's checking your work here. But later, if something breaks, it helps to know whether it was already broken when you got there.&lt;/p&gt;

&lt;p&gt;A quick note is enough: which plugins are active, whether the site is set to be indexed, whether debug mode is on. Later, that note gives you something to compare against when a problem shows up.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Look at the installed plugins
&lt;/h2&gt;

&lt;p&gt;Open the plugins screen and look past the ones that are active. Look at all of them, active and inactive.&lt;/p&gt;

&lt;p&gt;For each one, ask three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is this plugin still maintained?&lt;/strong&gt; Check its last update date on WordPress.org. A plugin that hasn't shipped an update in a year or more is one worth reviewing before you rely on it, even if you didn't choose to install it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this one inactive on purpose, or was it just never cleaned up?&lt;/strong&gt; An inactive plugin sitting around for years may simply be a leftover, or someone may have kept it there on purpose. Either way, it's often worth asking about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should I keep touching a site that depends on this?&lt;/strong&gt; Sometimes the answer is yes, and that's fine. The point is to ask that question before you start relying on the plugin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used to do this by opening each plugin's WordPress.org page one by one, which is tedious enough that I usually skipped the ones that "looked fine." Eventually, I automated that part of the process. I'll come back to that at the end.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check the current state, not the assumed state
&lt;/h2&gt;

&lt;p&gt;You assume noindex is off because "why would anyone launch a site that's hidden from Google." You assume debug mode is off because "it's a live site." Those assumptions are often right. But inherited sites are exactly where I don't want to rely on them.&lt;/p&gt;

&lt;p&gt;Check whether the site is set to be indexed by search engines, and don't rely only on the visible setting. A theme or plugin can force noindex through code even when the visible setting looks fine. Check whether &lt;code&gt;WP_DEBUG&lt;/code&gt; is on. Check whether the default &lt;code&gt;admin&lt;/code&gt; username still exists. These aren't complicated checks, but they're easy to miss when you're focused on the actual task you were hired to do.&lt;/p&gt;

&lt;p&gt;Ask yourself, for each setting: was this intentional, or is this just what came with the site? If you can't answer that, you can't safely assume it's fine.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make your changes
&lt;/h2&gt;

&lt;p&gt;Then you do the actual work: build the feature, fix the bug, make the requested changes. But now you're doing it against a baseline you actually understand, instead of a baseline you inherited blind.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Go through the site again before you hand it back
&lt;/h2&gt;

&lt;p&gt;Once your work is done, circle back to the same questions from step 3. Indexing, debug mode, admin username, and anything you added along the way: test posts, temporary pages, anything meant only for your own reference during the work.&lt;/p&gt;

&lt;p&gt;This is the same discipline you'd use before handing off a site you built yourself. The fact that you inherited the site doesn't change what you should leave behind when you're done. If anything, it's harder, because you're dealing with things you created during the job and things that were already there.&lt;/p&gt;

&lt;p&gt;　&lt;/p&gt;

&lt;h2&gt;
  
  
  Noshi-Kanamer
&lt;/h2&gt;

&lt;p&gt;This workflow is also why I added Site Check to Noshi-Kanamer. It scans every installed plugin, active or inactive, against WordPress.org and flags the ones that haven't been updated in 6 months or a year, so I don't have to open each plugin's WordPress.org page one by one. Then, before I hand the site back, I use Pre-Launch to check indexing, WP_DEBUG, debug.log, the default admin username, and a few other loose ends. It generates a plain-text report I can keep for myself or hand to whoever I'm delivering the work to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/noshi-kanamer/" rel="noopener noreferrer"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8w444bbcqb82rzfu0n3f.png" alt="Noshi-Kanamer: Pre-launch checklist &amp;amp; client handoff toolkit for WordPress" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's free on WordPress.org. If there's something you always check on an inherited site that I'm missing, I'd like to hear it.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>freelancing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I hosted my no-Mac iOS pipeline as a browser wizard (and dropped the price to $1 for a day)</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:00:07 +0000</pubDate>
      <link>https://dev.to/maclessdev/i-hosted-my-no-mac-ios-pipeline-as-a-browser-wizard-and-dropped-the-price-to-1-for-a-day-512e</link>
      <guid>https://dev.to/maclessdev/i-hosted-my-no-mac-ios-pipeline-as-a-browser-wizard-and-dropped-the-price-to-1-for-a-day-512e</guid>
      <description>&lt;p&gt;A while back I wrote about the GitHub Actions pipeline I built to ship Citolex to the App Store without ever owning a Mac. The pipeline itself hasn't changed much, but how you set it up has.&lt;/p&gt;

&lt;p&gt;Until now, using it meant downloading a template and running a setup script yourself. That's fine for most developers, but it's still a terminal and a handful of manual steps between paying and having a working pipeline.&lt;/p&gt;

&lt;p&gt;I built a hosted version instead. You pay, connect your GitHub account through a real OAuth flow, and pick or create a repo. A wizard commits the full pipeline template directly into it using GitHub's Git Data API, no git required on your end. Then it walks you through filling in your signing secrets, with an inline Signing Doctor check that tells you exactly what's wrong with a certificate or provisioning profile before you ever trigger a build, not after one fails. If a build does fail, the same wizard reruns that diagnosis automatically against your current secrets and shows it right next to the failed logs.&lt;/p&gt;

&lt;p&gt;The signing diagnosis itself doesn't shell out to any macOS specific tooling. Provisioning profiles are signed CMS/PKCS7 blobs, certificates are X.509, keystores are PKCS12, all standard crypto formats that Web Crypto can parse directly. That's what let this run as a Cloudflare Worker instead of needing an actual Mac or VM behind it.&lt;/p&gt;

&lt;p&gt;If you've been meaning to try it, it's $1 for the next 24 hours instead of the usual $99 for iOS or $39 for Android, mostly to get more people actually running it end to end rather than reading about it.&lt;/p&gt;

&lt;p&gt;Full disclosure, I build Macless. macless.dev if you want to poke at it.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>githubactions</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Google Gemini 3.5 and Gemini 3.5 Flash: The Complete Guide</title>
      <dc:creator>Mecanik1337</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/mecanik-dev/google-gemini-35-and-gemini-35-flash-the-complete-guide-59he</link>
      <guid>https://dev.to/mecanik-dev/google-gemini-35-and-gemini-35-flash-the-complete-guide-59he</guid>
      <description>&lt;p&gt;&lt;strong&gt;Gemini 3.5&lt;/strong&gt; represents Google's latest efforts to dominate the fast-growing market for agentic AI applications in 2026. Developers need models that are both fast and cost-effective to run complex reasoning tasks. Consequently, the introduction of these models addresses this need directly by combining high speed with frontier intelligence.&lt;/p&gt;

&lt;p&gt;This article reviews the core architecture of Gemini 3.5, the specific capabilities of Gemini 3.5 Flash, and how engineering teams use them to build autonomous coding pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google released Gemini 3.5 Flash in mid-May 2026 to target high-speed agentic development.&lt;/li&gt;
&lt;li&gt;The model natively supports text, images, video, audio, and PDF documents within a single context.&lt;/li&gt;
&lt;li&gt;It features a one million token input limit to allow deep codebase processing at low cost.&lt;/li&gt;
&lt;li&gt;Google designed Gemini 3.5 specifically for long-horizon workflows like autonomous coding cycles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Gemini 3.5?
&lt;/h2&gt;

&lt;p&gt;The Gemini 3.5 model series represents Google's core intelligence layer for 2026. For instance, while older models focused on text-based generation, this new generation is multimodal by default. Specifically, the engine processes multiple data formats simultaneously. As a result, it translates, reasons, and builds code across text, audio, video, and PDF structures without external conversion tools.&lt;/p&gt;

&lt;p&gt;Specifically, Google built the model to serve as a reliable platform for autonomous agents. For example, these agents need to interact with external tools and make decisions over long periods. Consequently, the API offers low latency and high reliability for tool-calling operations.&lt;/p&gt;

&lt;p&gt;Furthermore, the model also maintains a high level of code correctness. Specifically, it handles complex system integration tasks easily. Consequently, this makes it a strong choice for businesses that want to automate their software delivery lifecycles. For a step-by-step approach on implementing such automation, check out our guide on &lt;a href="https://mecanik.dev/en/posts/ai-software-development-a-uk-business-guide-for-2026/" rel="noopener noreferrer"&gt;AI software development&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini 3.5 Flash Architecture and Speed
&lt;/h2&gt;

&lt;p&gt;The standout release of this series is Gemini 3.5 Flash. Launched in mid-May 2026, the Flash variant targets speed and cost-efficiency. Therefore, it provides developers with a powerful tool for tasks that require quick responses.&lt;/p&gt;

&lt;p&gt;Therefore, despite its smaller size, Gemini 3.5 Flash handles a one million token input window. This allows developers to upload entire project codebases or hours of video directly into the prompt. The model processes this information quickly, making it ideal for real-time applications.&lt;/p&gt;

&lt;p&gt;In addition, Google also reduced the pricing for the Flash model. This cost reduction allows startups and SMEs to run high-volume agentic tasks without exceeding their budgets. It represents a major step toward making agentic programming accessible to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases for Gemini 3.5 in Development
&lt;/h2&gt;

&lt;p&gt;Specifically, developers use Gemini 3.5 for a variety of tasks that require both speed and multimodal understanding.&lt;/p&gt;

&lt;p&gt;Indeed, one major usecase involves automated code reviews and refactoring. Because the model supports a large context window, it can review multiple files at once. It checks for security vulnerabilities and suggests improvements based on project style guides. You can find more specifications on Google's technical milestones on the &lt;a href="https://deepmind.google/technologies/gemini/" rel="noopener noreferrer"&gt;Google DeepMind Gemini site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Similarly, another popular use case is video and audio analysis. Developers use the model to extract data from webinars, meetings, and tutorials. It can summarize key points, create transcripts, and even generate code snippets based on visual demonstrations in the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing API Performance: Context Caching
&lt;/h2&gt;

&lt;p&gt;When working with large codebases, API costs can accumulate quickly. Consequently, Google introduced context caching for the Gemini 3.5 series. This feature allows developers to store frequently used files in Google's cache, reducing the number of active tokens processed during each API call.&lt;/p&gt;

&lt;p&gt;Specifically, if you have a library that changes rarely, you can cache it once. The API then references the cached version for subsequent queries. This reduces latency significantly and cuts running costs by up to 50%.&lt;/p&gt;

&lt;p&gt;Consequently, developers can run continuous integration scripts without exceeding their budgets. These scripts can check every commit on &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for logical errors, ensuring that the main codebase remains clean and functional at all times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Google AI Studio: Getting Started
&lt;/h2&gt;

&lt;p&gt;For developers who want to experiment with these features immediately, Google provides a browser-based playground. This tool, known as &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;, allows you to write prompts, adjust parameters, and test API endpoints without setting up a local server.&lt;/p&gt;

&lt;p&gt;To get started, you can sign in with your developer account and generate an API key. The console provides a clean interface to test text, image, and video prompts. It also offers auto-generated code blocks in Python, JavaScript, and Curl to make integration faster, especially when setting up &lt;a href="https://mecanik.dev/en/posts/claude-ai-for-code-review-a-developers-guide-for-2026/" rel="noopener noreferrer"&gt;Claude AI for code review&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In addition, AI Studio allows you to test system instructions and safety filters directly. This helps you understand how the model behaves under different settings, making it easier to build secure applications for production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gemini 3.5 is a native multimodal model series targeting fast agentic AI applications.&lt;/li&gt;
&lt;li&gt;Gemini 3.5 Flash offers low latency and cost-effective processing for high-volume tasks.&lt;/li&gt;
&lt;li&gt;The model features a one million token context window to handle large datasets.&lt;/li&gt;
&lt;li&gt;Developers use it for codebase analysis, automated code reviews, and video processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; &lt;a href="https://mecanik.dev/en/posts/chatgpt-vs-gemini-vs-claude-which-is-best-in-2026/" rel="noopener noreferrer"&gt;ChatGPT vs Gemini vs Claude: Which AI is Best in 2026?&lt;/a&gt;, &lt;a href="https://mecanik.dev/en/posts/deepseek-r1-vs-openai-o3-mini-api/" rel="noopener noreferrer"&gt;DeepSeek R1 vs. OpenAI o3-mini: Which API is Best?&lt;/a&gt;, &lt;a href="https://mecanik.dev/en/posts/schema-markup-for-llms-ai-search/" rel="noopener noreferrer"&gt;How AI Search Engines Read Schema Markup and Structured Data&lt;/a&gt; and &lt;a href="https://mecanik.dev/en/posts/google-ai-overviews-seo-optimization/" rel="noopener noreferrer"&gt;Optimizing for Google AI Overviews: A 2026 SEO Guide&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is Gemini 3.5?&lt;/strong&gt;&lt;br&gt;
Gemini 3.5 is Google's latest generation of multimodal AI models. It natively processes text, images, audio, video, and PDF files. Google designed the model for autonomous agentic workflows and complex programming tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When did Google release Gemini 3.5 Flash?&lt;/strong&gt;&lt;br&gt;
Google released Gemini 3.5 Flash in mid-May 2026. Google designed the model to provide developers with a fast, cost-effective alternative for high-volume reasoning tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the context window size of Gemini 3.5 Flash?&lt;/strong&gt;&lt;br&gt;
The model supports an input context window of one million tokens. This capacity allows developers to process large repositories and document sets in a single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Gemini 3.5 handle coding tasks?&lt;/strong&gt;&lt;br&gt;
Google designed Gemini 3.5 to execute long-horizon coding cycles. It can analyze full project directories, perform automated code reviews, and suggest refactoring steps with high accuracy.&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>google</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>SSE Fragmented My JSON Into 30 Pieces. Here's the Parser That Put It Back Together.</title>
      <dc:creator>Taylor Wang</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:59:16 +0000</pubDate>
      <link>https://dev.to/codepy_1473/sse-fragmented-my-json-into-30-pieces-heres-the-parser-that-put-it-back-together-37ng</link>
      <guid>https://dev.to/codepy_1473/sse-fragmented-my-json-into-30-pieces-heres-the-parser-that-put-it-back-together-37ng</guid>
      <description>&lt;p&gt;I was building a streaming chat interface against MonkeyCode's free model access and its free server option, and the first few test runs went so smoothly that I stopped thinking about the transport layer entirely. The server sent back clean SSE events, each one a complete JSON object carrying a token of text, and my parser happily decoded them one by one. Then I asked for a longer response, and the terminal lit up with &lt;code&gt;json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)&lt;/code&gt; on the second event.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The full response, captured to a file, was valid JSON. The model had not failed. My parser had failed because it assumed something the SSE protocol never guarantees: that each event contains a complete, self-contained JSON document.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Stream Actually Looked Like
&lt;/h2&gt;

&lt;p&gt;Server-Sent Events is a simple framing protocol. Each event is a set of &lt;code&gt;key: value&lt;/code&gt; lines terminated by a blank line, and the &lt;code&gt;data:&lt;/code&gt; field carries the payload. When a model streams tokens, the server does not promise to flush one complete JSON object per event. It flushes whatever bytes are ready, which means a single logical object can be split across several events, or several objects can be packed into one event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data: {"choices":[{"delta":{"content":"Hel

data: lo"}}]}

data: {"choices":[{"delta":{"content":" world"}}]}

data: [DONE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My naive parser treated each &lt;code&gt;data:&lt;/code&gt; line as an independent JSON document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong: assumes every event is a complete JSON object
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sse_events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# crashes on the first fragment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first event in my trace contained &lt;code&gt;{"choices":[{"delta":{"content":"Hel&lt;/code&gt; — a fragment with no closing brace. The second event contained &lt;code&gt;lo"}}]}&lt;/code&gt; — a fragment with no opening brace. Both were meaningless on their own and perfectly meaningful together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Buffer, Then Decode Incrementally
&lt;/h2&gt;

&lt;p&gt;The robust approach is to accumulate raw bytes in a buffer and attempt a decode only when the buffer might contain a complete object. Python's &lt;code&gt;json.JSONDecoder.raw_decode&lt;/code&gt; is the right tool: it parses one JSON value from the start of a string and tells you where it ends, without requiring the whole string to be valid JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IncrementalJSONParser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
        &lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# incomplete fragment; wait for more bytes
&lt;/span&gt;            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\t\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop tries to decode at the current position. If it succeeds, it yields the object and advances past it, then tries again in case multiple objects arrived in the same chunk. If it fails, it stops and waits for more data. The leftover bytes stay in the buffer for the next call.&lt;/p&gt;

&lt;p&gt;Using it with an SSE client is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IncrementalJSONParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sse_events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;handle_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser keeps its own buffer across calls, so split fragments reassemble naturally and concatenated objects decode one after another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling a Mid-Stream Disconnect
&lt;/h2&gt;

&lt;p&gt;The incremental parser fixes the fragmentation problem, but free servers can also drop the connection mid-stream. At that point you have three choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discard the partial response&lt;/strong&gt; and retry the whole request. Simple, but wasteful if the model was 90% done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the partial text&lt;/strong&gt; if your use case tolerates truncation. A summary that ends mid-sentence is often better than no summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry with a resume hint&lt;/strong&gt; — send the partial text back to the model and ask it to continue. This is fragile and model-dependent; I would not rely on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For my batch summarizer, I chose option 1 with a retry cap of three attempts, because summaries are short and a truncated summary is worse than a delayed one. For a chat interface, option 2 is more humane: show what arrived, then let the user ask for more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The incremental parser handles JSON fragments and concatenated objects, but it has real limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It assumes the stream is UTF-8 text. Binary-safe protocols need a different approach.&lt;/li&gt;
&lt;li&gt;It buffers everything in memory. For a multi-megabyte response, that defeats the purpose of streaming.&lt;/li&gt;
&lt;li&gt;It does not validate that the objects form a meaningful sequence. You still need to check for &lt;code&gt;[DONE]&lt;/code&gt; sentinels or other end-of-stream markers and filter them out before decoding.&lt;/li&gt;
&lt;li&gt;If the provider wraps JSON in extra framing, like &lt;code&gt;data: [DONE]&lt;/code&gt; at the end, you need to handle that separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Should Not Use This
&lt;/h2&gt;

&lt;p&gt;If your provider guarantees one complete JSON object per SSE event, the naive parser is fine and this whole article is overkill. If you are not actually streaming — if you just want the final response — call the non-streaming endpoint and &lt;code&gt;json.loads&lt;/code&gt; the whole thing. Incremental parsing is a tool for the specific case where the stream is fragmented, the response is large, or you want to render tokens as they arrive.&lt;/p&gt;

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

&lt;p&gt;Streaming protocols and JSON documents have different ideas about where a message ends. SSE events are transport boundaries, not data boundaries, and confusing the two produces bugs that look like model failures but are actually client failures. Buffer, decode incrementally, and decide in advance what a mid-stream disconnect means for your use case.&lt;/p&gt;

&lt;p&gt;If you are experimenting with free model access and a free server, expect the stream to be less polished than a paid tier: more fragmentation, more disconnects, more surprises. Build your parser to survive those surprises, and the model's output quality becomes the only thing you have to judge.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>json</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I built a YouTube-to-text tool, and three things turned out much harder than expected</title>
      <dc:creator>Zhengxin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:59:06 +0000</pubDate>
      <link>https://dev.to/_94be737e156beb4d74df2/i-built-a-youtube-to-text-tool-and-three-things-turned-out-much-harder-than-expected-457o</link>
      <guid>https://dev.to/_94be737e156beb4d74df2/i-built-a-youtube-to-text-tool-and-three-things-turned-out-much-harder-than-expected-457o</guid>
      <description>&lt;p&gt;Someone links a 45-minute conference talk and says "the good part is in the middle somewhere." You want three sentences. You do not want 45 minutes.&lt;/p&gt;

&lt;p&gt;So I built SummarizeVideoToText: paste a video link, get a text workspace — full transcript, AI summary, timestamped chapters, a mind map, and a Q&amp;amp;A panel you can interrogate about the video. No sign-up needed to try it.&lt;/p&gt;

&lt;p&gt;That's the pitch. The interesting part is what broke along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Getting captions is a fallback chain, not an API call
&lt;/h2&gt;

&lt;p&gt;My first version called one endpoint and assumed a transcript came back. In practice, that endpoint fails constantly — YouTube rotates its internals, some videos need a proof-of-origin token, some tracks exist but not in the language you asked for.&lt;/p&gt;

&lt;p&gt;What actually works is a chain of providers where each layer falls back to the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChainProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;TranscriptProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranscriptProvider&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="c1"&gt;// try each in turn; fall through on failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-obvious part is knowing when &lt;strong&gt;not&lt;/strong&gt; to fall through. Two cases end the chain immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;invalid_url&lt;/code&gt; — the link itself is broken. No provider will do better.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no_transcript&lt;/code&gt; — layer one confirmed the page loads fine and has no caption track at all. A paid provider will confirm the same thing and bill you for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else falls through. That distinction is the difference between a robust chain and a machine that burns API credits to rediscover the same "nope."&lt;/p&gt;

&lt;p&gt;The honest limitation this leaves: &lt;strong&gt;if a video has no captions in any form, there's nothing to summarize.&lt;/strong&gt; I show that plainly instead of pretending. Audio transcription for YouTube is on the roadmap; TikTok and Instagram already go through AI transcription because they rarely ship captions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Caching wasn't about speed. It was about money.
&lt;/h2&gt;

&lt;p&gt;I started with Redis and a TTL, like you do. Then I watched the logs: a video would get summarized, sit for a week, the key would expire, someone would open the same URL — and the whole pipeline would run again. New caption fetch, new LLM call, new bill.&lt;/p&gt;

&lt;p&gt;The realization: &lt;strong&gt;a video's content never changes.&lt;/strong&gt; There is no correctness reason to ever evict a summary. TTL made sense for a hot cache, not for the artifact itself.&lt;/p&gt;

&lt;p&gt;So it became two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; — hot cache, short TTL, absorbs the repeat traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — permanent store, no TTL. Redis misses land here, not on the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Storing a few KB of text forever costs orders of magnitude less than regenerating it once. If your pipeline has an expensive deterministic step, "cache expiry" and "delete the result" should not be the same decision.&lt;/p&gt;

&lt;p&gt;The user-visible payoff is that opening a video someone else already summarized is instant and costs nobody anything — which is also why I could leave the free tier usable without an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Notion doesn't take Markdown
&lt;/h2&gt;

&lt;p&gt;I wanted "export this whole note to Notion." I assumed I'd POST some Markdown. Notion's API is a &lt;strong&gt;block model&lt;/strong&gt; — every heading, paragraph, and list item is a typed object, and the constraints stack up fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max &lt;strong&gt;100 child blocks&lt;/strong&gt; per request. A transcript is hundreds of lines.&lt;/li&gt;
&lt;li&gt;Max &lt;strong&gt;2000 characters&lt;/strong&gt; per rich-text object. Long paragraphs need chunking.&lt;/li&gt;
&lt;li&gt;Max &lt;strong&gt;2 levels of nesting&lt;/strong&gt; per request. So a collapsible toggle containing a full transcript can't be created in one shot: you create the toggle, read its ID out of the response, then append its children in batches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the one that cost me an evening of confusion: &lt;strong&gt;an integration without "read content" permission gets partial objects back.&lt;/strong&gt; Creating a page returns an object with an &lt;code&gt;id&lt;/code&gt; and no &lt;code&gt;url&lt;/code&gt;. Searching returns pages with no &lt;code&gt;properties&lt;/code&gt;, so no title. Nothing errors. You just get &lt;code&gt;undefined&lt;/code&gt; where you expected a link, and a page titled &lt;code&gt;""&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two lessons. First, when an API returns a suspiciously empty field, check the permission scope before you check your code. Second, degrade instead of inventing: my first fix put the string &lt;code&gt;"(Untitled page)"&lt;/code&gt; in the UI, which turned a missing title into a confidently wrong one. The real fix was to reconstruct the URL from the ID (&lt;code&gt;notion.so/&amp;lt;id-without-dashes&amp;gt;&lt;/code&gt; is a valid link) and drop the page name from the message when it isn't known.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: the Obsidian URL that was always too long
&lt;/h2&gt;

&lt;p&gt;Obsidian has a URI scheme: &lt;code&gt;obsidian://new?name=...&amp;amp;content=...&lt;/code&gt;. Clean, one click, works great in the demo.&lt;/p&gt;

&lt;p&gt;It never worked in production. A full transcript blows past the URI length limit every single time, so my code silently fell back to downloading a &lt;code&gt;.md&lt;/code&gt; file. Users clicked "Export to Obsidian" and got a file in their Downloads folder — technically not a failure, so nothing ever showed up in the error logs.&lt;/p&gt;

&lt;p&gt;The fix was one flag I'd missed in the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;copyText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`obsidian://new?name=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;clipboard=true`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;clipboard=true&lt;/code&gt; tells Obsidian to pull the body from the clipboard. The URI now carries only the title, and length stops being a factor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The analytics bug that made everything else unmeasurable
&lt;/h2&gt;

&lt;p&gt;One more, because it invalidated a week of numbers.&lt;/p&gt;

&lt;p&gt;My event helper was defensive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gtag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;gtag&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ← this line&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensible: ad blockers exist, analytics should never break a click. But the gtag stub was loading with &lt;code&gt;afterInteractive&lt;/code&gt;, meaning &lt;code&gt;window.gtag&lt;/code&gt; doesn't exist until hydration finishes. On a slow connection that's a multi-second window — and the "Summarize" button is the &lt;em&gt;first&lt;/em&gt; thing anyone clicks. Those events were dropped silently, so my funnel's denominator was quietly too small.&lt;/p&gt;

&lt;p&gt;The fix is to load the tiny stub &lt;code&gt;beforeInteractive&lt;/code&gt; (it only pushes to an array) and let the real script arrive later and replay the queue. That's what Google's own snippet does; I'd split it apart without thinking about ordering.&lt;/p&gt;

&lt;p&gt;Related lesson from the same audit: &lt;strong&gt;instrument outcomes, not intentions.&lt;/strong&gt; I was tracking "user clicked Export" but not whether the export succeeded. 100 clicks could be 97 successes or 3. Every click event that kicks off async work deserves a matching result event with a failure code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YouTube&lt;/strong&gt; via official captions; &lt;strong&gt;TikTok / Instagram&lt;/strong&gt; via AI transcription&lt;/li&gt;
&lt;li&gt;Summary, timestamped chapters, key insights, an interactive mind map, and Q&amp;amp;A grounded in the actual transcript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;25 summary templates&lt;/strong&gt; (study notes, meeting minutes, Twitter thread, flashcards, SEO article…) and &lt;strong&gt;14 output languages&lt;/strong&gt;, independent of the video's language&lt;/li&gt;
&lt;li&gt;Export the whole note to &lt;strong&gt;Notion&lt;/strong&gt;, &lt;strong&gt;Obsidian&lt;/strong&gt;, or &lt;strong&gt;Markdown&lt;/strong&gt;, with clickable timestamps that jump back into the video&lt;/li&gt;
&lt;li&gt;Free: 2 videos/day with no account (up to 15 min), 10/day with a free Google sign-in (up to 1 hour)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it: &lt;strong&gt;&lt;a href="https://summarizevideototext.com" rel="noopener noreferrer"&gt;summarizevideototext.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've fought with the Notion block API or the YouTube caption endpoints, I'd genuinely like to compare notes in the comments — especially if you found a cleaner answer than a fallback chain.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
