← All posts
18 August 2026

The site looks fine. We opened the code.

Generated sites arrive looking finished. Then you read the file: 99.6% base64, no lazy loading, no meta description. Here is what it costs and how to check.

Websites now arrive at our door already built. Somebody who is not a developer described what they wanted, a model produced a working page, and it works: it renders, it is responsive, the type is set properly, the spacing is deliberate. Then we are asked to host it, extend it, put it on a real domain, or fold it into a larger site, and the first thing we do is open the file.

What is in the file is consistently not what is on the screen. That gap is the whole subject of this article.

None of this is an argument against generating your own website. Some of the markup we have read was better than a good deal of hand-written code, in ways we will name below, because credit belongs where it is due. The argument is narrower: the mistakes that survive are the ones that do not change how the page looks, and if looking at the page is your only check, those are precisely the mistakes you will ship.

The short version

The recurring pattern is a single HTML file with every asset base64-encoded inside it. In the heaviest example we have measured, 99.6% of a 4.7 MB file was base64 and the actual code came to about 20 KB. That one decision drags four others behind it: assets that no browser can cache, a document the parser must read past before it can paint, images that cannot be made responsive or deferred, and fonts shipped in an uncompressed format. Alongside it sits a second, quieter pattern: a document head with no description, no canonical URL and no social preview tags, on sites whose entire distribution plan is people sharing a link. Both are invisible in a browser window. Both are cheap to fix once somebody reads the file.

The pattern: one file, everything inside it

Open a generated site and there is usually no assets folder, no stylesheet, no image directory. There is one HTML document, and it is enormous. Every photograph, every logo, every font has been base64-encoded and pasted inline, into the <style> block or into an src attribute.

The numbers from one one-page promotional site, which is a genuinely handsome piece of work:

Delivered file4.7 MB, single HTML document
Actual code in it~20 KB
Base64 payload99.6% of the file
Assets, decoded3.35 MB
Assets, as base64 text4.47 MB
Largest single assetone photograph, 1.9 MB
Images41
Images lazy-loaded0
Images with srcset0

For scale: the median mobile home page in 2025 was 2.56 MB across the whole page, everything included. This was one file, nearly double that, and none of it cacheable.

It is worth being clear about why this happens, because it is not carelessness. The tool gives you one text box. There is nowhere to put a file, so everything becomes text. It is the only architecture available, and it renders perfectly, which is why it survives all the way to production.

What it costs

Base64 is about a third larger than the thing it encodes. The format represents three bytes as four characters, so the overhead is arithmetic rather than opinion: 3.35 MB of images and fonts became 4.47 MB of text. Compression claws some of that back, but base64 compresses worse than the binary it replaced, so you never get to par.

Nothing can be cached separately. A browser caches by URL. When the font, the hero photograph and the copy all live at the same URL, they have one shared lifetime. Correct a typo and every returning visitor re-downloads every image. The reverse is worse: a visitor who reads two pages downloads the same font twice, because there is no shared file for the second page to reuse.

The document blocks itself. A stylesheet is render-blocking by design, and here the stylesheet contains the photographs. The parser reads megabytes of encoded text before it can paint a single pixel. Meanwhile the one thing that should be fetched early - the large image at the top of the page, which is almost always the Largest Contentful Paint element - cannot be preloaded or given a priority hint, because it is not a resource. It is a string.

Responsive images become impossible. srcset, <picture> and modern formats all need real files at real URLs. Inline the image and a phone on a slow connection downloads the same 1.9 MB photograph the desktop does. There is no smaller version to offer it.

Lazy loading stops meaning anything. loading="lazy" defers a fetch. There is no fetch to defer when the bytes are already inside the document you have finished downloading. Forty-one images below the fold cost their full weight before the first one is on screen.

And the file stops being editable. Most of those megabytes are single lines several hundred thousand characters long. Run a formatter over it once and the encoded data explodes across millions of lines, the diff becomes permanently unreadable, and version control stops being able to tell you what changed.

The version of this we would ship

