Skip to content

feat: Remix / React Router 7 integration (aeo.js/remix)#65

Merged
rubenmarcus merged 7 commits into
mainfrom
feat/remix-plugin
Jun 16, 2026
Merged

feat: Remix / React Router 7 integration (aeo.js/remix)#65
rubenmarcus merged 7 commits into
mainfrom
feat/remix-plugin

Conversation

@rubenmarcus

Copy link
Copy Markdown
Member

Adds the Remix plugin — FrameworkType already listed remix; this ships the integration.

What

  • aeo.js/remix new entry point following the postBuild pattern:
    • postBuild() — detects the static assets dir (build/client for Vite-based Remix/RR7, public/ for the classic compiler), scans app/routes flat-file + folder routes, merges content from prerendered HTML (React Router's prerender option), generates all AEO files into the assets dir.
    • Route-id parsing handles _index, dot-delimited nesting, pathless layouts (_marketing.), optional segments (().), trailing-underscore layout opt-out, and skips dynamic segments ($slug) — exported as remixRouteToPathname() and unit-tested.
    • generate() — source-only mode writing into public/.
    • getWidgetScript() — root-route widget snippet (Remix has no static index.html to inject into).
  • tsup entry + exports map wiring
  • Docs: frameworks/remix guide + sidebar entry + README quick start & table row

Verification

  • tsc --noEmit clean, build succeeds
  • 189 tests pass (9 new: route-id mapping incl. edge cases, source scanning, build/client + public/ fallback postBuild, widget script)

🤖 Generated with Claude Code

rubenmarcus and others added 2 commits June 10, 2026 13:14
aeo.js/remix follows the postBuild pattern: detects build/client (Vite
builds) or public/ (classic compiler), scans app/routes flat-file and
folder routes (pathless layouts and optional segments resolved, dynamic
segments skipped), merges prerendered HTML content, and generates all
AEO files into the static assets directory. getWidgetScript() provides
the root-route widget snippet since Remix has no static index.html.

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 16, 2026 10:49pm

Request Review

@github-actions

Copy link
Copy Markdown

Docs Preview

Preview URL: https://feat-remix-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/remix entry point, providing postBuild, generate, and getWidgetScript for Remix v2 / React Router 7 projects, following the same post-build pattern used by the other framework plugins.

  • postBuild detects build/client (Vite) or public/ (classic compiler), scans prerendered HTML and app/routes flat-file + folder routes, merges them into a unified page list, and writes all AEO files (robots.txt, sitemap.xml, llms.txt, ai-index.json) into the static assets dir.
  • remixRouteToPathname translates Remix v2 flat-route IDs to pathnames, handling _index, pathless layouts, optional segments, and dynamic segment exclusion — exported and covered by unit tests.
  • Build wiring (tsup entry + exports map) and docs (MDX guide + sidebar entry + README row) are all included.

Confidence Score: 4/5

The new Remix plugin is functional and well-structured, but the merge logic in postBuild has known correctness gaps around config.pages that were flagged in earlier review rounds and remain unresolved in this diff.

The route-scanning, HTML extraction, and file-generation paths all work correctly for the common case. The outstanding concern is in postBuild: when a user supplies config.pages overrides for routes that were also prerendered, the unconditional pageMap.set drops the extracted HTML content, silently producing empty entries in llms-full.txt and ai-index.json. The test suite does not exercise this code path against content-bearing fields, so the regression would be invisible until production use.

src/plugins/remix.ts — specifically the postBuild merge loop and the config.pages override block (lines 218–228)

Important Files Changed

Filename Overview
src/plugins/remix.ts New Remix integration — route scanning and merge logic have unresolved issues around config.pages handling and bracket-escaped route IDs
src/plugins/remix.test.ts Good coverage of core route-id mapping and build scenarios; missing assertions on content preservation when config.pages overrides a prerendered route
package.json Adds the aeo.js/remix export map entry; consistent with other plugin entries
tsup.config.ts Adds remix entry to the tsup build; no issues
website/src/content/docs/frameworks/remix.mdx New docs page for the Remix integration; widget example correctly uses the div wrapper pattern
README.md Adds Remix quick-start snippet and table entry; consistent with other frameworks

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["postBuild(config)"] --> B{config.outDir set?}
    B -- yes --> C[Use config.outDir]
    B -- no --> D{build/client exists?}
    D -- yes --> E[outputDir = build/client]
    D -- no --> F[outputDir = public/]
    C & E & F --> G[scanHtmlOutput\nprerendered HTML pages]
    A --> H[scanRemixRoutes\napp/routes flat-file + folder]
    G --> I["autoPages = [...buildPages, ...sourcePages]"]
    H --> I
    I --> J["Merge into pageMap\n(prerendered content wins)"]
    J --> K["config.pages unconditionally\noverwrite pageMap entries"]
    K --> L[Apply title/description fallbacks]
    L --> M[generateAEOFiles\nrobots.txt, sitemap.xml,\nllms.txt, ai-index.json]
    N["generate(config)"] --> O[scanRemixRoutes]
    O --> P[pageMap from source routes]
    P --> Q["config.pages overwrite"]
    Q --> R["generateAEOFiles → public/"]
    S["getWidgetScript(config)"] --> T{widget.enabled?}
    T -- no --> U["return ''"]
    T -- yes --> V["Return script type=module"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["postBuild(config)"] --> B{config.outDir set?}
    B -- yes --> C[Use config.outDir]
    B -- no --> D{build/client exists?}
    D -- yes --> E[outputDir = build/client]
    D -- no --> F[outputDir = public/]
    C & E & F --> G[scanHtmlOutput\nprerendered HTML pages]
    A --> H[scanRemixRoutes\napp/routes flat-file + folder]
    G --> I["autoPages = [...buildPages, ...sourcePages]"]
    H --> I
    I --> J["Merge into pageMap\n(prerendered content wins)"]
    J --> K["config.pages unconditionally\noverwrite pageMap entries"]
    K --> L[Apply title/description fallbacks]
    L --> M[generateAEOFiles\nrobots.txt, sitemap.xml,\nllms.txt, ai-index.json]
    N["generate(config)"] --> O[scanRemixRoutes]
    O --> P[pageMap from source routes]
    P --> Q["config.pages overwrite"]
    Q --> R["generateAEOFiles → public/"]
    S["getWidgetScript(config)"] --> T{widget.enabled?}
    T -- no --> U["return ''"]
    T -- yes --> V["Return script type=module"]
Loading

Reviews (10): Last reviewed commit: "Merge origin/main: resolve conflicts by ..." | Re-trigger Greptile

Comment thread src/plugins/remix.ts Outdated
Comment thread src/plugins/remix.ts
Comment thread src/plugins/remix.ts Outdated
- Give config.pages unconditional last-write priority in postBuild so user
  overrides are never silently dropped by auto-scanned entries that already
  have content.
- Correct the getWidgetScript JSDoc example: the function returns a complete
  <script> string, so wrapping it in another <script> tag would double-nest it.
- Add a regression test confirming config.pages entries beat prerendered HTML
  for the same pathname.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptileai review

@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptileai review

…pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptile review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptile review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptile review

Comment thread src/plugins/remix.ts
Comment on lines +225 to +228
// config.pages entries unconditionally overwrite auto-scanned entries
for (const page of config.pages || []) {
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 override silently drops prerendered content

pageMap.set(page.pathname, page) replaces the entire entry — including any content extracted from prerendered HTML. A user who uses React Router's prerender option and provides a config.pages title/description override for the same route will silently lose the extracted text content, causing empty content in llms-full.txt and ai-index.json. The fix is to shallow-merge instead of replace: pageMap.set(page.pathname, { ...pageMap.get(page.pathname), ...page }). The existing test for this path only asserts on llms.txt (which doesn't include body content), so the content loss goes undetected.

Suggested change
// config.pages entries unconditionally overwrite auto-scanned entries
for (const page of config.pages || []) {
pageMap.set(page.pathname, page);
}
// config.pages entries unconditionally overwrite auto-scanned entries
for (const page of config.pages || []) {
const existing = pageMap.get(page.pathname);
pageMap.set(page.pathname, existing ? { ...existing, ...page } : page);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/remix.ts
Line: 225-228

Comment:
**`config.pages` override silently drops prerendered `content`**

`pageMap.set(page.pathname, page)` replaces the entire entry — including any `content` extracted from prerendered HTML. A user who uses React Router's `prerender` option _and_ provides a `config.pages` title/description override for the same route will silently lose the extracted text content, causing empty content in `llms-full.txt` and `ai-index.json`. The fix is to shallow-merge instead of replace: `pageMap.set(page.pathname, { ...pageMap.get(page.pathname), ...page })`. The existing test for this path only asserts on `llms.txt` (which doesn't include body content), so the content loss goes undetected.

```suggestion
  // config.pages entries unconditionally overwrite auto-scanned entries
  for (const page of config.pages || []) {
    const existing = pageMap.get(page.pathname);
    pageMap.set(page.pathname, existing ? { ...existing, ...page } : page);
  }
```

How can I resolve this? If you propose a fix, please make it concise.

…tekit entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus rubenmarcus merged commit 5e58dfd into main Jun 16, 2026
1 check was pending
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