<?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: Rakibul Hasan</title>
    <description>The latest articles on DEV Community by Rakibul Hasan (@101rakibulhasan).</description>
    <link>https://dev.to/101rakibulhasan</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%2F1232786%2Fdc964c1d-82f1-4443-8062-c46a4869193c.jpeg</url>
      <title>DEV Community: Rakibul Hasan</title>
      <link>https://dev.to/101rakibulhasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/101rakibulhasan"/>
    <language>en</language>
    <item>
      <title>How I got a Flutter web app (CanvasKit) indexed by Google with static SEO shells</title>
      <dc:creator>Rakibul Hasan</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:18:55 +0000</pubDate>
      <link>https://dev.to/101rakibulhasan/how-i-got-a-flutter-web-app-canvaskit-indexed-by-google-with-static-seo-shells-315d</link>
      <guid>https://dev.to/101rakibulhasan/how-i-got-a-flutter-web-app-canvaskit-indexed-by-google-with-static-seo-shells-315d</guid>
      <description>&lt;p&gt;Flutter web is great for building an app once and shipping it everywhere. It's not great for SEO.&lt;/p&gt;

&lt;p&gt;With the CanvasKit renderer, everything is drawn onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. If you open "view source" on a Flutter web page, all you get is a nearly empty &lt;code&gt;index.html&lt;/code&gt; and a script tag. Google can render JavaScript, but rendering is queued and slow on a large app. Many other crawlers and the link preview bots (WhatsApp, Facebook, X) don't run it at all.&lt;/p&gt;

&lt;p&gt;I ran into this building &lt;a href="https://www.typeordrop.com/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;TypeorDrop&lt;/a&gt;, a free online file converter. It has 150 tool pages, like &lt;code&gt;/convert-jpg-to-pdf&lt;/code&gt; or &lt;code&gt;/merge-pdf&lt;/code&gt;. Every one of them needs its own title, description and indexable text, or it will never rank.&lt;/p&gt;

&lt;p&gt;There are packages for this (&lt;code&gt;flutter_prerender&lt;/code&gt;, &lt;code&gt;esen_seo&lt;/code&gt;) and paid prerender services. I wanted the SEO text to come from the same database column the app already renders, with no new dependency in the Flutter app, so I wrote a small build step instead: &lt;strong&gt;static SEO shells&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;A shell is a copy of Flutter's built &lt;code&gt;index.html&lt;/code&gt;, generated at deploy time, one per route. Each copy gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unique &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, meta description and &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open Graph and Twitter card tags&lt;/li&gt;
&lt;li&gt;JSON-LD: &lt;code&gt;SoftwareApplication&lt;/code&gt;, plus &lt;code&gt;FAQPage&lt;/code&gt; when the tool has FAQ items&lt;/li&gt;
&lt;li&gt;real body text: H1, intro, how-to steps, features, supported input and output formats, FAQ, related tools and related articles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Flutter bootstrap stays the same. A crawler reads the HTML. A person's browser loads Flutter, which draws the app on top. The same URL serves the same HTML to everyone. Nothing switches on user agent, which would be cloaking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/web/
├── index.html                    ← homepage shell
├── convert-jpg-to-pdf/index.html ← shell for /convert-jpg-to-pdf
├── merge-pdf/index.html
├── ...
├── main.dart.js                  ← shared by every shell
└── sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One source of truth for the text
&lt;/h2&gt;

&lt;p&gt;The most important rule: &lt;strong&gt;the shell text should match what users see&lt;/strong&gt;. If the hidden HTML says one thing and the app shows another, you're asking for trouble with search engines.&lt;/p&gt;

&lt;p&gt;So the text doesn't live in the generator. Each tool row in the database has an &lt;code&gt;seo&lt;/code&gt; JSON column (h1, intro, steps, features, FAQ, related tools). The API returns that column to the Flutter tool page, which renders the how-it-works, features, formats and FAQ sections from it. At deploy time, the same column is exported to a temporary JSON file and the generator reads it. When I edit a FAQ answer, the app and the shell both change on the next deploy.&lt;/p&gt;

&lt;p&gt;The pricing shell and the homepage's popular-tools list go one step further: the generator reads them from the same Dart constants the app uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generator (simplified)
&lt;/h2&gt;

