Two separate defects that compound: the SSG ignores partialsDir, and a failed @include doesn't fail the build. Together they let an app ship broken static HTML for weeks while buildApp() reports total success.
Found in a 124-template app whose entire pre-rendered output — header, footer, hero, CTA, pricing, every layout include — was replaced by error text. dist/index.html was a 6KB shell; after the fix, 95KB.
1. generateStaticSite never reads partialsDir
$ grep -n "partialsDir" packages/stx/src/ssg.ts
$ # no outputCompare its siblings, which the SSG does forward:
ssg.ts:1065 const middlewareDir = (stxConfig as any)?.routeMiddleware?.dir || 'middleware'
ssg.ts:1076 generateLayoutTypes((stxConfig as any)?.layoutsDir, stateDir(process.cwd()))ssg.ts:979 loads the user config specifically for this purpose — the comment says "Load user's stx.config.ts for proper layoutsDir, componentsDir, etc." — and partialsDir is the one that doesn't make it through. SSGConfig has no such field, so buildApp() can't pass it either:
// build.ts
ssgResult = await generateStaticSite({
pagesDir: path.join(config.root || '.', config.pagesDir || config.build?.pagesDir || 'pages'),
publicDir: …, outputDir: …, generate404: …, minify: …, cleanOutput: …,
...options.ssg
})So includes.ts:467 always falls back:
let partialsDir = options.partialsDir || path.join(path.dirname(filePath), 'partials')Net effect: in a static build, @include always resolves against the default partials directory, whatever the config says. An app that keeps partials anywhere else has every include fail. Verified by setting partialsDir both project-relative and resources-relative in the config the loader actually reads — no change to the resolved path either way.
componentsDir has the same gap, and the comment at ssg.ts:979 names it explicitly as a reason for loading the config — but it appears nowhere else in the file:
$ grep -n "componentsDir" packages/stx/src/ssg.ts
979: // Load user's stx.config.ts for proper layoutsDir, componentsDir, etc.So of the three directory keys that comment is justifying, only layoutsDir is actually consumed.
2. A failed @include doesn't fail the build
This is the part that made it invisible for weeks. includes.ts:889 turns the failure into createDetailedErrorMessage(...) and returns it as the include's rendered output, so the error is spliced into the page where the markup should be:
<div … >╭─────────…─────────╮ File: default.stx Message: Error loading include file
Bench/ConfirmHost: ENOENT: no such file or directory, open
'…/resources/partials/Bench/ConfirmHost.stx' …ANSI escape codes and all, in shipped HTML. Meanwhile:
Total: 37 pages
Success: 37
Failed: 015 failed includes on the home page alone, and the build's exit code is 0. Nothing in lint, typecheck, or the build output can see it — the only way to find it is to read the emitted HTML, which is exactly what nobody does when the build says success.
I'd argue an unresolvable include is not a recoverable condition in a build. In dev, rendering the error in place is genuinely useful; in SSG it produces a file you would never knowingly deploy. Suggestion: count include failures per page and fail the run (or at minimum report them in the summary and exit non-zero), with an opt-out for anyone who wants the current behaviour. Even just a line in the summary would have made this a five-minute fix instead of a three-week silent outage.
Repro
app/
stx.config.ts # partialsDir: 'components'
resources/
components/Thing.stx
layouts/default.stx #
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3041:29: include error: Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[1m[31m╭──────────────────────────────────────────────────────────────╮[0m
[1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m
[1m[31m╰──────────────────────────────────────────────────────────────╯[0m
[36m[1mFile:[0m [2mapp.stx[0m [90m(line 3041, col 29)[0m
[33m[1mMessage:[0m Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[34m[1mContext:[0m
[2m 3039: resources/
3040: components/Thing.stx
> 3041: layouts/default.stx #
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3042:864: include error: Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[1m[31m╭──────────────────────────────────────────────────────────────╮[0m
[1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m
[1m[31m╰──────────────────────────────────────────────────────────────╯[0m
[36m[1mFile:[0m [2mapp.stx[0m [90m(line 3042, col 864)[0m
[33m[1mMessage:[0m Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[34m[1mContext:[0m
[2m 3040: components/Thing.stx
3041: layouts/default.stx #
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3069:63: include error: Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[1m[31m╭──────────────────────────────────────────────────────────────╮[0m
[1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m
[1m[31m╰──────────────────────────────────────────────────────────────╯[0m
[36m[1mFile:[0m [2mapp.stx[0m [90m(line 3069, col 63)[0m
[33m[1mMessage:[0m Error loading include file Thing: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/Thing.stx'
[34m[1mContext:[0m
[2m 3067: title="+1"
3068: >
> 3069:
3070:
3071: [0m
> 3042: views/index.stx # @extends('layouts/default')bun run build → reports success; dist/index.html contains the ENOENT banner instead of Thing's markup. Point partialsDir anywhere you like; the resolved path stays <viewsRoot>/partials.
Workaround
Symlink the default location at the real one:
ln -s components resources/partialsWhich works, but is a filesystem workaround for a config key that reads as though it's supported — and silently re-breaks the entire static build the moment someone tidies it away.