<?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: erdonline</title>
    <description>The latest articles on DEV Community by erdonline (@erdonline).</description>
    <link>https://dev.to/erdonline</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4099832%2Fe3f834bb-61d9-4770-a0c7-9b3599b1e64f.png</url>
      <title>DEV Community: erdonline</title>
      <link>https://dev.to/erdonline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erdonline"/>
    <language>en</language>
    <item>
      <title>Average position 1. Zero clicks. Eight URLs, one identity.</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sat, 29 Aug 2026 10:32:57 +0000</pubDate>
      <link>https://dev.to/erdonline/average-position-1-zero-clicks-eight-urls-one-identity-25f4</link>
      <guid>https://dev.to/erdonline/average-position-1-zero-clicks-eight-urls-one-identity-25f4</guid>
      <description>&lt;p&gt;Search Console told us we were ranking. In Singapore, average position 1. Across three months: 42 impressions, &lt;strong&gt;0 clicks&lt;/strong&gt;. Not a low click-through rate — none.&lt;/p&gt;

&lt;p&gt;Twelve days and one engineering pass later, the same property read 2 clicks / 103 impressions / 1.9% CTR / average position 62.7. Inside that, two rows that shouldn't be able to coexist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Clicks / impressions&lt;/th&gt;
&lt;th&gt;Average position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.erdonline.com/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 90&lt;/td&gt;
&lt;td&gt;61.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.erdonline.com/compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 8&lt;/td&gt;
&lt;td&gt;~2–3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One page buried on page six for the queries people actually type. Another sitting at position two for a query almost nobody types. The instinct in that moment is to write more content, because "more content" is the answer every SEO article gives. It was the wrong read, and chasing it would have cost us a month.&lt;/p&gt;

&lt;p&gt;The real problem was that our site had &lt;strong&gt;one identity&lt;/strong&gt;. It had eight marketing URLs, a sitemap, hreflang tags, and JSON-LD — and every single one of those URLs handed Google the same first byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis takes one command
&lt;/h2&gt;

&lt;p&gt;We build a client-rendered UmiJS app hosted on Cloudflare Pages. Before diagnosing anything clever, curl what the crawler gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://www.erdonline.com/catalog | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title&amp;gt;|rel="canonical"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer, for months, was the homepage's &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link rel="canonical" href="https://www.erdonline.com/"&amp;gt;&lt;/code&gt;. Same for &lt;code&gt;/compare&lt;/code&gt;. Same for &lt;code&gt;/en&lt;/code&gt;. Same for &lt;code&gt;/demo&lt;/code&gt;. And then this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.erdonline.com/__seo_health_nonexistent_path__
&lt;span class="c"&gt;# HTTP/2 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A URL that has never existed, returning 200 with the homepage in the body. That is a soft 404, and once you have one, you have infinitely many. Googlebot doesn't see a site with eight pages plus some noise. It sees one page reachable at an unbounded number of addresses, which is the exact shape of a low-quality site, and it responds by picking one URL to keep and quietly discarding the rest.&lt;/p&gt;

&lt;p&gt;Rank was never the bottleneck. Being a distinct page was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two platform defaults, both documented, both easy to miss
&lt;/h2&gt;

