Skip to main content
Hosted apps are single-page apps: the same 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:
  1. Real HTML per URL, which you get by prerendering at build time from your app’s own build script.
  2. 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.
This page is about apps. A worker serves whatever headers its code sets, so if a worker returns HTML you want indexed, that’s yours to control.
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 from package.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 from dist/. 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' to next.config.js. The export lands in out/, so move it — for example "build": "next build && mv out dist".
  • Nuxt: use nuxt generate instead of nuxt build, and make sure its output ends up in dist/.
  • SvelteKit: use @sveltejs/adapter-static with its pages and assets options set to dist.
  • Remix: use a static adapter configured to write to dist/.
A build that finishes without creating 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 runs npm 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
There is nothing to opt into beyond the script itself. A 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), and index.html must 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.
Platform environment variables are available with each supported prefix — a Vite app reads 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 to index.html, i.e. the client-rendered shell.
So a prerendered route has to be linked and submitted as /about.html. Configure Vite for multiple entry points and link between pages with real <a href> tags:
vite.config.ts
A hosted app never returns a 404. Extension-less deep links (/about) resolve to the SPA shell, so an unknown path like /pricing serves that shell with a 200; a request for a file that doesn’t exist (/logo.png) serves Cargo’s own “App not found” page with a 200, on your domain. Both are soft-404s. Cargo’s fallback page carries <meta name="robots" content="noindex"> itself, but the SPA shell is your page — if your app renders a “not found” view client-side, set the same tag on it from the client so those paths stay out of the index.

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
Both are served with 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 serves index.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 with X-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:
The response carries the DNS records to add: a _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 TXT record, and submit https://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.
Then wait. Discovery takes days to weeks, and crawled is not the same as indexed — Google decides, and thin pages often stay out. 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 build script in package.json that prerenders (so it does more than vite build)
  • Routes linked as .html paths and listed in the sitemap
  • public/robots.txt allowing 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 is noindex — and every canonical pointing at it
  • Sitemap submitted in Google Search Console, since nothing indexes a site it hasn’t discovered