<?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: Guluhehe</title>
    <description>The latest articles on DEV Community by Guluhehe (@guluhehe).</description>
    <link>https://dev.to/guluhehe</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%2F4170562%2F26e391c0-1fd0-45b7-ac11-011a4716c903.jpg</url>
      <title>DEV Community: Guluhehe</title>
      <link>https://dev.to/guluhehe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guluhehe"/>
    <language>en</language>
    <item>
      <title>Building a chart editor with no backend: storing charts in the browser and exporting a PNG</title>
      <dc:creator>Guluhehe</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:33:37 +0000</pubDate>
      <link>https://dev.to/guluhehe/building-a-chart-editor-with-no-backend-storing-charts-in-the-browser-and-exporting-a-png-30k0</link>
      <guid>https://dev.to/guluhehe/building-a-chart-editor-with-no-backend-storing-charts-in-the-browser-and-exporting-a-png-30k0</guid>
      <description>&lt;p&gt;RelationCanvas is a relationship chart editor for fiction writers: characters are cards, relationships are labeled arrows. It has no accounts and no database. The server sends the pages and the JavaScript, and it never receives a chart. That decision shaped three parts of the code: saving, PNG export and sharing. Here's how each one works. (It's a Next.js 16 and React 19 app, but none of this depends on the framework.)&lt;/p&gt;

&lt;p&gt;The live tool is at &lt;a href="https://relationcanvas.com/" rel="noopener noreferrer"&gt;RelationCanvas&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One format for everything
&lt;/h2&gt;

&lt;p&gt;A chart is a JSON object wrapped in a small envelope: a format name (&lt;code&gt;relationcanvas&lt;/code&gt;), a version number (1) and the chart itself. The chart holds a title, a credit line, a legend toggle, the people (id, name, position, optional picture, note and colour) and the relationships (from, to, and a label that is either a preset or custom text).&lt;/p&gt;

&lt;p&gt;The same envelope is used for browser storage, for the downloadable project file, and inside share links. One parser reads all three, so a file can't be accepted in one place and rejected in another. Share links get an extra, stricter key check on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saving to localStorage
&lt;/h2&gt;

&lt;p&gt;Every change is written to &lt;code&gt;localStorage&lt;/code&gt; under one key, &lt;code&gt;relationcanvas.chart.v1&lt;/code&gt;, with a revision number added to the stored JSON. That sounds like one line of code. The edge cases took most of the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data that won't parse.&lt;/strong&gt; If the stored value fails validation, the editor copies the raw string to a second key (&lt;code&gt;relationcanvas.chart.v1.backup&lt;/code&gt;), starts with an empty chart and shows a notice saying the old data was kept aside. If that backup can't be confirmed, saving is blocked rather than risk overwriting the only copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two tabs.&lt;/strong&gt; Before each write, the editor reads the stored revision. If it differs from the revision this tab loaded, the write is treated as a conflict, not an overwrite. The editor also listens for the browser's &lt;code&gt;storage&lt;/code&gt; event, so when another tab saves, this tab can adopt the newer chart if it has no unsaved changes of its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage that fails.&lt;/strong&gt; When a write fails, the editor retries with a delay that starts at one second and doubles up to a 30-second cap. If storage can't be read at all, the notice tells the user to download a project file to keep new work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pictures.&lt;/strong&gt; A character picture is cropped to a square in the browser and stored as a data URL inside the chart. The encoder tries PNG at 256, 192 and 128 pixels, then WebP at quality 0.9, 0.75 and 0.6, and keeps the first result under 120,000 characters. The cap puts a known ceiling on what one picture adds to storage.&lt;/p&gt;

&lt;p&gt;The honest limit: clearing site data deletes the chart. The site says so, and the File menu has Download project file and Import project for backups and moving devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  PNG export without a server
&lt;/h2&gt;

&lt;p&gt;The chart is drawn by a function that returns an SVG string. The same renderer produces the export preview and the read-only share view.&lt;/p&gt;

&lt;p&gt;Export takes that SVG, serializes it into a Blob with type &lt;code&gt;image/svg+xml&lt;/code&gt;, loads it through an object URL into an image element, and draws the image onto a canvas that has been filled with the theme's background colour. &lt;code&gt;canvas.toBlob&lt;/code&gt; produces the PNG, which downloads as &lt;code&gt;relationcanvas.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three details came from things going wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale.&lt;/strong&gt; The user picks 2x or 4x. The dialog shows the final pixel size and the line "Full chart · No watermark" before downloading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size limits.&lt;/strong&gt; If either side would exceed 16,384 pixels, or the total would pass 32,000,000 pixels, export refuses and suggests 2x or moving the characters closer together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blank-image check.&lt;/strong&gt; After drawing, the code reads the top-left pixel. If it doesn't match the background colour, the export reports a failure instead of handing over an empty file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sharing without a database
&lt;/h2&gt;

