feat: SvelteKit integration (aeo.js/sveltekit)#64
Conversation
aeo.js/sveltekit follows the Angular plugin pattern: postBuild() scans the build output (adapter-static's build/, or .svelte-kit/output for other adapters' prerendered pages), generates all AEO files alongside it, and injects the widget into every prerendered page. generate() discovers routes from src/routes (group segments transparent, dynamic segments skipped) and writes into static/ for dev use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Docs PreviewPreview URL: https://feat-sveltekit-plugin.aeojs.pages.dev This preview was deployed from the latest commit on this PR. |
Greptile SummaryThis PR ships the
Confidence Score: 3/5The SvelteKit plugin has a functional gap: widget injection silently does nothing for any project that isn't using adapter-static, which is a common deployment target. Three issues in the core plugin logic affect real users: widget injection never fires for non-static adapter builds, caller overrides via config.pages are silently discarded, and unescaped </script> in the serialized config can break the injected script tag. src/plugins/sveltekit.ts — the widget injection call site (line 254), the page-merge loop (lines 221–228), and getWidgetScript (lines 134–139) all need attention before this is production-ready. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[postBuild called] --> B{config.outDir set?}
B -- yes --> C[Use config.outDir]
B -- no --> D[detectSvelteKitOutputDir]
D --> E{build/ exists?}
E -- yes --> F[outputDir = build/]
E -- no --> G{.svelte-kit/output/client exists?}
G -- yes --> H[outputDir = .svelte-kit/output/client]
G -- no --> F
C --> I[scanHtmlOutput outputDir]
F --> I
H --> I
I --> J[buildPages]
H --> K{outputDir contains .svelte-kit?}
K -- yes --> L[scanHtmlOutput prerenderedDir]
K -- no --> M[prerenderedPages = empty]
L --> N[prerenderedPages]
J --> O[discovered = buildPages + prerenderedPages]
N --> O
M --> O
O --> P[scanSvelteKitRoutes src/routes]
P --> Q[sourcePages]
Q --> R[Merge: discovered first, sourcePages second, config.pages last]
R --> S[generateAEOFiles]
S --> T[Write robots.txt / llms.txt / sitemap.xml / ...]
T --> U{injectWidget !== false AND widget.enabled?}
U -- yes --> V[injectWidgetIntoHtml outputDir only]
V --> W[For non-static adapters: outputDir = client dir with no HTML — prerenderedDir never walked]
U -- no --> X[Done]
Prompt To Fix All With AIFix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
src/plugins/sveltekit.ts:253-258
**Widget injection silently skipped for non-static adapters**
When a non-static adapter is used, `outputDir` is `.svelte-kit/output/client` — a directory that contains JS/CSS bundles but no HTML files. `injectWidgetIntoHtml` walks only that directory, so it will never find a `</body>` tag to inject into. The actual prerendered HTML lives in `prerenderedDir` (`.svelte-kit/output/prerendered/pages`), but that path is never passed to the injector. A non-static-adapter user who expects widget injection will see `injected = 0` with no warning and no indication anything is wrong.
### Issue 2 of 4
src/plugins/sveltekit.ts:221-228
**`config.pages` user overrides are silently dropped**
`config.pages` is placed last in `allPages`, but the merge condition `!existing || (page.content && !existing.content)` means a user-provided entry can only win if it carries content AND the auto-discovered entry does not. If the HTML scanner already found a page (with content) at the same pathname, the user's explicit entry is ignored entirely with no warning. This makes it impossible for callers to supply corrections for discovered pages (e.g., a better description or a canonical title for a prerendered route), which is the primary reason a caller would pass `config.pages` to `postBuild`.
### Issue 3 of 4
src/plugins/sveltekit.ts:134-139
`JSON.stringify` does not escape the `</script>` sequence inside a JSON string. If `config.title`, `config.description`, or `config.url` contains `</script>`, the browser will terminate the script element early, breaking the injection and potentially exposing whatever follows in the page as raw HTML. Escaping `</` to `<\/` (safe in both JSON and JS) closes this gap.
```suggestion
const widgetConfig = JSON.stringify({
title: resolvedConfig.title,
description: resolvedConfig.description,
url: resolvedConfig.url,
widget: resolvedConfig.widget,
}).replace(/\//g, '\\/');
```
### Issue 4 of 4
src/plugins/sveltekit.ts:78-81
`detectSvelteKitOutputDir` returns `build/` as a fallback even when that directory does not exist. Projects that have never been built or use adapters that write elsewhere will silently generate AEO files in a `build/` the adapter never owns. A warning here would surface the misconfiguration early.
```suggestion
const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client');
if (existsSync(clientDir)) return clientDir;
console.warn(
'[aeo.js] Could not detect SvelteKit output directory (build/ and .svelte-kit/output/client are absent). ' +
'Pass outDir explicitly or run your build first.',
);
return staticBuild;
```
Reviews (1): Last reviewed commit: "docs: SvelteKit framework guide" | Re-trigger Greptile |
| if (config.injectWidget !== false && resolvedConfig.widget.enabled) { | ||
| const injected = injectWidgetIntoHtml(outputDir, config); | ||
| if (injected > 0) { | ||
| console.log(`[aeo.js] Injected widget into ${injected} page(s)`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Widget injection silently skipped for non-static adapters
When a non-static adapter is used, outputDir is .svelte-kit/output/client — a directory that contains JS/CSS bundles but no HTML files. injectWidgetIntoHtml walks only that directory, so it will never find a </body> tag to inject into. The actual prerendered HTML lives in prerenderedDir (.svelte-kit/output/prerendered/pages), but that path is never passed to the injector. A non-static-adapter user who expects widget injection will see injected = 0 with no warning and no indication anything is wrong.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/sveltekit.ts
Line: 253-258
Comment:
**Widget injection silently skipped for non-static adapters**
When a non-static adapter is used, `outputDir` is `.svelte-kit/output/client` — a directory that contains JS/CSS bundles but no HTML files. `injectWidgetIntoHtml` walks only that directory, so it will never find a `</body>` tag to inject into. The actual prerendered HTML lives in `prerenderedDir` (`.svelte-kit/output/prerendered/pages`), but that path is never passed to the injector. A non-static-adapter user who expects widget injection will see `injected = 0` with no warning and no indication anything is wrong.
How can I resolve this? If you propose a fix, please make it concise.| const allPages = [...discovered, ...sourcePages, ...(config.pages || [])]; | ||
| const pageMap = new Map<string, PageEntry>(); | ||
| for (const page of allPages) { | ||
| const existing = pageMap.get(page.pathname); | ||
| if (!existing || (page.content && !existing.content)) { | ||
| pageMap.set(page.pathname, page); | ||
| } | ||
| } |
There was a problem hiding this comment.
config.pages user overrides are silently dropped
config.pages is placed last in allPages, but the merge condition !existing || (page.content && !existing.content) means a user-provided entry can only win if it carries content AND the auto-discovered entry does not. If the HTML scanner already found a page (with content) at the same pathname, the user's explicit entry is ignored entirely with no warning. This makes it impossible for callers to supply corrections for discovered pages (e.g., a better description or a canonical title for a prerendered route), which is the primary reason a caller would pass config.pages to postBuild.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/sveltekit.ts
Line: 221-228
Comment:
**`config.pages` user overrides are silently dropped**
`config.pages` is placed last in `allPages`, but the merge condition `!existing || (page.content && !existing.content)` means a user-provided entry can only win if it carries content AND the auto-discovered entry does not. If the HTML scanner already found a page (with content) at the same pathname, the user's explicit entry is ignored entirely with no warning. This makes it impossible for callers to supply corrections for discovered pages (e.g., a better description or a canonical title for a prerendered route), which is the primary reason a caller would pass `config.pages` to `postBuild`.
How can I resolve this? If you propose a fix, please make it concise.| const widgetConfig = JSON.stringify({ | ||
| title: resolvedConfig.title, | ||
| description: resolvedConfig.description, | ||
| url: resolvedConfig.url, | ||
| widget: resolvedConfig.widget, | ||
| }); |
There was a problem hiding this comment.
JSON.stringify does not escape the </script> sequence inside a JSON string. If config.title, config.description, or config.url contains </script>, the browser will terminate the script element early, breaking the injection and potentially exposing whatever follows in the page as raw HTML. Escaping </ to <\/ (safe in both JSON and JS) closes this gap.
| const widgetConfig = JSON.stringify({ | |
| title: resolvedConfig.title, | |
| description: resolvedConfig.description, | |
| url: resolvedConfig.url, | |
| widget: resolvedConfig.widget, | |
| }); | |
| const widgetConfig = JSON.stringify({ | |
| title: resolvedConfig.title, | |
| description: resolvedConfig.description, | |
| url: resolvedConfig.url, | |
| widget: resolvedConfig.widget, | |
| }).replace(/\//g, '\\/'); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/sveltekit.ts
Line: 134-139
Comment:
`JSON.stringify` does not escape the `</script>` sequence inside a JSON string. If `config.title`, `config.description`, or `config.url` contains `</script>`, the browser will terminate the script element early, breaking the injection and potentially exposing whatever follows in the page as raw HTML. Escaping `</` to `<\/` (safe in both JSON and JS) closes this gap.
```suggestion
const widgetConfig = JSON.stringify({
title: resolvedConfig.title,
description: resolvedConfig.description,
url: resolvedConfig.url,
widget: resolvedConfig.widget,
}).replace(/\//g, '\\/');
```
How can I resolve this? If you propose a fix, please make it concise.| const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client'); | ||
| if (existsSync(clientDir)) return clientDir; | ||
|
|
||
| return staticBuild; |
There was a problem hiding this comment.
detectSvelteKitOutputDir returns build/ as a fallback even when that directory does not exist. Projects that have never been built or use adapters that write elsewhere will silently generate AEO files in a build/ the adapter never owns. A warning here would surface the misconfiguration early.
| const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client'); | |
| if (existsSync(clientDir)) return clientDir; | |
| return staticBuild; | |
| const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client'); | |
| if (existsSync(clientDir)) return clientDir; | |
| console.warn( | |
| '[aeo.js] Could not detect SvelteKit output directory (build/ and .svelte-kit/output/client are absent). ' + | |
| 'Pass outDir explicitly or run your build first.', | |
| ); | |
| return staticBuild; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/sveltekit.ts
Line: 78-81
Comment:
`detectSvelteKitOutputDir` returns `build/` as a fallback even when that directory does not exist. Projects that have never been built or use adapters that write elsewhere will silently generate AEO files in a `build/` the adapter never owns. A warning here would surface the misconfiguration early.
```suggestion
const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client');
if (existsSync(clientDir)) return clientDir;
console.warn(
'[aeo.js] Could not detect SvelteKit output directory (build/ and .svelte-kit/output/client are absent). ' +
'Pass outDir explicitly or run your build first.',
);
return staticBuild;
```
How can I resolve this? If you propose a fix, please make it concise.
Adds the SvelteKit plugin — the types and detection already anticipated it (
FrameworkTypeincludessveltekit, tsup externals include@sveltejs/kit); this ships the actual integration.What
aeo.js/sveltekitnew entry point following the Angular plugin pattern:postBuild()— detects the output dir (build/for adapter-static,.svelte-kit/output+ prerendered pages for other adapters), scans prerendered HTML for content, generates all AEO files alongside the build, and injects the Human/AI widget into every prerendered page (idempotent, opt-out viainjectWidget: false).generate()— source-only mode scanningsrc/routes(+page.svelte/+page.md/+page.svx; route groups(name)transparent, dynamic[slug]segments skipped), writing intostatic/.getWidgetScript()— same surface as the Angular plugin.exportsmap wiringframeworks/sveltekitguide + sidebar entry + README quick start & table rowVerification
tsc --noEmitclean, build succeeds with the new entry🤖 Generated with Claude Code