Assets are files. That single rule removes every consequence above at once. On the interactive calculator we took over, moving four assets - one photograph and three fonts - out of the HTML and into an assets folder took the document from 415 KB to 43 KB. Same pixels, same behaviour, same code. The photograph and the fonts are now cached independently and survive every future copy edit.

The honest exception, because there is one: inlining a genuinely tiny asset is correct. A 2 KB file costs more in connection overhead than it saves in caching, and most build tools inline below a threshold of around 4 KB for exactly that reason. The technique is not the problem. The scale is. A 2 KB icon inline is a considered trade-off; a 1.9 MB photograph inline is the absence of one.

Fonts, where the same decision compounds

Fonts are where the single-file pattern does the most damage per kilobyte, and there were three separate issues in the files we read.

Uncompressed format. More than one file carried a 311 KB TTF, base64-encoded into the stylesheet. TTF is not compressed. WOFF2, which every browser in current use supports, is Brotli-compressed and is typically a fraction of the size for identical rendering. Converting costs nothing and changes nothing on screen.

No subsetting. A complete font contains glyphs for scripts your visitors will never see: Cyrillic, Greek, Vietnamese. Serving the whole thing to everyone is normal in generated code and unnecessary everywhere. unicode-range splits a font into subsets and the browser downloads only the ones the page’s text actually needs.

For Latvian this stops being an optimisation and becomes correctness. Ā, Č, Ē, Ģ, Ī, Ķ, Ļ, Ņ, Š, Ū and Ž are not in the latin subset - they live in latin-ext. Load only latin and the browser draws Latvian diacritics in a fallback font at a different weight and width, which produces the patchy, borrowed-letter look we wrote about at length in Latvian. It is one of the most common faults on Latvian sites and it is a one-line fix.

Two font strategies at once. One file self-hosted its display font as inline base64 and, in the same document, loaded its body font from the Google Fonts CDN. That pays both costs: the weight of the inlined font and a blocking third-party request on top.

The CDN half also carries a legal question in Europe. On 20 January 2022 the Munich Regional Court awarded a visitor EUR 100 in damages because a site’s Google Fonts embed transmitted their dynamic IP address to Google without consent (3 O 17493/20). One first-instance German judgment is not settled EU law, and the sum is trivial. The reasoning is not: a third-party request fires before the visitor has agreed to anything, and an IP address is personal data. Self-hosting removes the question and is faster anyway, so there is nothing to weigh.

The head nobody wrote

The second pattern is quieter and, for a campaign site, more expensive.

On the promotional site: no meta description. No canonical URL. No Open Graph or Twitter card tags at all. The <html> element declared Latvian and the <title> was in English.

This matters more than it sounds, because a promotional site’s entire distribution is people sending the link to each other. Paste a URL with no Open Graph tags into Messenger, WhatsApp, Slack or LinkedIn and you get a bare blue link - no image, no title, no description. Every share performs worse than it should, forever, and nothing in the browser ever tells you.

What we add, and it is a fixed list rather than a judgement call:

<meta name="description" content="…">
<link rel="canonical" href="https://example.com/page/">
<meta property="og:type" content="website">
<meta property="og:locale" content="lv_LV">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:title" content="…">
<meta property="og:description" content="…">
<meta property="og:image" content="https://example.com/assets/share.jpg">
<meta property="og:image:width" content="1920">
<meta property="og:image:height" content="1080">
<meta name="twitter:card" content="summary_large_image">

Absolute URLs in og:image, not relative ones, because the scraper fetching it has no page context. Explicit width and height, so the preview renders before the image finishes downloading. And a <title> in the language the lang attribute claims.

Headings as sizes rather than structure

The promotional site had four clearly distinct sections and exactly one heading element in the entire document: a single <h1>. No <h2>, no <h3>. Every section title was a styled <div> or <p>.

Visually this is indistinguishable from correct markup, which is the whole theme of this article. Structurally it is a page with no table of contents. A screen reader user navigating by heading - one of the primary ways blind users move around a page - finds one heading and then nothing. Search engines lose the outline that tells them what the page is about and how its parts relate.

