Skip to content

feat: SvelteKit integration (aeo.js/sveltekit)#64

Open
rubenmarcus wants to merge 2 commits into
mainfrom
feat/sveltekit-plugin
Open

feat: SvelteKit integration (aeo.js/sveltekit)#64
rubenmarcus wants to merge 2 commits into
mainfrom
feat/sveltekit-plugin

Conversation

@rubenmarcus

Copy link
Copy Markdown
Member

Adds the SvelteKit plugin — the types and detection already anticipated it (FrameworkType includes sveltekit, tsup externals include @sveltejs/kit); this ships the actual integration.

What

  • aeo.js/sveltekit new 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 via injectWidget: false).
    • generate() — source-only mode scanning src/routes (+page.svelte/+page.md/+page.svx; route groups (name) transparent, dynamic [slug] segments skipped), writing into static/.
    • getWidgetScript() — same surface as the Angular plugin.
  • tsup entry + exports map wiring
  • Docs: frameworks/sveltekit guide + sidebar entry + README quick start & table row

Verification

  • tsc --noEmit clean, build succeeds with the new entry
  • 187 tests pass (7 new, real-filesystem fixtures: route scanning incl. groups/dynamic segments, adapter-static postBuild output, widget injection idempotency, non-static adapter fallback)

🤖 Generated with Claude Code

rubenmarcus and others added 2 commits June 10, 2026 13:11
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>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aeo-js Ready Ready Preview, Comment Jun 10, 2026 11:12am

Request Review

@github-actions

Copy link
Copy Markdown

Docs Preview

Preview URL: https://feat-sveltekit-plugin.aeojs.pages.dev

This preview was deployed from the latest commit on this PR.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR ships the aeo.js/sveltekit integration, following the Angular plugin pattern: a postBuild function that detects the output directory, scans prerendered HTML, generates AEO files, and optionally injects the Human/AI widget; a generate function for source-only mode writing to static/; and a getWidgetScript helper.

  • Widget injection is broken for non-static adapters: injectWidgetIntoHtml is called with outputDir (.svelte-kit/output/client), which contains JS/CSS bundles only — the actual prerendered HTML lives in .svelte-kit/output/prerendered/pages and is never walked.
  • config.pages overrides are silently dropped: user-provided entries appear last in the merge array and can never overwrite auto-discovered pages that already have content, making explicit caller overrides a no-op.
  • </script> in serialized config breaks the injected script tag: JSON.stringify does not escape </ sequences, so a title or URL containing </script> would prematurely close the script element in the page.

Confidence Score: 3/5

The 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

Filename Overview
src/plugins/sveltekit.ts Core new plugin — widget injection is silently skipped for non-static adapters, config.pages overrides are dropped when a page was already discovered, and JSON.stringify doesn't escape </script> in the injected script tag.
src/plugins/sveltekit.test.ts 7 new real-filesystem tests covering route scanning, adapter-static postBuild, widget injection idempotency, and non-static adapter fallback; the non-static test only asserts AEO file generation, not widget injection, leaving the injection bug undetected.
package.json Adds ./sveltekit exports map entry (types + ESM + CJS), consistent with existing plugin entries.
tsup.config.ts Adds sveltekit entry point to the tsup build, matching the pattern of all other plugins.
website/src/content/docs/frameworks/sveltekit.mdx New documentation page — setup steps, how it works, programmatic usage, and configuration reference; accurate and consistent with the implementation.
README.md Adds SvelteKit quick-start snippet and a table row — straightforward documentation update.
website/astro.config.mjs Adds SvelteKit sidebar entry between Nuxt and Angular — clean, consistent change.

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]
Loading
Prompt To Fix All With AI
Fix 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

Comment thread src/plugins/sveltekit.ts
Comment on lines +253 to +258
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)`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread src/plugins/sveltekit.ts
Comment on lines +221 to +228
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread src/plugins/sveltekit.ts
Comment on lines +134 to +139
const widgetConfig = JSON.stringify({
title: resolvedConfig.title,
description: resolvedConfig.description,
url: resolvedConfig.url,
widget: resolvedConfig.widget,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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.

Comment thread src/plugins/sveltekit.ts
Comment on lines +78 to +81
const clientDir = join(projectRoot, '.svelte-kit', 'output', 'client');
if (existsSync(clientDir)) return clientDir;

return staticBuild;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant