Skip to content

fix: serve static content files and resolve image paths#46

Merged
rsbh merged 7 commits intomainfrom
fix_img_tag
May 5, 2026
Merged

fix: serve static content files and resolve image paths#46
rsbh merged 7 commits intomainfrom
fix_img_tag

Conversation

@rsbh
Copy link
Copy Markdown
Member

@rsbh rsbh commented May 5, 2026

Summary

  • Return web Response from all Nitro server handlers to fix Bun dev mode NodeResponse error (nitrojs/nitro#4228)
  • Add /_content/ catch-all route to serve static files (images, PDFs, etc.) from content directories
  • Add remarkResolveImages plugin to resolve image src paths relative to the MD/MDX file location
  • Read readingTime from eager frontmatter glob instead of loading full MDX module in API route
  • Disable fumadocs remarkImage to prevent it from converting image src to JS imports

Test plan

  • Run versioned example, verify sidebar navigation works (no Parse Error)
  • Verify <img src="images/vpn/vpn_07.png"> in .mdx files resolves to /_content/terranova/images/vpn/vpn_07.png
  • Verify /_content/ route serves images with correct content type
  • Verify markdown ![](images/foo.png) syntax also resolves correctly
  • Verify external URLs (https://...) in image src are untouched

🤖 Generated with Claude Code

rsbh and others added 6 commits May 4, 2026 16:53
Nitro wraps plain return values in NodeResponse, which Bun rejects
in dev mode (nitrojs/nitro#4228). Return standard Response/Response.json
directly to bypass this. Also read readingTime from eager frontmatter
glob instead of loading full MDX module in API route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Catch-all route serves non-md/mdx files (images, PDFs, etc.) from
the .content directory. Blocks .md/.mdx and path traversal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remark plugin rewrites image src (both markdown ![](…) and JSX <img>)
to absolute /_content/ URLs based on the MD file's location within
the content directory. External URLs left untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remark plugin resolves image src (markdown, HTML, JSX, HAST) relative
to the MD file's location. Disable fumadocs remarkImage to prevent
it from converting src to JS imports before our plugin runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace string array visit with type-checked node.type guard to
fix TS overload error. Add @types/unist to chronicle devDeps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview, Comment May 5, 2026 5:25am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@rsbh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 21 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 816edb79-8df4-4414-bb73-e2cd4e325e44

📥 Commits

Reviewing files that changed from the base of the PR and between 03f668f and d14ceef.

📒 Files selected for processing (2)
  • packages/chronicle/src/lib/remark-resolve-images.ts
  • packages/chronicle/src/server/routes/_content/[...path].ts
📝 Walkthrough

Walkthrough

This PR refactors server route handlers to return explicit Response objects instead of mutating event.res headers, adds a remark plugin for rewriting image URLs to a static content route, injects reading time metadata during build, and adds type definitions for Unified/MDX tooling.

Changes

Image Resolution & Static Content

Layer / File(s) Summary
Dependencies
packages/chronicle/package.json
Added @types/hast, @types/mdast, @types/unist, and mdast-util-mdx-jsx to support AST manipulation and MDX JSX processing.
Remark Plugin
packages/chronicle/src/lib/remark-resolve-images.ts
New plugin remarkResolveImages traverses markdown AST and rewrites image src attributes across image, html (via regex), MDX JSX, and HTML element nodes to point to /_content/... paths using file-relative directory resolution.
Vite Configuration
packages/chronicle/src/server/vite-config.ts
Integrated remarkResolveImages into the MDX remark plugin chain and disabled built-in remarkImageOptions to delegate image handling to the new plugin.
Static Content Route
packages/chronicle/src/server/routes/_content/[...path].ts
New catch-all route serves static files from the content directory with appropriate MIME types and 1-day cache headers; blocks markdown files and validates paths for security.

Reading Time Metadata Injection

Layer / File(s) Summary
Page Data Enrichment
packages/chronicle/src/lib/source.ts
Build-time injection of _readingTime computed from eager readingTimeGlob into each page's data, making reading time available downstream without dynamic module loading.
API Handler Update
packages/chronicle/src/server/api/page.ts
Removed loadPageModule call that previously computed _readingTime in the response; reading time is now sourced from page data injected during build and frontmatter no longer includes _readingTime.

Response Handler Standardization

Layer / File(s) Summary
API Handlers
packages/chronicle/src/server/api/health.ts, apis-proxy.ts, search.ts, specs.ts
Systematically refactored to return Response.json(...) instead of plain JavaScript objects, ensuring consistent HTTP response serialization across all API endpoints.
Document & Asset Routes
packages/chronicle/src/server/routes/[...slug].md.ts, [version]/llms.txt.ts, og.tsx, robots.txt.ts, sitemap.xml.ts, llms.txt.ts
Migrated from event-based header mutation (event.res.headers.set) to returning explicit Response objects with Content-Type and Cache-Control headers; handler signatures simplified to remove unused event parameter where applicable.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant MarkdownProcessor as Markdown<br/>Processor
    participant RemarkPlugin as Remark<br/>Image Plugin
    participant ContentRoute as _content<br/>Route
    participant FileSystem as File<br/>System

    Browser->>MarkdownProcessor: Load markdown with images
    MarkdownProcessor->>RemarkPlugin: Parse and traverse AST
    RemarkPlugin->>RemarkPlugin: Detect image nodes<br/>(img, html, JSX)
    RemarkPlugin->>RemarkPlugin: Resolve relative paths to<br/>/_content/...
    RemarkPlugin->>MarkdownProcessor: Return modified AST
    MarkdownProcessor->>Browser: Render with updated image URLs
    
    Browser->>ContentRoute: GET /_content/images/example.png
    ContentRoute->>FileSystem: Resolve safe path from<br/>CHRONICLE_CONTENT_DIR
    FileSystem->>ContentRoute: File contents
    ContentRoute->>Browser: Response with MIME type<br/>+ Cache-Control header
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • raystack/chronicle#44: Both PRs modify how reading time (_readingTime) is computed and made available to pages; this PR injects it during buildFiles(), while the referenced PR adds remark-reading-time plugin and exposes it in frontmatter.
  • raystack/chronicle#16: Both modify packages/chronicle/src/lib/source.ts to change how page data is constructed and enriched during the build phase.
  • raystack/chronicle#19: Both add type definitions for Unified/MDX ecosystem dependencies (@types/unist and related types) to support AST-level tooling and plugins.

Suggested reviewers

  • rohanchkrabrty
  • rohilsurana
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the primary changes: returning web Response objects from Nitro handlers (fix) and adding image path resolution and static content serving (serve static content files, resolve image paths).
Description check ✅ Passed The description is clearly related to the changeset, detailing the core modifications (Response refactoring, /_content/ route, remarkResolveImages plugin, readingTime handling, remarkImage disabling) and providing a test plan aligned with the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix_img_tag

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/chronicle/src/lib/source.ts (1)

239-251: ⚡ Quick win

Consider extracting the _readingTime normalization into a shared helper.

The clamped rounding formula minutes != null ? Math.max(1, Math.round(minutes)) : undefined is duplicated verbatim in buildFiles (Line 47) and here. If the "minimum 1 minute" policy changes, it needs updating in both places.

♻️ Proposed refactor
+function normalizeReadingTime(minutes: number | undefined): number | undefined {
+  return minutes != null ? Math.max(1, Math.round(minutes)) : undefined;
+}

 // in buildFiles():
-const _readingTime = rt?.minutes != null ? Math.max(1, Math.round(rt.minutes)) : undefined;
+const _readingTime = normalizeReadingTime(rt?.minutes);

 // in loadPageModule():
 const minutes = mod.readingTime?.minutes;
-return { default: mod.default ?? null, toc: mod.toc ?? [], _readingTime: minutes != null ? Math.max(1, Math.round(minutes)) : undefined };
+return { default: mod.default ?? null, toc: mod.toc ?? [], _readingTime: normalizeReadingTime(minutes) };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chronicle/src/lib/source.ts` around lines 239 - 251, The
reading-time normalization logic duplicated between loadPageModule and
buildFiles should be extracted to a single helper (e.g., normalizeReadingTime or
clampReadingMinutes) to avoid drift; create a small function that accepts
minutes?: number and returns minutes != null ? Math.max(1, Math.round(minutes))
: undefined, export or place it where both loadPageModule and buildFiles can
import it, then replace the inline expression in loadPageModule (the
_readingTime calculation) and the corresponding calculation in buildFiles to
call that helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/chronicle/src/lib/remark-resolve-images.ts`:
- Around line 22-26: Normalize path separators on the `file.path` string before
checking for '/content/': replace backslashes with forward slashes (e.g.,
normalize `filePath = filePath.replace(/\\+/g, '/')`) so `contentIdx =
filePath.lastIndexOf('/content/')` works on Windows; then continue to compute
`relative` and `dir` from that normalized `filePath` (these symbols: `filePath`,
`contentIdx`, `relative`, `dir` in remark-resolve-images.ts).
- Around line 9-14: The code currently treats protocol-relative URLs like
"//cdn.example.com/a.png" as root-relative and rewrites them; update the check
in the image-resolving logic (the block using src, path.posix.normalize/join in
remark-resolve-images.ts) to early-return protocol-relative URLs unchanged by
adding a condition such as if (src.startsWith('//')) return src before the
existing if (src.startsWith('/')) branch so that "//..." is not rewritten to
"/_content//...".

In `@packages/chronicle/src/server/routes/_content/`[...path].ts:
- Around line 24-25: Call to safePath(pathname, contentDir) can throw on
malformed percent-encoding and currently becomes a 500; wrap the safePath
invocation in a try/catch and treat any thrown error (or a falsy return) as a
controlled 404 by throwing new HTTPError({ status: 404, message: 'Not Found' }).
Specifically, update the logic around safePath/filePath so that errors from
safePath and a null/undefined filePath both result in the same HTTPError 404
response.

---

Nitpick comments:
In `@packages/chronicle/src/lib/source.ts`:
- Around line 239-251: The reading-time normalization logic duplicated between
loadPageModule and buildFiles should be extracted to a single helper (e.g.,
normalizeReadingTime or clampReadingMinutes) to avoid drift; create a small
function that accepts minutes?: number and returns minutes != null ? Math.max(1,
Math.round(minutes)) : undefined, export or place it where both loadPageModule
and buildFiles can import it, then replace the inline expression in
loadPageModule (the _readingTime calculation) and the corresponding calculation
in buildFiles to call that helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56109651-e503-4c0d-9d62-792bc8f5036f

📥 Commits

Reviewing files that changed from the base of the PR and between 5da4bf3 and 03f668f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • packages/chronicle/package.json
  • packages/chronicle/src/lib/remark-resolve-images.ts
  • packages/chronicle/src/lib/source.ts
  • packages/chronicle/src/server/api/apis-proxy.ts
  • packages/chronicle/src/server/api/health.ts
  • packages/chronicle/src/server/api/page.ts
  • packages/chronicle/src/server/api/search.ts
  • packages/chronicle/src/server/api/specs.ts
  • packages/chronicle/src/server/routes/[...slug].md.ts
  • packages/chronicle/src/server/routes/[version]/llms.txt.ts
  • packages/chronicle/src/server/routes/_content/[...path].ts
  • packages/chronicle/src/server/routes/llms.txt.ts
  • packages/chronicle/src/server/routes/og.tsx
  • packages/chronicle/src/server/routes/robots.txt.ts
  • packages/chronicle/src/server/routes/sitemap.xml.ts
  • packages/chronicle/src/server/vite-config.ts

Comment thread packages/chronicle/src/lib/remark-resolve-images.ts
Comment thread packages/chronicle/src/lib/remark-resolve-images.ts
Comment thread packages/chronicle/src/server/routes/_content/[...path].ts Outdated
@rsbh rsbh requested review from rohanchkrabrty and rohilsurana May 5, 2026 05:23
Skip protocol-relative URLs (//cdn.example.com) in image resolver.
Catch malformed percent-encoding in _content route safePath call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rsbh rsbh merged commit a8048f4 into main May 5, 2026
4 checks passed
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.

2 participants