Headings are not font sizes. They are the document’s structure, and CSS handles the sizes.

What was right, and it was a lot

An audit that only lists faults is a bad audit, and it would misrepresent what we actually read.

  • Every one of those 41 images had real, descriptive alternative text. Not filenames, not “image” - proper descriptions. Missing alternative text appears on 53.1% of the top million home pages. Every single one here was written.
  • Every external link carried rel="noopener". All of them.
  • Native elements were used natively. <details> and <summary> for a collapsible section instead of a JavaScript accordion. A real <select>. Real <button> elements.
  • The ARIA that was there was correct. aria-pressed on toggle buttons, aria-live="polite" on the panel that updates with results, aria-hidden on decorative icons. Correct ARIA is rarer than no ARIA.
  • prefers-reduced-motion was respected, which a large share of professionally built sites still skip.
  • Currency and number formatting used Intl.NumberFormat with the correct locale rather than string concatenation, so thousands separators and the euro symbol land where Latvian convention puts them.
  • The CSS was good. Custom properties, clamp() for fluid type, sensible grid, no framework, no jQuery. About 20 KB of code doing the entire job - a figure plenty of agency builds would struggle to match.

That list is the point, not a courtesy. This is not incompetent work. It is work that was checked in the one place a browser shows you, and not checked in the places it does not.

Different route, different failure

Everything above describes one route: you asked a general-purpose assistant for a web page and it handed you a page. That route produces the single-file, base64-everything artefact reliably, and for a structural reason - a chat window has no filesystem. There is nowhere to put a file, so nothing becomes a file.

The purpose-built app builders do not have that constraint and do not make that mistake. Ask Lovable, v0, Bolt.new or Replit for something and you get a real project: a component tree, a package.json, a build step, assets in folders. Lovable’s stack is React with Tailwind and shadcn/ui, TypeScript by default, and, per its own documentation, TanStack Start with server-side rendering for apps created from 13 May 2026, with older React and Vite projects prerendered on deployed URLs so crawlers and social preview bots see rendered content. Most of this article’s structural criticism simply does not apply to that output, and the widely repeated claim that these tools produce blank pages for search engines is now out of date - though it is worth checking which stack a given project is actually on, because the older one is still out there in quantity.

Their failure modes are somewhere else entirely, and they are more serious, because these tools do not just write the page. They provision the database.

The backend is where it bites. CVE-2025-48757, published 29 May 2025 and scored 9.3 critical, reads: “An insufficient database Row-Level Security policy in Lovable through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites.” The mechanism is a chain of sane-looking defaults. A new Postgres table in Supabase does not enforce row-level security unless somebody enables it. The generated schemas did not enable it. And the Supabase anon key ships to the browser by design, because that is how the client is meant to work. Each link is defensible; the combination means the database answers to anyone who opens developer tools. The researcher who disclosed it, Matt Palmer, reported 303 endpoints across 170 projects - around a tenth of those he scanned - with tables readable by unauthenticated requests.

Note the last line of the NVD entry: the CVE is disputed by the supplier, whose position is that customers are responsible for protecting their application data. Whatever you make of that as a commercial stance, it is this article’s argument stated by the vendor. The tool builds it. Somebody still has to know what to check.

And the operational end. In July 2025 Replit’s agent deleted a production database during a declared code freeze, taking live records for over 1,200 companies with it, then reported that a rollback was impossible when it was not. Replit shipped automatic development and production separation and a planning-only mode within days, and the response was the right one. The lesson is not that the tool is bad. It is that an agent with write access to production is an operational decision, and it was made by somebody who did not know they were making it.

So the question to ask depends on the route:

  • Chat-window HTML - what does the page weigh, what can be cached, and is there anything in the document head?
  • App builder - who can read the database, does a crawler see the content, and does anything with write access point at production?

Neither route is unsafe, and neither is a reason not to build this way. Both need somebody who knows which of those questions applies.

What the wider numbers say

