Skip to content

feat: provide rss, atom and json feeds for the blog#2562

Draft
Kiwow wants to merge 17 commits intonpmx-dev:mainfrom
Kiwow:rss
Draft

feat: provide rss, atom and json feeds for the blog#2562
Kiwow wants to merge 17 commits intonpmx-dev:mainfrom
Kiwow:rss

Conversation

@Kiwow
Copy link
Copy Markdown
Contributor

@Kiwow Kiwow commented Apr 17, 2026

I'm opening this as a draft so that progress on it is transparent. Feedback is appreciated even before it's ready!

🔗 Linked issue

Resolves #2489

The plan

  • Generate RSS, Atom and JSON feeds from blog posts using the feed package
    • Generate these at build time
    • Use the data that modules/blog.ts is creating in .nuxt/blog/posts.ts, possibly in the same module setup
    • Use prerendered routes, similarly to how opensearch.xml gets served (thanks to @t128n for the pointer on this)
  • Serve the feeds at "/rss.xml", "/atom.xml" and "/feed.json" respectively
    • Serve with types "application/rss+xml", "application/atom+xml" and "application/feed+json"
  • Provide <link rel="alternate"> elements for readers to find the feeds
  • Test that this works (with an e2e test?)

Checklist

Before merging, I need to make sure that:

Issues / obstacles:

The feed package currently doesn't support JSON Feed 1.1, which added support for multiple authors. This means it silently throws away all but the first author we specify in an the author array. Manually parsing the object after it gets generated and adding in the other authors is an option, but then we'd be lying about the version that we're serving. Alternatively, it shouldn't be that hard to write the JSON Feed serializer ourselves (the feed implementation is a bit over 100 lines of code)

RSS (the spec) seemingly doesn't support multiple authors at all, or just in convention, where you put everyone into an <author> tag. I'll have to try and test some readers / find how feeds handle multiple authors to figure out a good solution. Either way, the feed package includes multiple <author> tags, which seems to be valid in Atom but not in RSS.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 17, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 19, 2026 9:36pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 19, 2026 9:36pm
npmx-lunaria Ignored Ignored Apr 19, 2026 9:36pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added blog feed syndication in three formats: RSS, Atom, and JSON Feed.
    • Feed links are now discoverable on the blog page.
    • Feeds include full post content with author and metadata information.
    • Enabled cross-origin access to feed endpoints for broader compatibility.
  • Tests

    • Added end-to-end tests verifying feed availability and correct content types.

Walkthrough

Adds RSS, Atom and JSON Feed support: new server routes serving feeds, a feed-generation utility that builds and caches a Feed from blog posts, Nuxt route rules and esbuild tweaks, alternate feed links on the blog page, dependency addition, and Playwright e2e tests validating discoverability, content types and CORS.

Changes