&lt;p&gt;Neither cause was exotic. Both were things we had written ourselves and then stopped looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catch-all rewrite.&lt;/strong&gt; &lt;code&gt;public/_redirects&lt;/code&gt; contained &lt;code&gt;/* /index.html 200&lt;/code&gt;. This is the line every SPA tutorial tells you to add so client-side routing survives a hard refresh, and it is correct for &lt;code&gt;/project/1234&lt;/code&gt;, a page behind a login that has no business being indexed. It is destructive for &lt;code&gt;/compare&lt;/code&gt;, a page whose entire job is to be indexed under its own title.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No top-level &lt;code&gt;404.html&lt;/code&gt;.&lt;/strong&gt; Cloudflare Pages has a documented rule here that catches people: if there is no &lt;code&gt;404.html&lt;/code&gt; at the root of your build output, Pages assumes you are a single-page app and serves &lt;code&gt;200 + index.html&lt;/code&gt; for every path that doesn't match a static file. Ship a &lt;code&gt;404.html&lt;/code&gt; and that automatic SPA mode turns off — unmatched paths get a real HTTP 404. One file, and infinite soft 404s stop.&lt;/p&gt;

&lt;p&gt;Three smaller footguns cost us a deploy each, and they are the kind of thing nobody writes down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A rewrite target of &lt;code&gt;/index.html&lt;/code&gt; doesn't work. Cloudflare 308-redirects &lt;code&gt;*.html&lt;/code&gt; to its extension-less form, so a rewrite meant to be invisible becomes a redirect to &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The splat in &lt;code&gt;/catalog/* → / 200&lt;/code&gt; matches &lt;code&gt;/catalog/&lt;/code&gt; with an empty segment, so the catch-all quietly shadowed the very list-page shell we had just generated. It has to be &lt;code&gt;/catalog/:id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We first sent unknown template IDs to a placeholder shell at &lt;code&gt;/catalog/_item&lt;/code&gt;. Because that path is a directory, Cloudflare 308'd &lt;code&gt;/catalog/some-bad-id&lt;/code&gt; onto &lt;code&gt;/catalog/_item/&lt;/code&gt; — we had invented a brand-new crawlable junk URL while trying to clean up crawlable junk URLs. The fix was to rewrite unknown IDs to &lt;code&gt;/catalog/&lt;/code&gt; (already slashed, so nothing to 308 onto) and 301 &lt;code&gt;_item&lt;/code&gt; away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a redirect rule that is &lt;em&gt;almost&lt;/em&gt; right. Which is the point: this class of bug produces a perfectly working website for humans and an unindexable one for crawlers, and no amount of reading your React code will surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four artifacts, one source of truth
&lt;/h2&gt;

&lt;p&gt;The fix was to prerender a per-path shell at build time — &lt;code&gt;dist/catalog/index.html&lt;/code&gt;, &lt;code&gt;dist/compare/index.html&lt;/code&gt;, &lt;code&gt;dist/en/compare/index.html&lt;/code&gt;, and so on — each carrying its own &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, canonical, hreflang set, and JSON-LD, then remove those exact paths from the redirect file so the static file wins.&lt;/p&gt;

&lt;p&gt;That's the obvious half. The half worth stealing is what we did to stop it from rotting.&lt;/p&gt;

&lt;p&gt;Adding one public route requires four artifacts to agree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the list of URLs in &lt;code&gt;sitemap.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the set of paths that get a prerendered shell&lt;/li&gt;
&lt;li&gt;the Cloudflare &lt;code&gt;_redirects&lt;/code&gt; rules for everything that doesn't&lt;/li&gt;
&lt;li&gt;the nginx &lt;code&gt;map&lt;/code&gt; for self-hosted Docker deployments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four files, maintained by hand, in a repo where the person adding a route is thinking about React Router and nothing else. They will drift. Ours did.&lt;/p&gt;

&lt;p&gt;So they all became derived values exported from one module, &lt;code&gt;frontend/scripts/seo-config.mjs&lt;/code&gt;: &lt;code&gt;SITEMAP_PATHS&lt;/code&gt;, &lt;code&gt;PRERENDER_PAGES&lt;/code&gt;, &lt;code&gt;CF_SPA_REDIRECT_RULES&lt;/code&gt;, &lt;code&gt;NGINX_SPA_URI_REGEXES&lt;/code&gt;, plus &lt;code&gt;marketingHreflang()&lt;/code&gt; and &lt;code&gt;jsonLdForPage()&lt;/code&gt;. The build script consumes it and writes &lt;code&gt;sitemap.xml&lt;/code&gt;, &lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;_redirects&lt;/code&gt;, &lt;code&gt;_headers&lt;/code&gt;, &lt;code&gt;404.html&lt;/code&gt;, and every shell in one pass. Adding a route is one entry in one array. Drift between the sitemap and the redirects is no longer a mistake you can make.&lt;/p&gt;

&lt;p&gt;That file is also where the hard-won knowledge lives, as comments next to the rules they explain — including the one that reads, in effect, &lt;em&gt;do not use `/catalog/&lt;/em&gt;` here, we tried, here is what Cloudflare does*. Comments that record a footgun earn their keep. Comments that narrate what the next line does do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs that only exist in single-page apps
&lt;/h2&gt;

&lt;p&gt;With the shells shipped, two failures showed up that have no equivalent on a server-rendered site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hydration fought the shell.&lt;/strong&gt; &lt;code&gt;/catalog/demo-authz&lt;/code&gt; prerendered correctly, then the client booted and &lt;code&gt;CatalogLayout&lt;/code&gt; called &lt;code&gt;usePageSeo(catalog.seo.*)&lt;/code&gt; unconditionally — so the detail page's title was replaced, a few hundred milliseconds later, by the &lt;em&gt;list&lt;/em&gt; page's title. The prerender was right and the app overwrote it. If you prerender per-path metadata in a CSR app, your client-side SEO hook needs to know which page it is on, or you have built two systems that disagree about &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;. Now the list applies list metadata, and a detail page waits for its API response and writes &lt;code&gt;{name} — ER diagram templates&lt;/code&gt; with a canonical pointing at &lt;code&gt;/catalog/:id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every page claimed to be the app.&lt;/strong&gt; Our JSON-LD generator was parameterizing &lt;code&gt;url&lt;/code&gt; and nothing else, so &lt;code&gt;@type&lt;/code&gt; stayed &lt;code&gt;WebApplication&lt;/code&gt; everywhere. Eight distinct pages each announcing themselves as the application living at the site root — structured data actively contradicting the canonical tags we had just fixed. It's now typed per path: &lt;code&gt;/&lt;/code&gt; is a &lt;code&gt;WebApplication&lt;/code&gt;, the template list is a &lt;code&gt;CollectionPage&lt;/code&gt;, an official template detail is an &lt;code&gt;ItemPage&lt;/code&gt;, everything else is a &lt;code&gt;WebPage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The title problem was a promise problem
&lt;/h2&gt;

&lt;p&gt;Position 1 with zero clicks is not a ranking failure. It is a snippet that doesn't sound like the thing the searcher wanted.&lt;/p&gt;

&lt;p&gt;Ours was brand-first, and the searcher's job-to-be-done was not "learn about a brand." It was "draw an ER diagram, in a browser, now." The queries said so plainly: &lt;code&gt;erd online&lt;/code&gt;, &lt;code&gt;erd diagram online&lt;/code&gt;, &lt;code&gt;make|create|draw erd online&lt;/code&gt;. So the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; became job-first — &lt;code&gt;Draw ER Diagram Online — Free Editor | ERD Online&lt;/code&gt; — and the description covers the intent cluster in one honest sentence: draw, ERD editor and maker, entity-relationship models.&lt;/p&gt;

&lt;p&gt;Three things we deliberately did &lt;strong&gt;not&lt;/strong&gt; do, now written into &lt;code&gt;docs/landing.md&lt;/code&gt; as rules rather than left to judgment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No "file viewer."&lt;/strong&gt; It's a plausible, high-volume phrase. We support ERD/PdMan/DBML import; we do not ship a dedicated file viewer. Ranking for a query you can't satisfy buys one visit and one bounce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No naming Google Draw.&lt;/strong&gt; Our comparison page names draw.io, because we have a real technical claim there — a line in draw.io is a line, while a relationship in ERD Online carries foreign-key semantics. We won't name a product we haven't actually compared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No stuffing the non-English queries&lt;/strong&gt; we could see in the query report. Impressions from an audience we don't serve in their language are not a win.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The H1 stayed &lt;strong&gt;"Git + Figma for database design."&lt;/strong&gt; The &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and the H1 now disagree, on purpose. The &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; answers a query; the H1 answers "what is this, and why should I care" for someone who has already arrived. Collapsing both into keywords costs you the second job and reads like a landing page from 2011.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the crawler's view a test
&lt;/h2&gt;

&lt;p&gt;The failure mode here is silent, so the only durable fix is an assertion. &lt;code&gt;scripts/seo-index-health.mjs&lt;/code&gt; runs daily in CI and checks, against the live site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each of &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/demo&lt;/code&gt;, &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/docs/&lt;/code&gt;, &lt;code&gt;/docs/roadmap&lt;/code&gt;: HTTP 200, &lt;code&gt;text/html&lt;/code&gt;, non-empty &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, non-empty meta description, a canonical, and no &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/robots.txt&lt;/code&gt;: 200, not &lt;code&gt;text/html&lt;/code&gt; (a robots.txt served as HTML means the SPA fallback ate it), contains &lt;code&gt;User-agent: *&lt;/code&gt;, contains an absolute &lt;code&gt;Sitemap:&lt;/code&gt; URL, and does &lt;strong&gt;not&lt;/strong&gt; contain &lt;code&gt;content-signal:&lt;/code&gt; — that last string is how you detect that your platform's injected default robots.txt is still winning over yours.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/__seo_health_nonexistent_path__&lt;/code&gt;: must return &lt;strong&gt;404&lt;/strong&gt;. This is the soft-404 regression, pinned as one assertion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/demo&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;: must still return &lt;strong&gt;200&lt;/strong&gt;, because the obvious way to fix soft 404s is to break your real routes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.xml&lt;/code&gt;: parses, has at least one &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, and the first 20 URLs are reachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At build time, &lt;code&gt;yarn test:seo-static&lt;/code&gt; runs the same config without a full build, and the production smoke suite has a test named, verbatim, &lt;code&gt;crawler first HTML uses path canonical (not homepage)&lt;/code&gt;. The bug we shipped for months now has a name in a test file.&lt;/p&gt;

&lt;p&gt;Two more things from the same pass, both non-obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache headers alone did nothing.&lt;/strong&gt; The homepage was returning &lt;code&gt;CF-Cache-Status: DYNAMIC&lt;/code&gt; with TTFB around 1.6s (our docs site, 3.8s) while Search Console flagged impressions sliding. We set &lt;code&gt;Cache-Control: public, max-age=0, s-maxage=600, must-revalidate&lt;/code&gt; on the HTML shells. Nothing changed — Cloudflare Pages treats &lt;code&gt;text/html&lt;/code&gt; as dynamic by default, and you must also create a Cache Rule in the dashboard making HTML eligible for cache. That dependency is now a comment in &lt;code&gt;_headers&lt;/code&gt;, because a header file that silently does nothing is worse than no header file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick one slash form and make all four places agree.&lt;/strong&gt; Our docs site canonical said no trailing slash, the sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; said no trailing slash, the host 308'd to a trailing slash, and legacy redirects pointed at the non-slash form — so an old URL took a 301 and then a 308 to arrive. &lt;code&gt;trailingSlash: true&lt;/code&gt;, redirect targets slashed, sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; slashed, &lt;code&gt;/search&lt;/code&gt; and blog tag/archive pages dropped from the sitemap. One hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we stopped doing
&lt;/h2&gt;

&lt;p&gt;Clicking "Request indexing." We tried it on &lt;code&gt;/catalog&lt;/code&gt; twice and got Search Console's reCAPTCHA error both times; nothing entered the queue. Manual submission is a nudge, not a mechanism, and treating it as progress hides the fact that your artifacts are still wrong. Fix the artifact, submit the sitemap, then wait.&lt;/p&gt;

&lt;p&gt;We also decided, in advance, what would make us change strategy. Our locale routing ADR ships English marketing pages as CSR with a sitemap and hreflang, and states the trigger explicitly: &lt;strong&gt;if &lt;code&gt;/en&lt;/code&gt; is not indexed 90 days after launch, we build static export for the marketing routes.&lt;/strong&gt; Writing the trigger down before you need it is what stops "should we go SSR?" from being re-litigated every two weeks on vibes.&lt;/p&gt;

&lt;p&gt;The same ADR is worth reading for what it refused to do. Full-site &lt;code&gt;/en/*&lt;/code&gt; routing would have touched 60 route entries, 76 &lt;code&gt;history.push&lt;/code&gt; call sites across 39 files, 236 &lt;code&gt;page.goto&lt;/code&gt; calls in the E2E suite, 16 backend files containing frontend URLs, and the immutable &lt;code&gt;/s/:token&lt;/code&gt; share links already pasted into other people's chat logs. The pages that would have gained from it — &lt;code&gt;/project/**&lt;/code&gt;, &lt;code&gt;/design/**&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt; — are all behind a login and have no search value whatsoever. Marketing pages got the prefix. Nothing else did. Internal link points touched: about 21.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard, honestly
&lt;/h2&gt;

&lt;p&gt;As of today: &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/compare&lt;/code&gt;, and &lt;code&gt;/en/compare&lt;/code&gt; are indexed. &lt;code&gt;/en/catalog&lt;/code&gt; is "discovered — currently not indexed." The docs site's English sitemap was accepted and 66 pages were discovered. Total organic clicks over three months: &lt;strong&gt;2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not a traffic case study. Indexing moves on a timescale of weeks, and it would be dishonest to attach a hockey stick to a twelve-day engineering pass. What we can report is that the crawler's view of the site is now correct, asserted daily, and cheap to extend — which is the part that had to be true before any content investment could compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;If you run a client-rendered site, these are worth thirty minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;curl -sL &amp;lt;your-non-homepage-url&amp;gt; | grep -E '&amp;lt;title&amp;gt;|canonical'&lt;/code&gt;. If you see your homepage, stop reading and fix that.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl -sI &amp;lt;your-domain&amp;gt;/__definitely_not_a_real_path__&lt;/code&gt;. If it's 200, you have unbounded soft 404s. On Cloudflare Pages, ship a root &lt;code&gt;404.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delete the catch-all rewrite. Enumerate the SPA paths that genuinely need a 200, and let everything else 404.&lt;/li&gt;
&lt;li&gt;Make sitemap, prerendered shells, host rewrites, and self-host rewrites derive from one module. Adding a route should be one line.&lt;/li&gt;
&lt;li&gt;Check that hydration isn't overwriting per-path metadata your build just wrote.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;@type&lt;/code&gt; in your JSON-LD is actually per-page, not the homepage type with a swapped &lt;code&gt;url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pick one trailing-slash form. Canonical, sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, host redirect, and legacy redirects must all agree.&lt;/li&gt;
&lt;li&gt;Turn every one of the above into a daily assertion against production. This bug class is silent by construction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.erdonline.com/" rel="noopener noreferrer"&gt;ERD Online&lt;/a&gt; is an open-source (MIT) database design tool: &lt;strong&gt;Git + Figma for database design.&lt;/strong&gt; Versions and collaboration are the moat, and the &lt;code&gt;projectJSON&lt;/code&gt; format is open so humans and AI agents read and write the same source of truth — an agent can call &lt;code&gt;create_version&lt;/code&gt; through MCP, and a human still reviews the diff in the designer. It is not ChatSQL; it will not invent a diagram from a sentence.&lt;/p&gt;

&lt;p&gt;Open a real ER diagram, read-only, no signup: &lt;strong&gt;&lt;a href="https://www.erdonline.com/demo" rel="noopener noreferrer"&gt;https://www.erdonline.com/demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source, issues, and the SEO scripts described above: &lt;strong&gt;&lt;a href="https://github.com/erdonline/erdonline" rel="noopener noreferrer"&gt;https://github.com/erdonline/erdonline&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The config module is &lt;code&gt;frontend/scripts/seo-config.mjs&lt;/code&gt;, the health check is &lt;code&gt;scripts/seo-index-health.mjs&lt;/code&gt;. Both are short. Steal them.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to let AI agents manage your database schema (with MCP)</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sat, 29 Aug 2026 06:07:07 +0000</pubDate>
      <link>https://dev.to/erdonline/how-to-let-ai-agents-manage-your-database-schema-with-mcp-12k1</link>
      <guid>https://dev.to/erdonline/how-to-let-ai-agents-manage-your-database-schema-with-mcp-12k1</guid>
      <description>&lt;p&gt;AI agents are becoming first-class citizens in developer workflows. They can read code, run tests, and deploy apps. But one thing they struggle with is understanding database schemas.&lt;/p&gt;

&lt;p&gt;Database design tools haven't changed in 20 years. You either use a heavyweight desktop app (Navicat, PDManer) or a pretty but closed web app (dbdiagram). Neither supports versioning, real-time collaboration, or AI agent integration.&lt;/p&gt;

&lt;p&gt;I built ERD Online to solve this. It's an open-source database design tool that combines Git-like versioning with Figma-like collaboration, plus MCP integration for AI agents.&lt;/p&gt;

&lt;p&gt;In this article, I'll show you how to let Cursor, Claude, or Cline read and write your database schema through MCP, while you keep full control.&lt;/p&gt;

&lt;p&gt;Database schema changes are hard to track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who changed what?&lt;/li&gt;
&lt;li&gt;When did they change it?&lt;/li&gt;
&lt;li&gt;Why did they change it?&lt;/li&gt;
&lt;li&gt;How do I rollback?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now with AI agents, there's a new problem: how do you let an AI agent suggest schema changes without giving it a black box that generates random ER diagrams?&lt;/p&gt;

&lt;p&gt;The wrong approach: ask AI to "generate an ER diagram for an e-commerce app." You get a diagram, but it has no connection to your actual project, no versioning, and no approval flow.&lt;/p&gt;

&lt;p&gt;The right approach: let the AI agent read your existing schema, suggest changes, and submit them as a version that you review and approve.&lt;/p&gt;

&lt;p&gt;That's what ERD Online + MCP does.&lt;/p&gt;

&lt;p&gt;MCP (Model Context Protocol) is a protocol for AI agents to interact with external tools. Think of it as a USB-C port for AI applications. It standardizes how agents discover and call tools.&lt;/p&gt;

&lt;p&gt;MCP has three main primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: Functions the AI can call (like &lt;code&gt;list_projects&lt;/code&gt; or &lt;code&gt;create_version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: Data the AI can read (like &lt;code&gt;project.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: Pre-defined templates for common tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ERD Online exposes MCP tools that let AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_projects&lt;/code&gt;: List all your ERD projects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_project&lt;/code&gt;: Get a project's projectJSON&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_version&lt;/code&gt;: Suggest a new version of your schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key boundary: AI agents read/write the same projectJSON the human designer uses. They don't generate ER diagrams from a sentence. You stay in control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Mint a PAT in ERD Online&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sign in to ERD Online and go to Account Settings → Personal Access Tokens. Create a new token (read-only is enough to start).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Add MCP config to Cursor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; and add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. AI agents become useful, not dangerous&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By giving AI agents structured access to your schema, they can make useful suggestions. But because they go through &lt;code&gt;create_version&lt;/code&gt; and you review the diff, they can't silently break your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Versioning becomes the source of truth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every AI-suggested change creates a version. You can see exactly what changed, when, and why. This is essential for database schema management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Collaboration improves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multiple people (and agents) can work on the same schema. Changes are merged, versioned, and reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No vendor lock-in&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;projectJSON is an open format. You can export your schema, self-host ERD Online, or build your own tools on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERD Online&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo (no signup): &lt;a href="https://www.erdonline.com/demo" rel="noopener noreferrer"&gt;https://www.erdonline.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/erdonline/erdonline" rel="noopener noreferrer"&gt;https://github.com/erdonline/erdonline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP docs: &lt;a href="https://doc.erdonline.com/docs/guide/api-and-mcp/" rel="noopener noreferrer"&gt;https://doc.erdonline.com/docs/guide/api-and-mcp/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cursor bridge: &lt;a href="https://www.erdonline.com/cursor-mcp/" rel="noopener noreferrer"&gt;https://www.erdonline.com/cursor-mcp/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What would you build?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear what AI + database schema workflows you'd want. Drop a comment or open an issue on GitHub.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