Our own sample is small and we are not going to pretend otherwise. The industry-scale measurements point the same way.

The WebAIM Million for 2026 found detectable WCAG 2 failures on 95.9% of the top one million home pages, up from 94.8% a year earlier, averaging 56.1 errors per page, up 10.1%. After six consecutive years of slow improvement the trend reversed. WebAIM attributes the reversal to increased reliance on third-party frameworks and libraries and to “automated or AI-assisted coding practices (‘vibe coding’)”.

Their six most common failures account for 96% of everything detected: low contrast text (83.9%), missing alternative text (53.1%), missing form input labels (51%), empty links (46.3%), empty buttons (30.6%) and missing document language (13.5%). Every one is invisible to someone checking by looking at the page.

HTTP Archive’s 2025 data puts the median mobile page at 2.56 MB, up 8.4% in a year, with images the largest contributor. Page weight has roughly tripled in a decade.

And since 28 June 2025, Directive (EU) 2019/882, the European Accessibility Act, has applied to e-commerce, banking, transport ticketing, telecoms and audiovisual services sold in the EU, with WCAG 2.1 AA via EN 301 549 as the practical standard. Microenterprises providing services - under 10 staff and under EUR 2 million - are exempt on the services side. Most of the businesses asking us to host their generated site are not exempt, and a heading structure of one <h1> and nothing else is not a WCAG 2.1 AA page.

Five minutes, no developer required

Run these on your own site before you run them on anyone else’s.

  1. View the source. Right-click the page, choose View page source, and search for base64. A short match is fine - that is a build tool inlining something tiny. A match that runs for hundreds of lines is a photograph that should have been a file.
  2. Read the network total. Open developer tools, go to Network, reload the page. Two numbers matter: the transferred total at the bottom, and the size of the very first row, which is the HTML document. The document should be tens of kilobytes. If it is megabytes, everything is inlined.
  3. Share the link with yourself. Paste the URL into any chat app. If no image, title and description appear, the Open Graph tags are missing and every share you have ever sent looked like that.
  4. Look at the browser tab. The title should be in the site’s language, describe the page, and not be a placeholder.
  5. Reload on a throttled connection. In the Network tab, set throttling to Slow 4G and reload. This is the experience of a real visitor on mobile data, and it is the only honest way to feel what an inlined 1.9 MB photograph costs.

The same checklist, run on this site

It would be a poor article that set a standard the author does not hold. Across all 164 pages this site builds:

Every page carries a meta description, a canonical URL, Open Graph tags, a lang attribute and exactly one <h1>. Every <img> element has alternative text. Fonts are self-hosted WOFF2, split by unicode-range with latin-ext included, so Latvian diacritics are drawn by the real font rather than a fallback. No public page makes a request to Google Fonts or to any other third-party asset host. Below-the-fold images are deferred; portfolio covers reserve their space with aspect-ratio so nothing moves as they load.

There is exactly one base64 asset in our compiled CSS, and it is there deliberately: a 2,028-byte WOFF2 subset that the build tool inlines because it falls under its 4 KB threshold. At that size the round trip costs more than the cache saves. That is precisely the distinction this article turns on - the technique is sound, and scale is what turns it into a fault.

The actual lesson

Generating a site is a reasonable thing to do. The output can be good, and the output we have read was, in the places its author could see.

The problem is that a browser window is a very poor test instrument. It will not tell you that your visitor is downloading 4.7 MB, that nothing is cached, that your links share as bare URLs, that your page has one heading, or that a font request is sending an IP address to a third party. All of that is legible in thirty seconds to somebody who opens the file, and completely invisible to somebody who does not.

So: generate the site. Then have someone read it. That is a couple of hours, not a rebuild, and it is the difference between a page that looks finished and one that is.

If you have a site that was generated and never reviewed, that read-through is a service we sell on its own - the output is a written list like the one above, whether or not we do the fixing.

Tell us what’s broken.
We’ll tell you the truth.

Book a free call →
Reply within one business day · EN / LV
↑↓ navigate · ↵ open · esc close