&lt;p&gt;A share link is a URL to &lt;code&gt;/view&lt;/code&gt; with the chart in the fragment, after the &lt;code&gt;#&lt;/code&gt;. The fragment has three dot-separated parts: a version tag (&lt;code&gt;rc1&lt;/code&gt;), the project JSON compressed with the browser's &lt;code&gt;CompressionStream&lt;/code&gt; in &lt;code&gt;deflate-raw&lt;/code&gt; mode and encoded as base64url, and a base64url SHA-256 digest of the compressed bytes.&lt;/p&gt;

&lt;p&gt;Browsers don't send the fragment to the server, so names and labels stay out of request logs.&lt;/p&gt;

&lt;p&gt;Two rules keep links usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No pictures.&lt;/strong&gt; Avatars are removed before encoding. The share dialog says "Images aren't included in this link. PNG export keeps them."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,000 characters, total.&lt;/strong&gt; The whole URL, not only the fragment, has to fit in 2,000 characters, because a normal Discord message is 2,000 characters and a longer one turns into a text file instead of a link. If the chart is too big, there's no link, and the editor suggests Export PNG and a project file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The viewer is defensive about what it accepts. It checks the digest before decompressing. It caps decompression at 4,968,766 bytes, which is twice the size of the largest chart the editor can produce (20 people, every field at its limit, 2,484,383 bytes). It then checks the JSON against a fixed list of allowed keys before turning it into a chart. Older links without the digest still open, because a link someone has already posted should keep working.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/view&lt;/code&gt; page is &lt;code&gt;noindex, follow&lt;/code&gt;, so shared charts don't end up in search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this design gives up
&lt;/h2&gt;

&lt;p&gt;A chart lives in one browser profile. There's no sync between devices beyond the project file. Share links are read-only snapshots, not collaboration. Large charts can't be shared as links at all. The editor is designed around casts of up to 20 people, and the limits above are sized for that.&lt;/p&gt;

&lt;p&gt;If you've built something similar with localStorage, I'd like to know how you handled two tabs editing the same data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I drafted this article with help from an AI assistant. The technical details come from the project's source code, and I checked them before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I ran the same 8 searches on 6 search engines to see which ones don't use AI</title>
      <dc:creator>Guluhehe</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:48:52 +0000</pubDate>
      <link>https://dev.to/guluhehe/i-ran-the-same-8-searches-on-6-search-engines-to-see-which-ones-dont-use-ai-4pp0</link>
      <guid>https://dev.to/guluhehe/i-ran-the-same-8-searches-on-6-search-engines-to-see-which-ones-dont-use-ai-4pp0</guid>
      <description>&lt;p&gt;If I don't want AI-generated answers on top of my search results, which search engine should I use? I couldn't find a straight answer.&lt;/p&gt;

