Summary
The SSG's sitemap generator has three gaps that compound: the domain defaults to http://localhost and is never read from the app's site config; SSGConfig has no exclusion option of any kind, so every generated route — auth-walled pages, partials, anything — is advertised; and no robots.txt is produced on this build path even though stx contains two robots generators.
Repro
A two-page fixture (index.stx owning a document, login.stx a fragment), built with the shipped CLI:
$ stx build --pages resources/views --out dist
Generated sitemap.xml with 2 URLs
$ cat dist/sitemap.xml
<url><loc>http://localhost/</loc>…</url>
<url><loc>http://localhost/login</loc>…</url>
$ ls dist
404.html index.html login.html sitemap.xml # no robots.txtImpact (real app)
analyticshq declares url: 'https://analyticshq.org' in site.config.ts:7. Every one of the 34 entries in its dist/sitemap.xml is http://localhost/…, and the list includes /account, /dashboard, /login, /register, /sites/new, plus /components/site-nav and /components/site-footer — partials enumerated as public pages.
The app's config/ui.ts:51-53 documents the exclusion half:
Route discovery and the SSG have no exclusion for layout directories, so a layout under
resources/views/is a public URL that gets built to HTML and listed in the sitemap.
Its response was structural: delete resources/views/layouts/ so layouts cannot be routed. For robots it fell back to hand-authoring, and said why in the file (public/robots.txt:8-9):
stx ships a robots generator (
site-builder/robots.js) but it only runs from the site-builder build path, which this app does not execute — so this file is authored.
That is 26 hand-written lines with explicit Disallow for /dashboard /account /login /register /sites/ /api/ /payments/, plus robots: 'noindex, nofollow' added to each auth-walled page's useSeoMeta (account.stx:6, sites/new.stx:6, 404.stx:22) as a second line of defence.
Evidence in stx
packages/stx/src/ssg.ts
:874const stxConfig = await loadStxConfig()— the config is loaded…:881domain: options.domain || 'http://localhost'— …but no site URL is read from it, and nothing falls back tosite.config.:1258const sitemap = generateSitemap(sitemapEntries, cfg.domain)grep -n 'exclude' packages/stx/src/ssg.ts→ no matches.SSGConfig(:60-120) hassitemap,rss,generate404,trailingSlash… and no way to omit a route.- Robots generators exist but not here:
packages/stx/src/site-builder/robots.ts:4(generateRobots, site-builder path only) andpackages/stx/src/seo.ts:847(generateRobotsTxt, exported, called by nothing inssg.ts).
The caller compounds it: Stacks builds views with bunx --bun @stacksjs/stx build --pages resources/views --out dist (storage/framework/core/actions/src/build/views.ts:21) and passes no --domain, even though the CLI accepts one (packages/stx/bin/cli.ts:982).
Prior art
@nuxtjs/sitemaptakessiteUrlandexcludepatterns;robots.txtcomes from@nuxtjs/robotson the same build.- Next.js
app/sitemap.tsandapp/robots.tsare both first-class conventions fed by one config, generated in the same pass. spatie/laravel-sitemaprequires an explicit base URL — there is no localhost default to fall into.
Ask
- Default
cfg.domainfrom the loaded site config (site.url/app.url) before falling back tohttp://localhost, and warn loudly when the fallback is used in a production build. - Add
sitemapExclude?: (string | RegExp)[](and honourdefinePageMeta({ sitemap: false })/useSeoMeta({ robots: 'noindex' })) so a page can keep itself out. - Exclude non-page directories from route discovery by default —
components/,partials/,layouts/underpagesDirshould never become public URLs. - Generate
robots.txton the SSG path from the same config that drives the sitemap, reusinggenerateRobotsTxt(seo.ts:847), and write the sitemap URL into it.
Related: #1821