Cohort / File(s) Summary
Feed route handlers
server/routes/rss.xml.get.ts, server/routes/atom.xml.get.ts, server/routes/feed.json.get.ts
New server routes returning serialized feeds by calling getFeed().rss2(), getFeed().atom1(), and getFeed().json1() respectively.
Feed generation utility
server/utils/feeds.ts
New exported getFeed() that lazily generates and caches a Feed instance using site metadata and non-draft posts, mapping authors, absolute URLs, dates and images into feed items.
Blog page metadata
app/pages/blog/index.vue
Added useHead entries that inject three <link rel="alternate"> references for RSS, Atom and JSON Feed pointing to the site feed endpoints.
Nuxt/Nitro configuration
nuxt.config.ts
Added routeRules for /rss.xml, /atom.xml, /feed.json to prerender and set CORS/Content-Type headers; adjusted nitro.esbuild.options.exclude to allow server-side TS feed import.
Dependencies
package.json
Added feed v5.2.0 to devDependencies.
End-to-end tests
test/e2e/feeds.spec.ts
New Playwright tests that find alternate feed links on /blog, fetch each feed, and assert expected Content-Type and Access-Control-Allow-Origin headers.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant BlogPage as Blog Page
    participant FeedRoute as Feed Route (/rss.xml, /atom.xml, /feed.json)
    participant FeedUtil as getFeed() Utility
    participant BlogPosts as Blog Posts Data

    Browser->>BlogPage: Request /blog
    BlogPage-->>Browser: Render page with alternate feed links

    Browser->>FeedRoute: Request feed endpoint (via link)
    FeedRoute->>FeedUtil: Call getFeed()
    FeedUtil->>BlogPosts: Load posts (exclude drafts)
    BlogPosts-->>FeedUtil: Return posts
    FeedUtil->>FeedUtil: Build/cached Feed object (items, authors, links, dates)
    FeedUtil-->>FeedRoute: Return Feed
    FeedRoute->>FeedRoute: Serialize (.rss2/.atom1/.json1)
    FeedRoute-->>Browser: Respond with feed + Content-Type + CORS
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: provide rss, atom and json feeds for the blog' clearly and specifically summarizes the main change: adding syndication feed support for the blog.
Description check ✅ Passed The description provides comprehensive details about the implementation plan, design choices, linked issue reference, test coverage, and known limitations regarding multiple authors and feed validation.
Linked Issues check ✅ Passed The PR successfully implements RSS feed support (#2489) plus additional Atom and JSON Feed formats, with proper serving at designated endpoints, feed discovery via link elements, and e2e test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing feed syndication: feed generation utilities, route handlers, configuration updates, dependency addition, and tests. No unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 17, 2026

⚠️ Duplicate Dependencies (found: 20, threshold: 4)

📦 Package 📋 Versions
h3
5 versions
  • root@
    • @nuxt/fonts@0.14.0
      • fontless@0.2.1
        • unstorage@1.17.5
          • h3@1.15.11

  • root@
    • @nuxt/fonts@0.14.0
      • h3@1.15.8

h3@2.0.1-rc.11
h3@2.0.1-rc.16
h3@2.0.1-rc.20
oxc-parser
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0

@oxc-parser/binding-wasm32-wasi
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-wasm32-wasi@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-wasm32-wasi@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-wasm32-wasi@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-wasm32-wasi@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-wasm32-wasi@0.95.0

glob
5 versions
  • root@
    • @nuxt/test-utils@4.0.0
      • @vue/test-utils@2.4.6
        • js-beautify@1.15.4
          • glob@10.5.0

  • root@
    • @vite-pwa/nuxt@1.1.1
      • vite-plugin-pwa@1.2.0
        • workbox-build@7.4.0
          • glob@11.1.0

  • root@
    • @vueuse/nuxt@14.2.1
      • ...
        • nitropack@2.13.3
          • @vercel/nft@1.5.0
            • glob@13.0.6

  • root@
    • vue-i18n-extract@2.0.7
      • dot-object@2.1.5
        • glob@7.2.3

  • root@
    • vue-i18n-extract@2.0.7
      • glob@8.1.0

@rolldown/pluginutils
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • ...
        • rollup-plugin-visualizer@7.0.1
          • rolldown@1.0.0-rc.12
            • @rolldown/pluginutils@1.0.0-rc.12

  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • @nuxt/vite-builder@4.3.1
          • @vitejs/plugin-vue-jsx@5.1.5
            • @rolldown/pluginutils@1.0.0-rc.13

  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • @nuxt/vite-builder@4.3.1
          • @vitejs/plugin-vue@6.0.5
            • @rolldown/pluginutils@1.0.0-rc.2

  • root@
    • @nuxt/a11y@1.0.0-alpha.1
      • @nuxt/devtools-kit@3.2.4
        • vite@8.0.0-beta.18
          • rolldown@1.0.0-rc.8
            • @rolldown/pluginutils@1.0.0-rc.8

@rolldown/pluginutils@1.0.0-rc.9
@oxc-project/types
7 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-project/types@0.112.0

  • root@
    • @nuxt/a11y@1.0.0-alpha.1
      • @nuxt/devtools-kit@3.2.4
        • vite@8.0.0-beta.18
          • rolldown@1.0.0-rc.8
            • @oxc-project/types@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-project/types@0.120.0

  • root@
    • @vueuse/nuxt@14.2.1
      • ...
        • rollup-plugin-visualizer@7.0.1
          • rolldown@1.0.0-rc.12
            • @oxc-project/types@0.122.0

  • root@
    • vite-plus@0.1.16
      • @oxc-project/types@0.123.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-project/types@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-project/types@0.95.0

@oxc-parser/binding-android-arm64
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-android-arm64@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-android-arm64@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-android-arm64@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-android-arm64@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-android-arm64@0.95.0

@oxc-parser/binding-darwin-arm64
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-darwin-arm64@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-darwin-arm64@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-darwin-arm64@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-darwin-arm64@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-darwin-arm64@0.95.0

@oxc-parser/binding-darwin-x64
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-darwin-x64@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-darwin-x64@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-darwin-x64@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-darwin-x64@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-darwin-x64@0.95.0

@oxc-parser/binding-freebsd-x64
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-freebsd-x64@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-freebsd-x64@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-freebsd-x64@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-freebsd-x64@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-freebsd-x64@0.95.0

@oxc-parser/binding-linux-arm-gnueabihf
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-arm-gnueabihf@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-arm-gnueabihf@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-arm-gnueabihf@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-arm-gnueabihf@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-arm-gnueabihf@0.95.0

@oxc-parser/binding-linux-arm-musleabihf
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-arm-musleabihf@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-arm-musleabihf@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-arm-musleabihf@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-arm-musleabihf@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-arm-musleabihf@0.95.0

@oxc-parser/binding-linux-arm64-gnu
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-arm64-gnu@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-arm64-gnu@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-arm64-gnu@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-arm64-gnu@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-arm64-gnu@0.95.0

@oxc-parser/binding-linux-arm64-musl
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-arm64-musl@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-arm64-musl@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-arm64-musl@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-arm64-musl@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-arm64-musl@0.95.0

@oxc-parser/binding-linux-riscv64-gnu
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-riscv64-gnu@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-riscv64-gnu@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-riscv64-gnu@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-riscv64-gnu@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-riscv64-gnu@0.95.0

@oxc-parser/binding-linux-s390x-gnu
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-s390x-gnu@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-s390x-gnu@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-s390x-gnu@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-s390x-gnu@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-s390x-gnu@0.95.0

@oxc-parser/binding-linux-x64-gnu
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-x64-gnu@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-x64-gnu@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-x64-gnu@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-x64-gnu@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-x64-gnu@0.95.0

@oxc-parser/binding-linux-x64-musl
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-linux-x64-musl@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-linux-x64-musl@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-linux-x64-musl@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-linux-x64-musl@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-linux-x64-musl@0.95.0

@oxc-parser/binding-win32-arm64-msvc
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-win32-arm64-msvc@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-win32-arm64-msvc@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-win32-arm64-msvc@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-win32-arm64-msvc@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-win32-arm64-msvc@0.95.0

@oxc-parser/binding-win32-x64-msvc
5 versions
  • root@
    • @vueuse/nuxt@14.2.1
      • nuxt@4.3.1
        • oxc-parser@0.112.0
          • @oxc-parser/binding-win32-x64-msvc@0.112.0

  • root@
    • @unocss/nuxt@66.6.7
      • unocss@66.6.7
        • @unocss/transformer-attributify-jsx@66.6.7
          • oxc-parser@0.115.0
            • @oxc-parser/binding-win32-x64-msvc@0.115.0

  • root@
    • knip@6.0.5
      • oxc-parser@0.120.0
        • @oxc-parser/binding-win32-x64-msvc@0.120.0

  • root@
    • nuxt-og-image@6.4.3
      • oxc-parser@0.126.0
        • @oxc-parser/binding-win32-x64-msvc@0.126.0

  • root@
    • @nuxtjs/i18n@10.2.3
      • oxc-parser@0.95.0
        • @oxc-parser/binding-win32-x64-msvc@0.95.0

💡 To find out what depends on a specific package, run: pnpm -r why example-package

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Comment thread modules/blog.ts Outdated
const atomPath = 'atom.xml'
const jsonFeedPath = 'feed.json'

await Promise.all([
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe instead of saving these files to disk, creating pre-rendered server-side routes could be a cleaner approach 🤔. Similar to server/routes/opensearch.xml.get.ts with https://nuxt.com/docs/4.x/getting-started/prerendering#selective-pre-rendering

Copy link
Copy Markdown
Contributor Author

@Kiwow Kiwow Apr 18, 2026

Choose a reason for hiding this comment

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

Sounds like exactly what I was looking for and didn't find. Thanks!

I'll add a mention of this to the PR description and look into it when I have time to work on this again.

Copy link
Copy Markdown
Contributor

@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: 4

🧹 Nitpick comments (3)
test/e2e/feeds.spec.ts (1)

20-49: Test is mostly solid; a couple of small robustness nits.

  • expect(href.slice(0, 16)).toBe('https://npmx.dev') is correct arithmetic ('https://npmx.dev'.length === 16) but brittle — any host-name change silently breaks both the prefix check and the derived url = href.slice(16). Prefer matching on the parsed URL so intent is obvious and the "derive path from href" step can't go wrong:

    -        // href is an absolute link
    -        expect(href.slice(0, 16)).toBe('https://npmx.dev')
    -
    -        const { contentType, corsHeader } = await page.evaluate(async href => {
    -          // Fetch the same path as in the alternate link
    -          const url = href.slice(16)
    -          const response = await fetch(url)
    +        // href must be an absolute URL on the production origin
    +        const parsed = new URL(href)
    +        expect(parsed.origin).toBe('https://npmx.dev')
    +
    +        const { contentType, corsHeader } = await page.evaluate(async path => {
    +          const response = await fetch(path)
               return {
                 contentType: response.headers.get('Content-Type'),
                 corsHeader: response.headers.get('Access-Control-Allow-Origin'),
               }
    -        }, href)
    +        }, parsed.pathname)
  • await expect(locator).toHaveAttribute('href') without a second argument checks existence (fine), and the subsequent getAttribute('href') + expect(href).not.toBeNull() + typeof href !== 'string' guard is a bit redundant. You can drop the first assertion and keep the null/string guard, since expect(href).not.toBeNull() already fails the test on absence.

  • Minor: response.headers.get('Content-Type') returns the full header value, which may include charset parameters (application/rss+xml; charset=utf-8) depending on the server. Right now toBe(feed.contentType) would break if Nitro ever adds a charset. Consider expect(contentType).toMatch(new RegExp('^' + feed.contentType)) to be forward-compatible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/feeds.spec.ts` around lines 20 - 49, The test should parse the
alternate link href with the URL API instead of slicing and remove the redundant
locator existence assert; change the code to getAttribute('href') into href,
guard for null/string, then create const parsed = new URL(href) and assert
parsed.origin === 'https://npmx.dev' (or the intended host) and pass
parsed.pathname+parsed.search into page.evaluate for fetching; finally relax the
content-type check to match the media type prefix (e.g., use a startsWith or
regex like '^' + feed.contentType) when asserting
response.headers.get('Content-Type') so charset parameters won't break the test
— update references: locator, href, page.evaluate, response.headers.get and the
expect(contentType) assertion accordingly.
nuxt.config.ts (1)

195-206: CORS + Content-Type via route rules — looks good.

Matches the PR checklist (enable CORS, serve correct MIME types) and the values are exactly what test/e2e/feeds.spec.ts asserts. One small thought: if you want feeds to be cacheable by intermediaries, consider adding a Cache-Control: public, max-age=… header alongside — otherwise feed aggregators may hit the origin more often than needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nuxt.config.ts` around lines 195 - 206, The route rules for '/rss.xml',
'/atom.xml', and '/feed.json' currently set CORS and Content-Type but lack
caching; update the headers object in nuxt.config.ts for the route keys
'/rss.xml', '/atom.xml', and '/feed.json' to include a Cache-Control header
(e.g. 'Cache-Control': 'public, max-age=3600') so intermediaries can cache
feeds; modify the headers for the RouteRule entries (the objects used for these
paths) accordingly and run/update any tests that assert exact header sets if
needed.
app/pages/blog/index.vue (1)

19-40: Remove hard-coded feed URLs and title; use root-relative hrefs or derive from site config.

The three hard-coded https://npmx.dev/* hrefs and the fixed 'Blog - npmx' title duplicate values already present in nuxt.config.ts (site.url, site.name) and in server/utils/feeds.ts. If the canonical origin ever changes (staging, preview branches, a rename), these links will silently point at the wrong host. Also note these are already absolute on a blog page on npmx.dev — a root-relative href (/rss.xml, etc.) is equally valid for <link rel="alternate"> and would avoid hard-coding the origin on the client.

Consider either:

Option A — root-relative hrefs
-      href: 'https://npmx.dev/rss.xml',
+      href: '/rss.xml',
...
-      href: 'https://npmx.dev/atom.xml',
+      href: '/atom.xml',
...
-      href: 'https://npmx.dev/feed.json',
+      href: '/feed.json',
Option B — derive from site config
const siteConfig = useSiteConfig()
const origin = siteConfig.url // 'https://npmx.dev'
useHead({
  link: [
    { rel: 'alternate', title: `${siteConfig.name} Blog`, type: 'application/rss+xml',  href: `${origin}/rss.xml` },
    { rel: 'alternate', title: `${siteConfig.name} Blog`, type: 'application/atom+xml', href: `${origin}/atom.xml` },
    { rel: 'alternate', title: `${siteConfig.name} Blog`, type: 'application/feed+json', href: `${origin}/feed.json` },
  ],
})

Note: useSiteConfig() is already used elsewhere in the codebase (app/components/OgImage/Splash.takumi.vue), making Option B a viable pattern. Also consider updating the matching hard-coded URLs in server/utils/feeds.ts for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/index.vue` around lines 19 - 40, Replace the hard-coded feed
hrefs and title in the useHead call with either root-relative paths or values
derived from the site's config: stop using 'https://npmx.dev/*' and the fixed
'Blog - npmx' string in the link array inside useHead; instead call
useSiteConfig() (already used elsewhere), read siteConfig.url (if you need
absolute origin) and siteConfig.name (for the title), and set hrefs to
'/rss.xml', '/atom.xml', '/feed.json' or to `${siteConfig.url}/rss.xml` etc.,
and set title to `${siteConfig.name} Blog`; update the same pattern in
server/utils/feeds.ts as well for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nuxt.config.ts`:
- Around line 243-251: The esbuild exclude regex under the esbuild options
currently targets node_modules/.cache/nuxt/.nuxt/blog/posts.ts which won't match
Nuxt's actual buildDir (e.g. .nuxt/blog/posts.ts); update the exclude pattern in
the esbuild.options.exclude to allow the generated .nuxt/blog/posts.ts (or
otherwise include .nuxt/**/blog/posts.ts) so the `#blog/posts` import isn't
excluded, and add a simple runtime/build-time assertion or log of the resolved
buildDir used by Nuxt (referenced from server/utils/feeds.ts) to surface
mismatches early; also fix the comment path to use forward slashes
(server/utils/feeds.ts) for consistency.

In `@package.json`:
- Line 136: Move the "feed" package from devDependencies to dependencies in
package.json so it is available at runtime; update package.json by removing
"feed": "5.2.0" from devDependencies and adding the same entry under
dependencies. This ensures imports in server/utils/feeds.ts (used by the
/rss.xml, /atom.xml and /feed.json routes with prerender: true) are present in
production builds and prevents runtime errors if those routes are ever rendered
on-demand.

In `@server/utils/feeds.ts`:
- Around line 4-17: Add a short clarifying comment above the module-level
variable "feed" explaining that this cache is build-time only because getFeed()
calls generateFeed() once at module import (used for prerendered /rss.xml,
/atom.xml, /feed.json), and that if prerendering/ISR is removed the cache will
become stale and must be invalidated or regenerated (e.g., reset feed or call
generateFeed on each request); reference the "feed" variable, the getFeed()
function and generateFeed() to make the intended lifetime and required
invalidation explicit for future maintainers.
- Around line 19-54: generateFeed currently passes post.image through unchanged
causing broken thumbnails; update the feed.addItem call in function generateFeed
to normalize post.image the same way author.avatar is normalized (e.g., if
post.image exists, set image to new URL(post.image, siteUrl).toString()), and
replace the hard-coded title/description/id/link values with values pulled from
the shared site config (useSiteConfig() or equivalent) so title/description/site
URL are derived from the single source of truth used by nuxt.config.ts and
app/pages/blog/index.vue.

---

Nitpick comments:
In `@app/pages/blog/index.vue`:
- Around line 19-40: Replace the hard-coded feed hrefs and title in the useHead
call with either root-relative paths or values derived from the site's config:
stop using 'https://npmx.dev/*' and the fixed 'Blog - npmx' string in the link
array inside useHead; instead call useSiteConfig() (already used elsewhere),
read siteConfig.url (if you need absolute origin) and siteConfig.name (for the
title), and set hrefs to '/rss.xml', '/atom.xml', '/feed.json' or to
`${siteConfig.url}/rss.xml` etc., and set title to `${siteConfig.name} Blog`;
update the same pattern in server/utils/feeds.ts as well for consistency.

In `@nuxt.config.ts`:
- Around line 195-206: The route rules for '/rss.xml', '/atom.xml', and
'/feed.json' currently set CORS and Content-Type but lack caching; update the
headers object in nuxt.config.ts for the route keys '/rss.xml', '/atom.xml', and
'/feed.json' to include a Cache-Control header (e.g. 'Cache-Control': 'public,
max-age=3600') so intermediaries can cache feeds; modify the headers for the
RouteRule entries (the objects used for these paths) accordingly and run/update
any tests that assert exact header sets if needed.

In `@test/e2e/feeds.spec.ts`:
- Around line 20-49: The test should parse the alternate link href with the URL
API instead of slicing and remove the redundant locator existence assert; change
the code to getAttribute('href') into href, guard for null/string, then create
const parsed = new URL(href) and assert parsed.origin === 'https://npmx.dev' (or
the intended host) and pass parsed.pathname+parsed.search into page.evaluate for
fetching; finally relax the content-type check to match the media type prefix
(e.g., use a startsWith or regex like '^' + feed.contentType) when asserting
response.headers.get('Content-Type') so charset parameters won't break the test
— update references: locator, href, page.evaluate, response.headers.get and the
expect(contentType) assertion accordingly.
🪄 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: dae01d38-ddfa-4c63-be76-09ac26327e96

📥 Commits

Reviewing files that changed from the base of the PR and between 5cfe58f and 453ceb5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • app/pages/blog/index.vue
  • nuxt.config.ts
  • package.json
  • server/routes/atom.xml.get.ts
  • server/routes/feed.json.get.ts
  • server/routes/rss.xml.get.ts
  • server/utils/feeds.ts
  • test/e2e/feeds.spec.ts

Comment thread nuxt.config.ts
Comment on lines 243 to 251
esbuild: {
options: {
target: 'es2024',
// HACK: Excluding node_modules is the default. Here, we exempt
// .cache/nuxt/.nuxt/blog/posts.ts from that so that #blog/posts can be
// imported, parsed and executed as TS in server\utils\feeds.ts by esbuild
exclude: /node_modules\/(?!\.cache\/nuxt\/\.nuxt\/blog\/posts\.ts)/,
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the default buildDir for this project resolves under node_modules
rg -nP "buildDir|\.cache/nuxt" nuxt.config.ts modules/ --type=ts -C2
echo '---'
# Look at how the blog module writes posts.ts and which dir it joins against
rg -nP "addTemplate|buildDir" modules/blog.ts -C3

Repository: npmx-dev/npmx.dev

Length of output: 1546


🏁 Script executed:

#!/bin/bash
# Check if buildDir is explicitly configured in nuxt.config.ts
rg -nP "buildDir\s*[:=]" nuxt.config.ts -A2

# Also check for .cache/nuxt references in config
rg -nP "\.cache" nuxt.config.ts -C2

# Look for any builder or build configuration that might set the path
rg -nP "build\s*[:=]|nitro\s*[:=]" nuxt.config.ts -B1 -A5

Repository: npmx-dev/npmx.dev

Length of output: 1250


🏁 Script executed:

#!/bin/bash
# Get full content of nuxt.config.ts to see complete configuration
wc -l nuxt.config.ts
echo '---'
# Search for buildDir anywhere in config files
find . -maxdepth 2 -name "*.ts" -o -name "*.json" | xargs rg -l "buildDir" 2>/dev/null | head -10
echo '---'
# Check what Nuxt's default buildDir should be
rg -nP "buildDir" . --type=ts --type=json -C1 | grep -E "(buildDir|default)" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Read the full nuxt.config.ts file
cat -n nuxt.config.ts | head -100
echo '---'
cat -n nuxt.config.ts | sed -n '200,300p'

Repository: npmx-dev/npmx.dev

Length of output: 6315


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or references to the esbuild config or blog posts
rg -l "blog/posts|esbuild" test/ --type=ts 2>/dev/null | head -5
echo '---'
# Check how the blog posts are actually imported/used
rg -n "#blog/posts" . --type=ts | head -10

Repository: npmx-dev/npmx.dev

Length of output: 441


🏁 Script executed:

#!/bin/bash
# Comprehensive search for buildDir configuration
rg -rn "buildDir\s*[:=]" . --type=ts --type=json 2>/dev/null
echo '---'
# Also search for .cache/nuxt references that might indicate custom buildDir
rg -rn "\.cache/nuxt" . --type=ts --type=json --type=sh 2>/dev/null | grep -v node_modules | head -20

Repository: npmx-dev/npmx.dev

Length of output: 151


The esbuild exclude regex pattern will not match the generated blog posts path — the file will remain excluded and builds will fail.

The pattern expects the path node_modules/.cache/nuxt/.nuxt/blog/posts.ts, but Nuxt's default buildDir is .nuxt at the project root. The generated file will be at .nuxt/blog/posts.ts, which does not match the regex and will still be caught by esbuild's default node_modules exclusion. Since server/utils/feeds.ts imports from #blog/posts, this will cause build failures at runtime.

Two actions needed:

  • Fix the regex pattern to match .nuxt/blog/posts.ts instead, or
  • Add runtime assertion/logging of the resolved buildDir path at build time to catch mismatches immediately.

Also, update the comment's file path to use forward slashes consistently: server/utils/feeds.ts (the regex and actual Linux/macOS paths use /, not Windows-style \).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nuxt.config.ts` around lines 243 - 251, The esbuild exclude regex under the
esbuild options currently targets node_modules/.cache/nuxt/.nuxt/blog/posts.ts
which won't match Nuxt's actual buildDir (e.g. .nuxt/blog/posts.ts); update the
exclude pattern in the esbuild.options.exclude to allow the generated
.nuxt/blog/posts.ts (or otherwise include .nuxt/**/blog/posts.ts) so the
`#blog/posts` import isn't excluded, and add a simple runtime/build-time assertion
or log of the resolved buildDir used by Nuxt (referenced from
server/utils/feeds.ts) to surface mismatches early; also fix the comment path to
use forward slashes (server/utils/feeds.ts) for consistency.

Comment thread package.json
"devalue": "5.6.4",
"eslint-plugin-regexp": "3.1.0",
"fast-check": "4.6.0",
"feed": "5.2.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the feed package is only used behind prerendered routes and nowhere on a runtime path.
rg -nP "from ['\"]feed['\"]|require\(['\"]feed['\"]\)" --type=ts --type=js --type=vue
echo '---'
# Double-check no non-prerendered route imports getFeed
rg -nP -C2 "getFeed\s*\(" --type=ts

Repository: npmx-dev/npmx.dev

Length of output: 992


🏁 Script executed:

# Check where feed is imported in feeds.ts
cat -n server/utils/feeds.ts | head -20

echo '=== Checking nuxt.config.ts for prerender settings ==='
# Search for prerender configurations for the feed routes
rg -nP "atom\.xml|rss\.xml|feed\.json" nuxt.config.ts

echo '=== Current package.json state for feed ==='
# Check where feed is currently placed in package.json
rg -nP -B2 -A2 '"feed":\s*"5\.2\.0"' package.json

echo '=== Searching for any other getFeed or feed imports ==='
# Broader search for any other getFeed invocations
rg -nP "getFeed" --type=ts

Repository: npmx-dev/npmx.dev

Length of output: 1513


🏁 Script executed:

# View the prerender configuration for the feed routes in nuxt.config.ts
sed -n '193,210p' nuxt.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 693


Move feed from devDependencies to dependencies for runtime safety.

feed is imported in server/utils/feeds.ts and used only by three routes (/rss.xml, /atom.xml, /feed.json), all of which have prerender: true in nuxt.config.ts. While this means the dependency is needed only at build time in a fully prerendered deploy, moving it to dependencies is recommended as a safer posture. If any of these routes ever fall back to runtime rendering—such as an ISR fallback, dev preview in production mode, or a prerender failure served on-demand—Nitro's bundled output will reference feed at runtime and it will be missing in a --prod/--production install.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 136, Move the "feed" package from devDependencies to
dependencies in package.json so it is available at runtime; update package.json
by removing "feed": "5.2.0" from devDependencies and adding the same entry under
dependencies. This ensures imports in server/utils/feeds.ts (used by the
/rss.xml, /atom.xml and /feed.json routes with prerender: true) are present in
production builds and prevents runtime errors if those routes are ever rendered
on-demand.

Comment thread server/utils/feeds.ts
Comment on lines +4 to +17
let feed: Feed | undefined = undefined

/**
* Lazily creates and returns object that serializes into RSS, Atom and JSON
* feeds to be served on the website.
*
* @returns feed object that serializes into different feed formats
*/
export function getFeed(): Feed {
if (!feed) {
feed = generateFeed()
}
return feed
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Module-level cache is fine for prerender, but note the lifetime.

feed is held in a module-level variable and only reset on a cold module load. Because /rss.xml, /atom.xml, and /feed.json are all prerender: true, this is effectively "build-time only" and posts cannot change mid-run — good. If you ever remove prerendering (e.g. move to ISR), this cache will return a stale snapshot for the lifetime of the server process, since #blog/posts is resolved once at import time. Worth a short comment so a future maintainer doesn't flip prerendering without also invalidating this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/feeds.ts` around lines 4 - 17, Add a short clarifying comment
above the module-level variable "feed" explaining that this cache is build-time
only because getFeed() calls generateFeed() once at module import (used for
prerendered /rss.xml, /atom.xml, /feed.json), and that if prerendering/ISR is
removed the cache will become stale and must be invalidated or regenerated
(e.g., reset feed or call generateFeed on each request); reference the "feed"
variable, the getFeed() function and generateFeed() to make the intended
lifetime and required invalidation explicit for future maintainers.

Comment thread server/utils/feeds.ts
Comment on lines +19 to +54
function generateFeed() {
// Generate content for RSS, Atom and JSON feeds
const feed = new Feed({
title: 'Blog - npmx',
description: 'a fast, modern browser for the npm registry',
id: 'https://npmx.dev/',
link: 'https://npmx.dev/',
language: 'en',
image: 'https://npmx.dev/logo.svg',
favicon: 'https://npmx.dev/favicon.ico',
feedLinks: {
rss: 'https://npmx.dev/rss.xml',
atom: 'https://npmx.dev/atom.xml',
json: 'https://npmx.dev/feed.json',
},
})

for (const post of posts.filter(post => !post.draft)) {
feed.addItem({
title: post.title,
id: new URL(post.path, 'https://npmx.dev').toString(),
link: new URL(post.path, 'https://npmx.dev').toString(),
description: post.description,
author: post.authors.map(author => ({
name: author.name,
link: author.profileUrl ?? undefined,
// author.avatar is a relative URL - make it absolute to work in feed readers
avatar: author.avatar ? new URL(author.avatar, 'https://npmx.dev').toString() : undefined,
})),
date: new Date(post.date),
image: post.image,
})
}

return feed
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

post.image is not converted to an absolute URL.

The inline comment on line 45 correctly notes that relative URLs don't work in feed readers, and author.avatar is normalised via new URL(...). However, image: post.image on line 49 passes the value straight through. Per BlogPostSchema, image is an optional string and is typically authored as a relative path (same as author avatars). RSS/Atom/JSON readers won't resolve it against the site origin, so thumbnails will be broken.

Additionally, the feed description 'a fast, modern browser for the npm registry' is lower-case here, but nuxt.config.ts (site.description) uses the upper-case 'A fast, …'. Consider deriving both the title/description and the site URL from the shared site config (e.g. via useSiteConfig()) so these three places — feeds.ts, nuxt.config.ts, and app/pages/blog/index.vue — cannot drift.

🛠️ Proposed fix for the absolute image URL
-      image: post.image,
+      image: post.image ? new URL(post.image, 'https://npmx.dev').toString() : undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/feeds.ts` around lines 19 - 54, generateFeed currently passes
post.image through unchanged causing broken thumbnails; update the feed.addItem
call in function generateFeed to normalize post.image the same way author.avatar
is normalized (e.g., if post.image exists, set image to new URL(post.image,
siteUrl).toString()), and replace the hard-coded title/description/id/link
values with values pulled from the shared site config (useSiteConfig() or
equivalent) so title/description/site URL are derived from the single source of
truth used by nuxt.config.ts and app/pages/blog/index.vue.

Copy link
Copy Markdown
Contributor

@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: 1

♻️ Duplicate comments (1)
server/utils/feeds.ts (1)

49-49: ⚠️ Potential issue | 🟠 Major

Normalise post.image to an absolute URL.

post.image can be a relative authored value, unlike the avatar path which is already normalised. Passing it through can break thumbnails in feed readers.

Proposed fix
-      image: post.image,
+      image: post.image ? new URL(post.image, 'https://npmx.dev').toString() : undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/feeds.ts` at line 49, post.image is sometimes a relative URL and
needs to be converted to an absolute URL before being emitted in the feed;
update the code that sets image: post.image in server/utils/feeds.ts to
normalise post.image to an absolute URL (using the app/site base origin from
your config or request context), e.g. resolve relative paths against the site
base and ensure the value is an absolute URL string, and reuse any existing
avatar-normalisation helper if available so feed thumbnails don’t break.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/utils/feeds.ts`:
- Around line 42-47: The feed item currently sets author: post.authors.map(...)
which creates an authors array that the feed library ignores past the first
element; change this to use only the primary author (e.g., author:
post.authors[0] transformed the same way) so co-authors are not silently
dropped, and add a brief comment near the author assignment referencing the feed
library/JSON Feed 1.0 limitation (or alternately note in docs that only the
first author is preserved); update the transformation logic used in author
mapping (name, link, absolute avatar URL) to operate on post.authors[0] and
ensure null/empty checks handle when there is no author.

---

Duplicate comments:
In `@server/utils/feeds.ts`:
- Line 49: post.image is sometimes a relative URL and needs to be converted to
an absolute URL before being emitted in the feed; update the code that sets
image: post.image in server/utils/feeds.ts to normalise post.image to an
absolute URL (using the app/site base origin from your config or request
context), e.g. resolve relative paths against the site base and ensure the value
is an absolute URL string, and reuse any existing avatar-normalisation helper if
available so feed thumbnails don’t break.
🪄 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: 6a61b6bf-8a0c-4faa-b54a-2193e8891cd7

📥 Commits

Reviewing files that changed from the base of the PR and between 453ceb5 and 1389051.

📒 Files selected for processing (2)
  • server/utils/feeds.ts
  • test/e2e/feeds.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • test/e2e/feeds.spec.ts

Comment thread server/utils/feeds.ts
Comment on lines +42 to +47
author: post.authors.map(author => ({
name: author.name,
link: author.profileUrl ?? undefined,
// author.avatar is a relative URL - make it absolute to work in feed readers
avatar: author.avatar ? new URL(author.avatar, 'https://npmx.dev').toString() : undefined,
})),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Description: Verify how the locked `feed` dependency serialises multiple item authors to JSON.
# Expected: If the generated item has only `author` and no `authors` array, co-authors are not preserved in /feed.json.

node --input-type=module <<'NODE'
import { Feed } from 'feed'

const feed = new Feed({
  title: 'Test feed',
  description: 'Test feed',
  id: 'https://example.com/',
  link: 'https://example.com/',
  copyright: 'Test',
})

feed.addItem({
  title: 'Multi-author post',
  id: 'https://example.com/posts/multi-author',
  link: 'https://example.com/posts/multi-author',
  date: new Date('2026-01-01T00:00:00.000Z'),
  author: [
    { name: 'Author One' },
    { name: 'Author Two' },
  ],
})

const item = JSON.parse(feed.json1()).items[0]
console.log(JSON.stringify(item, null, 2))
console.log('has_authors_array=' + Array.isArray(item.authors))
console.log('single_author_name=' + (item.author?.name ?? ''))
NODE

Repository: npmx-dev/npmx.dev

Length of output: 1061


🏁 Script executed:

git ls-files | head -20

Repository: npmx-dev/npmx.dev

Length of output: 699


🏁 Script executed:

cat -n server/utils/feeds.ts | head -60

Repository: npmx-dev/npmx.dev

Length of output: 2006


🏁 Script executed:

cat package.json

Repository: npmx-dev/npmx.dev

Length of output: 5846


🏁 Script executed:

# Install feed and test multi-author serialization
cd /tmp && npm install feed@5.2.0 --silent 2>&1 | tail -5

Repository: npmx-dev/npmx.dev

Length of output: 43


🌐 Web query:

feed npm package 5.2.0 multi-author JSON Feed 1.1 serialization

💡 Result:

The npm package "feed" version 5.2.0 (published Jan 12, 2026) is a Node.js library for generating RSS 2.0, Atom 1.0, and JSON Feed 1.0 feeds. It supports serialization to JSON Feed via the feed.json1 method. Official documentation and repo explicitly state "JSON Feed 1.0" support (e.g., README: "console.log(feed.json1); // Output: JSON Feed 1.0"). No mentions of JSON Feed 1.1 support (which adds authors array, deprecates singular author, adds language) in v5.2.0 changelog, releases, or docs. JSON Feed 1.1 spec dates to 2020, but package labels it as 1.0. Multi-author support: Uses singular author object for feed-level author (with avatar JSON-only) and addContributor for additional contributors (array). This maps to JSON Feed 1.0's singular author + contributors array, not 1.1's authors array. No evidence of authors array output in 1.1 format. Example usage for JSON Feed serialization: import { Feed } from 'feed'; const feed = new Feed({ title: 'Example Feed', id: 'https://example.com', link: 'https://example.com', description: 'Multi-author feed example', author: { name: 'Primary Author', email: 'primary@example.com' }, // ... }); feed.addContributor({ name: 'Contributor 1', link: 'https://example.com/c1' }); feed.addContributor({ name: 'Contributor 2' }); const jsonFeed = feed.json1; // Serializes to JSON Feed 1.0 format To confirm exact output, inspect feed.json1 string (starts with {"version":"https://jsonfeed.org/version/1.0",...} with "author":{...} and "contributors":[...], not 1.1 authors array).

Citations:


🏁 Script executed:

cd /tmp && cat > test-feed.js << 'EOF'
import { Feed } from 'feed';

const feed = new Feed({
  title: 'Test',
  description: 'Test',
  id: 'https://example.com/',
  link: 'https://example.com/',
});

// Test 1: Array in author field (what the code does)
feed.addItem({
  title: 'Multi-author (array)',
  id: '1',
  link: 'https://example.com/1',
  date: new Date('2026-01-01'),
  author: [
    { name: 'Author One', link: 'https://example.com/a1' },
    { name: 'Author Two', link: 'https://example.com/a2' },
  ],
});

const json = JSON.parse(feed.json1());
const item = json.items[0];
console.log('=== TEST: Array in author field ===');
console.log('JSON Feed version:', json.version);
console.log('Item author field type:', typeof item.author);
console.log('Item author:', JSON.stringify(item.author, null, 2));
console.log('Has authors array:', Array.isArray(item.authors));
console.log('Item contributors:', JSON.stringify(item.contributors || 'NONE', null, 2));
EOF
node test-feed.js

Repository: npmx-dev/npmx.dev

Length of output: 697


🏁 Script executed:

cd /tmp && cat > test-feed-contributors.js << 'EOF'
import { Feed } from 'feed';

const feed = new Feed({
  title: 'Test',
  description: 'Test',
  id: 'https://example.com/',
  link: 'https://example.com/',
});

// Test: Using contributors parameter
feed.addItem({
  title: 'Multi-author (with contributors)',
  id: '2',
  link: 'https://example.com/2',
  date: new Date('2026-01-01'),
  author: { name: 'Author One', link: 'https://example.com/a1' },
  contributors: [
    { name: 'Author Two', link: 'https://example.com/a2' },
    { name: 'Author Three', link: 'https://example.com/a3' },
  ],
});

const json = JSON.parse(feed.json1());
const item = json.items[0];
console.log('=== TEST: contributors parameter ===');
console.log('Item author:', JSON.stringify(item.author, null, 2));
console.log('Item contributors:', JSON.stringify(item.contributors || 'NONE', null, 2));
EOF
node test-feed-contributors.js

Repository: npmx-dev/npmx.dev

Length of output: 605


🏁 Script executed:

cd /tmp && cat > inspect-feed.js << 'EOF'
import { Feed } from 'feed';

const feed = new Feed({
  title: 'Test',
  description: 'Test',
  id: 'https://example.com/',
  link: 'https://example.com/',
});

feed.addItem({
  title: 'Test item',
  id: '1',
  link: 'https://example.com/1',
  date: new Date('2026-01-01'),
  author: { name: 'Single Author' },
});

// Check available methods
console.log('Feed methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(feed))
  .filter(m => m.startsWith('json') || m.includes('feed') || m.includes('item')));

// Full JSON Feed 1.0 output
const json1Output = JSON.parse(feed.json1());
console.log('\njson1() version:', json1Output.version);
console.log('First item keys:', Object.keys(json1Output.items[0]));
EOF
node inspect-feed.js

Repository: npmx-dev/npmx.dev

Length of output: 592


Co-authors are lost when serialising to JSON Feed because the feed library only preserves the first author.

The code passes an array to the author field, but the feed library (v5.2.0) outputs JSON Feed 1.0 format, which supports only a single author object per item—not an authors array. When an array is passed, the library silently uses only the first author and discards the rest. The JSON Feed 1.0 spec has no mechanism to represent co-authors at the item level.

To preserve co-author attribution, either use only the first author (author: post.authors[0]) and document the limitation, or consider an alternative feed library with JSON Feed 1.1 support. JSON Feed 1.1 introduced the authors array field, though the current feed library does not yet support it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/feeds.ts` around lines 42 - 47, The feed item currently sets
author: post.authors.map(...) which creates an authors array that the feed
library ignores past the first element; change this to use only the primary
author (e.g., author: post.authors[0] transformed the same way) so co-authors
are not silently dropped, and add a brief comment near the author assignment
referencing the feed library/JSON Feed 1.0 limitation (or alternately note in
docs that only the first author is preserved); update the transformation logic
used in author mapping (name, link, absolute avatar URL) to operate on
post.authors[0] and ensure null/empty checks handle when there is no author.

@Kiwow Kiwow marked this pull request as draft April 19, 2026 21:42
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.

RSS support for the blog

2 participants