&lt;p&gt;The real script is longer and has tests (77 across the SEO and blog generators, all offline with mocked HTTP). This is the shape of it:&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;# runs after `flutter build web --release`
# tools = JSON export of active web tools, including the seo column
&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build/web/index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&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;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;            &lt;span class="c1"&gt;# e.g. "convert-jpg-to-pdf"
&lt;/span&gt;    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stamp_head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# replaces &amp;lt;title&amp;gt; + meta description; adds og:*, twitter:* and
&lt;/span&gt;    &lt;span class="c1"&gt;# &amp;lt;link rel="canonical"&amp;gt; just before &amp;lt;/head&amp;gt;
&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace_seo_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;seo_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# fills the &amp;lt;div id="seo-content" class="visually-hidden"&amp;gt; placeholder
&lt;/span&gt;    &lt;span class="c1"&gt;# (outside the Flutter host element): h1, intro, how-to &amp;lt;ol&amp;gt;, features,
&lt;/span&gt;    &lt;span class="c1"&gt;# formats, FAQ, related tools/articles + SoftwareApplication &amp;amp; FAQPage JSON-LD
&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace_footer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# plain-HTML footer with real links
&lt;/span&gt;    &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build/web/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# then: sitemap.xml (lastmod = each tool's updated_at), robots.txt,
# and the homepage shell (Organization, WebSite, ItemList of popular tools)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules live in the generator too: titles stay at 60 characters or less with the brand last, descriptions land between 120 and 160 characters, and duplicate titles or descriptions print a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it work: keep Flutter out of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;By default, Flutter takes over &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; and your shell content ends up under the canvas or gone. I use a custom &lt;code&gt;flutter_bootstrap.js&lt;/code&gt; that mounts Flutter into a dedicated element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_flutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onEntrypointLoaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engineInitializer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;appRunner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engineInitializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;hostElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#flutter-host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;();&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;The &lt;code&gt;#seo-content&lt;/code&gt; block sits outside &lt;code&gt;#flutter-host&lt;/code&gt;, and nothing removes it after boot. It's hidden with a standard visually-hidden class, not &lt;code&gt;display:none&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.visually-hidden&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;Two fallbacks come almost free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript:&lt;/strong&gt; a &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; style block turns the shell into a normal visible page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flutter fails to load:&lt;/strong&gt; if the app hasn't painted after 20 seconds, the static page is shown instead of a blank screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also make sure &lt;code&gt;&amp;lt;base href="/"&amp;gt;&lt;/code&gt; is set. Otherwise &lt;code&gt;/convert-jpg-to-pdf/index.html&lt;/code&gt; looks for &lt;code&gt;main.dart.js&lt;/code&gt; inside its own folder and the app never boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving it with nginx
&lt;/h2&gt;

&lt;p&gt;Canonical URLs have no trailing slash, so &lt;code&gt;/convert-jpg-to-pdf&lt;/code&gt; must return the shell with a 200, not a redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# /convert-jpg-to-pdf -&amp;gt; convert-jpg-to-pdf/index.html, 200, no redirect&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/index.html&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-cache"&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;The detail that matters is &lt;code&gt;$uri/index.html&lt;/code&gt; instead of &lt;code&gt;$uri/&lt;/code&gt;. With &lt;code&gt;$uri/&lt;/code&gt;, nginx adds its own trailing-slash redirect. I also 301 &lt;code&gt;/x/&lt;/code&gt; to &lt;code&gt;/x&lt;/code&gt;, so the two rules together would loop. With &lt;code&gt;$uri/index.html&lt;/code&gt;, there's no loop.&lt;/p&gt;

&lt;p&gt;HTML is served &lt;code&gt;no-cache&lt;/code&gt;, and each build ID is added to the URLs of &lt;code&gt;main.dart.js&lt;/code&gt;, &lt;code&gt;flutter_bootstrap.js&lt;/code&gt; and the service worker, so a deploy never leaves people on a stale app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things around the shells that helped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap from the same data.&lt;/strong&gt; The generator writes &lt;code&gt;sitemap.xml&lt;/code&gt; from the same tool list, and the blog and static page generators add their URLs. A tool can't be live and missing from the sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean removals.&lt;/strong&gt; Every build starts from an empty folder, and the upload to the server deletes files that are no longer there, so a removed tool's page disappears. Routes I discontinued return &lt;code&gt;410 Gone&lt;/code&gt;, so search engines drop them fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IndexNow after deploy.&lt;/strong&gt; When all post-deploy checks pass, the script POSTs every sitemap URL to &lt;code&gt;api.indexnow.org&lt;/code&gt;. Bing and others pick up changes without waiting for a recrawl.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noindex the API host.&lt;/strong&gt; My API subdomain had started showing up in search results. A &lt;code&gt;robots.txt&lt;/code&gt; on it plus &lt;code&gt;X-Robots-Tag: noindex, nofollow&lt;/code&gt; fixed that. The tools endpoint stays allowed, because Googlebot needs it to render the real pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain HTML for articles.&lt;/strong&gt; Blog posts are fully static pages generated from Markdown. Flutter is the wrong tool for article pages. The deploy script fails if a post accidentally includes the Flutter bootstrap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd still improve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Soft 404s.&lt;/strong&gt; Unknown URLs fall back to &lt;code&gt;/index.html&lt;/code&gt; with a 200, so a mistyped URL gets the homepage instead of a real 404. Blog URLs already return real 404s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;og:image on tool pages.&lt;/strong&gt; Link previews show the right title and description, but tool shells don't have their own preview image yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results so far
&lt;/h2&gt;

&lt;p&gt;Before shells, every URL returned the same empty document with the same title, and Search Console showed little beyond the homepage. Now tool pages get indexed on their own, each with its own title and description in results, and chat link previews show the right page.&lt;/p&gt;

&lt;p&gt;The site is young, so rankings are moving slowly. But the "Google can't see my Flutter app" problem is solved, and it needed no SSR, no second frontend and no new Flutter package.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this approach is enough
&lt;/h2&gt;

&lt;p&gt;Static shells work well when your public pages are &lt;strong&gt;known at build time&lt;/strong&gt;: tool pages, landing pages, docs. If you have millions of user-generated pages, you need real server rendering or a prerender service.&lt;/p&gt;

&lt;p&gt;If you're shipping a Flutter web app and wondering why nobody finds it, open "view source" on a few of your URLs. If they all look the same, that's your problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://www.typeordrop.com/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;TypeorDrop&lt;/a&gt;: free file conversion, PDF and image tools in the browser, built with Flutter web, FastAPI and Celery. Questions about the setup are welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>seo</category>
      <category>webdev</category>
      <category>python</category>
    </item>
  </channel>
</rss>
