Two Nuxt-config parity gaps, surfaced by mapping a real Nuxt app (app.head.script for Contentsquare/Fathom + vite.server.proxy) onto stx.
1. app.head now injects on already-shelled / SSG pages
ensureDocumentShell only populated config app.head when it built the shell. A page whose layout already renders <html><head> — the common case, and what SSG/static builds emit — skipped that, so a site-wide analytics <script> (Fathom / Contentsquare / ts-analytics) silently never injected on static builds. (This is the gap that made tsAnalytics() need a companion tsAnalyticsTag().)
New injectConfigHeadTags() injects the config head (script/meta/link/headRaw — not the merged runtime head; renderHead still owns useHead) into the existing <head>, idempotently — deduped by script src, link href, meta name/property/charset/http-equiv, and inline content. title is left to the page.
Verified: fragment still works; full-doc pages now get the script; exactly one tag (no double); layout's own charset not duplicated; re-running is a no-op.
2. dev-server server.proxy (Vite-style)
Forward matching paths to a backend before routing, so an stx app can hit a separate API in dev without CORS:
// config/stx.ts
server: { proxy: { '/api': 'http://localhost:8000' } }
// or per-rule:
server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, rewrite: p => p.replace('/api','') } } }Keys match by path prefix; full path+query forwarded (or rewrite(path)), changeOrigin sets the Host header. Closes the gap vs Nuxt's vite.server.proxy / nitro.devProxy. (HTTP only for now — WS proxying is a follow-up.)
Verified end-to-end against a live backend: GET path+query, POST body, changeOrigin, rewrite, and non-matching pass-through.
Tests
test/shell/config-head-existing-shell.test.ts (6 cases). Full suite green (9465 pass).