index.html shell is served for every route and the page is rendered in the browser. That is the right shape for an internal dashboard, but a crawler that doesn’t execute JavaScript sees an empty document — no title, no copy, no links. Most non-Google crawlers, including the ones behind social link previews, fall in that category.
Two things gate indexing, and an app needs both:
- Real HTML per URL, which you get by prerendering at build time from your app’s own build script.
- Your own domain. Cargo-owned hostnames — an app’s default hostname and every per-deployment preview URL — answer with
X-Robots-Tag: noindex, which keeps them out of search results. Attaching a custom domain is the opt-in.
Apps built on
CargoRefineApp from @cargo-ai/app-sdk require a Cargo login,
and anything behind a login can’t be indexed at all. A public app skips
CargoRefineApp and renders its own tree — nothing at the platform level
requires authentication.Supported frameworks
Cargo automatically detects your frontend framework frompackage.json and configures the build accordingly:
If no framework is detected, Cargo defaults to Vite conventions (
dist output, VITE_ prefix).
SSR-first frameworks
Frameworks like Next.js, Remix, SvelteKit, and Nuxt are server-side rendering frameworks by default, and Cargo doesn’t detect them: an app using one gets the no-framework defaults, so the build output is read fromdist/. Cargo hosts static sites only, so the app needs a build script that produces a static export and leaves it in dist/:
- Next.js: add
output: 'export'tonext.config.js. The export lands inout/, so move it — for example"build": "next build && mv out dist". - Nuxt: use
nuxt generateinstead ofnuxt build, and make sure its output ends up indist/. - SvelteKit: use
@sveltejs/adapter-staticwith itspagesandassetsoptions set todist. - Remix: use a static adapter configured to write to
dist/.
dist/ fails with an error naming the directory. Cargo still recognizes these frameworks’ env var prefixes (NEXT_PUBLIC_, NUXT_PUBLIC_, PUBLIC_), so environment variables work as expected.
Prerender from your build script
On deploy, Cargo runsnpm ci --ignore-scripts and then your app’s build script whenever it does more than vite build, so a prerender step in that script runs server-side as part of the build:
package.json
build script that is only a plain Vite build — vite build, or create-vite’s tsc && vite build and tsc -b && vite build — keeps the default build, exactly as if it weren’t declared, and the build log says so. Apps without a build script get the detected framework’s default build command (npx vite build for Vite and for undetected frameworks, npx astro build for Astro, and so on).
Your script also runs with the platform and app environment variables in its process environment, so a prerender step running as a plain Node script can read them; the default build reads them from .env.production instead.
Two rules hold either way:
- Output has to land in the detected framework’s output directory (
dist/unless the table above says otherwise), andindex.htmlmust exist. The build fails with a user-facing error otherwise, since the app’s routes fall back to that shell. - Your build script owns the whole build. Cargo doesn’t run the framework build before or after it, so the script needs to produce the client bundle too — not just the prerendered HTML.
VITE_CARGO_API_URL, a Next.js app reads NEXT_PUBLIC_CARGO_API_URL, and so on. Your own environment variables must also use a recognized prefix to be exposed to the build.
If your
build script does more than vite build — a different --mode, a
custom output directory, a lint step — that now runs on deploy where it
previously didn’t. A failed build leaves the currently live deployment
serving, and the build log shows what broke.Use .html URLs for prerendered routes
The edge router serves any path with a file extension directly from the build, and falls back to index.html for everything else so client-side routing keeps working. That means:
/about.html— served as the prerendered file. Crawlable./about— rewritten toindex.html, i.e. the client-rendered shell.
/about.html. Configure Vite for multiple entry points and link between pages with real <a href> tags:
vite.config.ts
Ship robots.txt and a sitemap
Anything in Vite’s public/ directory is copied verbatim into the build and served at the root. Nothing extra is needed:
public/robots.txt
Cache-Control: no-cache so they revalidate on every request and a promote takes effect immediately. Hashed assets under assets/ are the only files cached long-term.
Put the metadata in the prerendered head
Cargo servesindex.html exactly as your build produced it — no tags are injected. Every prerendered page needs its own title, description, canonical URL, and Open Graph tags:
about.html
Serve it on your own domain
An app’s default hostname is a subdomain Cargo owns, and Cargo serves those withX-Robots-Tag: noindex. So a custom domain isn’t a nice-to-have here — it’s what makes the app indexable at all, and it puts the content on a domain whose authority is yours:
_cargo-verify.<hostname> TXT record that proves you control the domain, validation records for the certificate, and a cnameTarget to point the hostname at. Add them, then POST /v1/hosting/custom-domains/<uuid>/refresh-status until the status reaches active. Nothing is served on the hostname until the TXT record resolves. The TXT value is unique to this attachment, so a record left over from an earlier one won’t match, and you can delete it once the domain is active.
Hostnames need at least three DNS labels, so attach www.example.com rather than the bare example.com.
Every deployment also gets a permanent preview URL
(
deployment-<uuid>.app.getcargo.run) serving an identical copy of the site.
Those
carry the same noindex as the default hostname, so they can’t compete with
your domain as duplicates. Point <link rel="canonical"> at your custom
domain anyway: it’s what consolidates a URL that gets shared or linked from
more than one host.Get it discovered
Indexable HTML on your own domain makes the site eligible, and that’s all it does. Cargo doesn’t announce it: no search engine is pinged, no sitemap is submitted for you. A new domain that nothing links to can stay uncrawled indefinitely, so one of these has to happen:- Submit the sitemap. In Google Search Console, add the domain as a property, verify it with the DNS
TXTrecord, and submithttps://www.example.com/sitemap.xml. URL Inspection also requests indexing for one URL at a time, which is worth doing for the homepage. Bing Webmaster Tools is the equivalent if Bing matters to you. - Get one link from a page that’s already crawled. A marketing site, a docs page, a public repo — anything already in the index gives a crawler a path to the new domain.
curl -sSI https://www.example.com/ confirms the response carries no X-Robots-Tag, but only Search Console’s page indexing report answers whether a URL is actually in the index.
Checklist
- Public app that doesn’t require a Cargo login
- A
buildscript inpackage.jsonthat prerenders (so it does more thanvite build) - Routes linked as
.htmlpaths and listed in the sitemap public/robots.txtallowing crawlers, pointing at the sitemap- Per-page title, description, canonical, and Open Graph tags in the prerendered head
- Custom domain attached and
active— required, since the default hostname isnoindex— and every canonical pointing at it - Sitemap submitted in Google Search Console, since nothing indexes a site it hasn’t discovered

