NON_PAGE_DIRS is missing emails, so a framework convention directory of email templates is scanned as pages, routed, served, and statically built to HTML.
What happens
A stacks app's views tree ships an emails/ directory as a first-class convention, alongside the things that genuinely are pages:
$ ls storage/framework/defaults/resources/views/
cart.stx checkout/ coming-soon.stx dashboard/ emails/ errors/ index.stx orders/Regenerating the route table in a real app puts the email templates in it:
# .stx/routes.ts
+ { pattern: '/emails/welcome', filePath: 'storage/framework/defaults/resources/views/emails/welcome.stx', isDynamic: false, params: [] },
# .stx/route-types.d.ts
+ '/emails/welcome': { }So GET /emails/welcome is a page, navigate('/emails/welcome') type-checks, and an SSG build emits it as HTML.
Cause
packages/router/src/page-routes.ts:24:
export const NON_PAGE_DIRS: readonly string[] = ['components', 'layouts', 'partials']That list gates all three places a non-page directory is excluded — the scan-time skip in file-router.ts:28, and the manifest and route-type filters in codegen.ts:22 and :84. An emails/ directory matches none of them, so it flows through every stage as if it were a section of the site.
This is the failure mode the module was written to prevent
From file-router.ts:25-27, unchanged:
Building blocks, not pages. Skipped at scan time so they are absent from the route table itself — otherwise they get served, statically built to HTML, and listed in sitemap.xml as public URLs (#1866).
That is exactly what emails/ does now. The #1866 fix enumerated three directories and stopped there; emails/ is the same category and was missed.
Why it matters
An email template is not a page. Rendered as one it is a crawlable public URL whose body is the message layout — unsubscribe and preference links, any support or internal addresses in the footer, the reset/verify flow's copy and its link structure, and whatever placeholder scaffolding the template carries. None of that is per-user data, so this is not a data leak; it is unintended public surface, indexed, that no one chose to publish and that reveals the shape of flows an app may not want enumerated.
It also degrades the route types: RouteMap gains entries for URLs that are not navigable destinations.
Fix
Add emails to NON_PAGE_DIRS. Everything downstream — scan skip, manifest filter, type filter — already keys off that one list, so it is a one-line change.
Worth deciding on errors/ in the same pass. It is the other convention directory in that tree, and unlike emails/ there is a real argument for it staying routable (a directly-reachable /errors/404), so it should be a deliberate call rather than another oversight.
packages/router/test/page-routes.test.ts:67-69 asserts the list contents item by item and is the natural place to pin whatever is decided.
Found when a routine route-table regeneration in a production app added /emails/welcome on its own.