&lt;p&gt;Most articles on this repeat what each company says about itself, so I tested it: same queries, same browser, same day, a screenshot of every result. Below is the setup, the results, and the engines I couldn't test.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date:&lt;/strong&gt; October 6, 2026, one sitting of about 40 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser:&lt;/strong&gt; Chrome 154 (154.0.8037.98) on a Mac, desktop window at 1366×900, signed out of everything, in a dedicated Chrome profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; a US exit IP (San Jose, California). A small script checks the IP with two services before the run and refuses to start if either one says it isn't US. It was a proxy, so a datacenter address, not a home connection. That likely played a part in the CAPTCHAs and blocks described below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation:&lt;/strong&gt; a Playwright script drove a visible Chrome window. For each query it opened the results URL, waited for the network to go quiet plus 8 seconds, then saved a full-page screenshot and the page text. Queries were 4 to 8 seconds apart. If a CAPTCHA appeared, the script paused so a person could solve it. If it stayed blocked, the cell was logged as blocked, not guessed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The eight queries, locked before the first run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;knowledge&lt;/td&gt;
&lt;td&gt;why is the sky blue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;how-to&lt;/td&gt;
&lt;td&gt;how to remove a stripped screw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;health&lt;/td&gt;
&lt;td&gt;symptoms of low iron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product&lt;/td&gt;
&lt;td&gt;best budget noise cancelling headphones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;person&lt;/td&gt;
&lt;td&gt;who was Ada Lovelace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;news&lt;/td&gt;
&lt;td&gt;inflation news&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comparison&lt;/td&gt;
&lt;td&gt;baking soda vs baking powder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;golden retriever puppy (image search)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven are web searches, and they're the ones the AI results below are counted on. The eighth is an image search, used to check for AI image filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What counted as AI:&lt;/strong&gt; a generated answer, summary or Overview that shows up on its own, an AI chat or assistant panel, or AI-generated images in results. Ranking algorithms, spell check, autocomplete and knowledge panels didn't count. A tab or button that only does something AI-related after you click it is noted, but doesn't count as AI by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Web queries with an AI answer&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DuckDuckGo No-AI (&lt;code&gt;noai.duckduckgo.com&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0 of 7&lt;/td&gt;
&lt;td&gt;A Duck.ai link stays in the top bar. "AI images: hide" was on by default in image search.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google with the Web filter (udm=14)&lt;/td&gt;
&lt;td&gt;0 of 7&lt;/td&gt;
&lt;td&gt;The AI Mode tab is still in the menu. The filter doesn't stick between searches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startpage&lt;/td&gt;
&lt;td&gt;0 of 7&lt;/td&gt;
&lt;td&gt;Results come from Google and Bing. Shopping ads can show on product queries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SearXNG (&lt;code&gt;baresearch.org&lt;/code&gt; instance)&lt;/td&gt;
&lt;td&gt;0 of 7&lt;/td&gt;
&lt;td&gt;Depends on the instance; I only tested this one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosia&lt;/td&gt;
&lt;td&gt;no verdict&lt;/td&gt;
&lt;td&gt;1 query loaded, then a Cloudflare challenge on the other 7.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mojeek&lt;/td&gt;
&lt;td&gt;no verdict&lt;/td&gt;
&lt;td&gt;HTTP 403 on my test search.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Four with no AI answers (one of them is a workaround)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;DuckDuckGo No-AI&lt;/strong&gt; is a separate address, &lt;code&gt;noai.duckduckgo.com&lt;/code&gt;, not a setting on the normal site. DuckDuckGo's help pages say it turns off Search Assist and Duck.ai and filters AI-generated images by default, and that's what I saw: no AI answer on any of the 7 web queries, and the image search had "AI images: hide" switched on. The regular &lt;code&gt;duckduckgo.com&lt;/code&gt; can still show Search Assist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google with the Web filter&lt;/strong&gt; also showed no AI Overview on any of the 7 queries. The Web filter is the "Web" option above the results (sometimes under "More"), or &lt;code&gt;udm=14&lt;/code&gt; in the URL. Two catches. Google's own help page says these features "cannot be turned off", so this is a workaround, and it resets on every new search unless you make that URL your default search engine (not tested yet). Also, I didn't run default Google in this round, so I can't say how often these queries get an AI Overview without the filter, only that the filtered page never had one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startpage&lt;/strong&gt; showed no AI answers on any of the 7. It has no index of its own and passes your query to Google and Bing, so of the engines I tested, it's the nearest to Google results without the AI. There's no filter for AI-generated images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SearXNG&lt;/strong&gt; is open-source metasearch software that anyone can host, so "SearXNG" really means "one particular instance". On &lt;code&gt;baresearch.org&lt;/code&gt;: 0 of 7. Another instance may be configured differently, and public instances go down or rate-limit you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two I couldn't test properly
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Ecosia:&lt;/strong&gt; the first query loaded with no AI answer, though there's an "AI Chat" tab in the top menu. Every query after that hit a Cloudflare "are you human" challenge. Ecosia's help pages describe an "AI-free searching" setting and say AI Overviews are only offered in Germany, France, the US and the UK. One query isn't a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mojeek&lt;/strong&gt; returned HTTP 403 on my test search, from both machines I tried. Mojeek runs its own crawler and index, so it's the one I want to retest first.&lt;/p&gt;

&lt;p&gt;Two more on my list aren't in this write-up. &lt;strong&gt;Brave Search&lt;/strong&gt;: my testing isn't finished (I still need to check the results page with its AI setting switched off), so I'll report it after the next round. &lt;strong&gt;Kagi&lt;/strong&gt;: even the free trial needs an account, and I didn't sign up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bot protection was the biggest obstacle.&lt;/strong&gt; My first attempt, a headless browser on a server outside the US, was nearly useless: Google, Mojeek and DuckDuckGo all blocked it or refused the connection. Even with a visible browser on a US IP, Google wanted a CAPTCHA on the first query, Ecosia blocked 7 of 8, and Mojeek refused my test search outright. If you try to automate this, you'll need a real browser, a residential connection and someone to solve CAPTCHAs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"No AI" is mostly about the default, not the product.&lt;/strong&gt; Two of the four clean results are opt-outs: a special subdomain (DuckDuckGo) or a URL parameter (Google). If you visit the normal homepage, you get the normal experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Location matters.&lt;/strong&gt; From a Singapore IP, Ecosia showed no AI on the one query that loaded, which is expected if AI Overviews aren't offered there. A test from the wrong country can look clean when it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One browser (desktop Chrome), one country (US), one day. No Firefox, Edge, Safari or phones yet.&lt;/li&gt;
&lt;li&gt;Signed out. Signed-in results may differ.&lt;/li&gt;
&lt;li&gt;It's a snapshot. I plan to rerun the same queries once a month.&lt;/li&gt;
&lt;li&gt;Brave Search, Ecosia, Mojeek and Kagi need another round before I'd call any of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the screenshots live
&lt;/h2&gt;

&lt;p&gt;I keep every screenshot, dated, plus a comparison table on a small site I run called Web Without AI. No ads or affiliate links there. The test rules are the ones in this post, and I'll update the table after each monthly rerun.&lt;/p&gt;

&lt;p&gt;If you use a search engine I didn't cover, or you see different results in your country, tell me in the comments and I'll add it to the next round.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I drafted this article with help from an AI assistant. The test runs, screenshots and numbers come from my own test log, and I checked them before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I made &lt;a href="https://webwithoutai.com/" rel="noopener noreferrer"&gt;Web Without AI&lt;/a&gt; to keep these results updated, including mobile workarounds.&lt;/p&gt;

</description>
      <category>search</category>
      <category>privacy</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
