{"version":"https://jsonfeed.org/version/1.1","title":"Alessandro Jean's Blog","description":"Just a personal blog.","authors":[{"name":"Alessandro Jean","avatar":"https://alessandrojean.github.io/img/avatar-okabe-small.webp"}],"language":"en-US","home_page_url":"https://alessandrojean.github.io/blog","feed_url":"https://alessandrojean.github.io/blog/feed.json","icon":"https://alessandrojean.github.io/img/apple-touch-icon.png","items":[{"id":"generating-an-rss-feed-with-nuxt-content","url":"https://alessandrojean.github.io/post/generating-an-rss-feed-with-nuxt-content","title":"Generating an RSS feed with Nuxt Content","summary":"A simple walk through on how to create a feed and also provide the posts HTML.","content_html":"<p>Providing an RSS feed when you’re using Nuxt Content isn’t really a hard task, but you may find some harsh obstacles on the way.</p>\n<h2 id=\"generating-the-feed\">Generating the feed</h2>\n<p>We will first start by creating the server route which will serve the RSS. Usually it’s convenient to put the feed in the same post where you content is. For example, if your post list is at <code>/blog</code>, the path <code>/blog/feed.xml</code> seems like a good place.</p>\n<p>In Nuxt, we can create such a route by creating the file <code>server/routes/blog/feed.xml.get.ts</code>.</p>\n<div><p><strong>Caution:</strong></p><p>This isn’t an API route, so it shouldn’t be placed at <code>/server/api</code>.</p></div>\n<p>The skeleton of the file is basically this:</p>\n<pre><code class=\"language-ts\">import { queryCollection } from '@nuxt/content/server';\n\nexport default defineEventHandler(async (event) => {\n  const posts = await queryCollection(event, 'blog')\n    .order('created_at', 'DESC')\n    .limit(10)\n    .all();\n\n  return posts;\n});\n</code></pre>\n<p>What we want to do is manipulate the posts in such a way that the route actually returns a readable RSS feed. We can use the package <a href=\"https://npmjs.com/package/rss\" target=\"_blank\" rel=\"noopener noreferrer external\">rss</a> to help us, as it contains an RSS builder which will make things easier.</p>\n<p>We will need to use the <code>RSS</code> class.</p>\n<pre><code class=\"language-ts\">import RSS from 'rss';\n</code></pre>\n<p>To get started, we need to create the feed and provide some basic information.</p>\n<pre><code class=\"language-ts\">const url = 'https://example.org';\n\nconst feed = new RSS({\n  title: 'Example Blog',\n  description: 'Just a blog.',\n  site_url: url,\n  feed_url: `${url}/blog/feed.xml`,\n  language: 'en-US',\n  custom_elements: [\n    { icon: `${url}/img/apple-touch-icon.png` },\n  ],\n  custom_namespaces: {\n    content: 'http://purl.org/rss/1.0/modules/content/',\n    dc: 'http://purl.org/dc/elements/1.1/',\n    sy: 'http://purl.org/rss/1.0/modules/syndication/',\n  },\n});\n</code></pre>\n<p>Some of the properties are pretty straightfoward. Other’s are just complements, such as <code>custom_namespaces</code>, which allows the generated XML to have extra tags that are not in the RSS specification.</p>\n<p>Since we have the array of posts, we can iterate over it to create each feed item.</p>\n<pre><code class=\"language-ts\">for (const post of posts) {\n  feed.item({\n    title: post.title,\n    guid: `${url}/post/${post.path}`,\n    url: `${url}/post/${post.path}`,\n    description: post.description,\n    date: new Date(post.created_at),\n    categories: post.category ? [post.category] : undefined,\n    custom_elements: [\n      { 'dc:creator': { _cdata: 'John Doe' } },\n    ],\n  });\n}\n</code></pre>\n<p>Now that the feed is complete, we need to define the <code>Content-Type</code> header.</p>\n<pre><code class=\"language-ts\">setResponseHeader(event, 'Content-Type', 'text/xml');\n</code></pre>\n<p>Then we can finally return the XML feed.</p>\n<pre><code class=\"language-ts\">return feed.xml();\n</code></pre>\n<p>You can have a look at <a href=\"https://alessandrojean.github.io/blog/feed.xml\">this site’s RSS</a> for an example.</p>\n<h2 id=\"prerendering\">Prerendering</h2>\n<p>If your site is deployed with an server, you’re done. But if it is deployed as an static site, such when you use GitHub Pages, you need one additional step.</p>\n<p>We need to inform Nuxt to also prerender this server route during build. This can be done by editing the <code>nitro</code> property in <code>nuxt.config.ts</code>.</p>\n<pre><code class=\"language-ts\">export default defineNuxtConfig({\n  nitro: {\n    prerender: {\n      routes: ['/blog/feed.xml'],\n    },\n  },\n});\n</code></pre>\n<h2 id=\"informing-rss-readers-about-the-feed\">Informing RSS readers about the feed</h2>\n<p>You can put a link to the RSS feed in your page, but this will only work for the reader to click and copy it in their RSS reader manually. You can make things easier a bit by providing a <code>&#x3C;link rel=\"alternate\"></code> tag in your HTML <code>&#x3C;head></code>.</p>\n<p>With this, if the reader just paste your blog URL in the RSS reader, it can get the RSS link automatically. In Nuxt, this can be done with the <code>useHead</code> composable.</p>\n<pre><code class=\"language-ts\">useHead({\n  link: [{ \n    rel: 'alternate', \n    type: 'application/rss+xml', \n    title: 'Feed (RSS)', \n    href: '/blog/feed.xml',\n  }],\n});\n</code></pre>\n<p>This is not mandatory, but it is considered a good practice.</p>\n<h2 id=\"providing-the-post-html\">Providing the post HTML</h2>\n<p>The current feed will work fine for all readers, but we’re just providing the links, there’s no body for the posts. While most sites do just that, I think it’s nice to also provide the full post content, so the reader can read in their preferred environment.</p>\n<p>This is the most tricky part to do with Nuxt Content. As in the moment of writing this article, Nuxt Content doesn’t seem to have a method to return your post rendered HTML in the server context. You need to do this <strong>manually</strong>.</p>\n<p>Nuxt Content uses <a href=\"https://npmjs.com/package/@nuxtjs/mdc\" target=\"_blank\" rel=\"noopener noreferrer external\">@nuxtjs/mdc</a> under the hoods, which then uses Unified’s <a href=\"https://npmjs.com/package/remark\" target=\"_blank\" rel=\"noopener noreferrer external\">remark</a> and <a href=\"https://npmjs.com/package/rehype\" target=\"_blank\" rel=\"noopener noreferrer external\">rehype</a>.</p>\n<p>If you take a look into the <code>post.body</code> property, it is a Minimark AST. We could use the <a href=\"https://npmjs.com/package/minimark\" target=\"_blank\" rel=\"noopener noreferrer external\">minimark</a> package to get the Markdown string, but I faced some converting issues when doing that. It is easier if you get the Markdown source file content directly instead.</p>\n<p>To make things reusable, we will create a server util function <code>markdownToHtml</code>.</p>\n<pre><code class=\"language-ts\">import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport async function markdownToHtml(fileName: string) {\n  const filePath = join(process.cwd(), 'content', `${fileName}.md`);\n  const markdown = await readFile(filePath, 'utf-8');\n}\n</code></pre>\n<p>The tricky part is creating a Unified pipeline to do a similar conversion that Nuxt Content does. We can get started by importing the plugins.</p>\n<pre><code class=\"language-ts\">import rehypeSlug from 'rehype-slug';\nimport rehypeStringify from 'rehype-stringify';\nimport remarkFrontmatter from 'remark-frontmatter';\nimport remarkGfm from 'remark-gfm';\nimport remarkMdc from 'remark-mdc';\nimport remarkParse from 'remark-parse';\nimport remarkRehype from 'remark-rehype';\nimport { unified } from 'unified';\n</code></pre>\n<p>And then doing the pipeline.</p>\n<pre><code class=\"language-ts\">const result = await unified()\n  .use(remarkParse)\n  .use(remarkFrontmatter, ['yaml'])\n  .use(remarkGfm)\n  .use(remarkMdc)\n  .use(remarkRehype)\n  .use(rehypeSlug)\n  .use(rehypeStringify)\n  .process(markdown);\n\nreturn String(result);\n</code></pre>\n<p>This will parse the Markdown, generate an AST, which will then be converted to HAST and processed until it is joined to a final HTML string.</p>\n<p>For most cases, this may do the job, but MDC supports Vue components, which is a problem. The approach I choose was to convert the components I use into simpler and equivalent ones by using just HTML existing tags.</p>\n<p>If you have any component into your Markdown, this existing pipeline will put it in the HTML as if it is a custom element. For example, <code>Note.vue</code>, called by <code>::note</code> in your Markdown, will be outputted as <code>&#x3C;note>&#x3C;/note></code>, which is not an existing HTML element.</p>\n<p>We can circumvent this by using the <npm pkg=\"rehype-components\"></npm> plugin. We will also need to install the <a href=\"https://npmjs.com/package/hastscript\" target=\"_blank\" rel=\"noopener noreferrer external\">hastscript</a> package.</p>\n<p>What this plugin does is converting custom elements into other you specify. We can put in the pipeline with the following code.</p>\n<pre><code class=\"language-ts\">.use(rehypeComponents, {\n  components: {\n    'note': Note,\n  },\n})\n</code></pre>\n<p>And then we create the <code>Note</code> component replacement. In this case, the <code>Note</code> element is originally a callout. The approach I choosed was to transform it into a <code>&#x3C;div></code> with a <code>&#x3C;p>&#x3C;strong>Note:&#x3C;/strong>&#x3C;/p></code> as the first child.</p>\n<p>The <code>Note</code> component can be defined as so:</p>\n<pre><code class=\"language-ts\">import { h } from 'hastscript';\nimport type { ComponentFunction } from 'rehype-components';\n\nconst Note: ComponentFunction = (_, children) => h('div', [\n  h('p', h('strong', 'Note:')),\n  ...children,\n]);\n</code></pre>\n<p>For example, the following <code>Note</code> block in Markdown</p>\n<pre><code class=\"language-mdc\">::note\nThis is a note\n::\n</code></pre>\n<p>Will be transformed into the following HTML:</p>\n<pre><code class=\"language-html\">&#x3C;div>\n  &#x3C;p>&#x3C;strong>Note:&#x3C;/strong>&#x3C;/p>\n  &#x3C;p>This is a note&#x3C;/p>\n&#x3C;/div>\n</code></pre>\n<p>As you might have guessed, you will need to do that for each custom Vue component you use in your Markdown files.</p>\n<p>With the function completed, we can put the result into each feed item:</p>\n<pre><code class=\"language-ts\">feed.item({\n  title: post.title,\n  guid: `${url}/post/${slug}`,\n  url: `${url}/post/${slug}`,\n  description: post.description,\n  date: new Date(post.created_at),\n  categories: post.category ? [post.category] : undefined,\n  custom_elements: [\n    { 'dc:creator': { _cdata: 'John Doe' } },\n    { 'content:encoded': { _cdata: await markdownToHtml(post.path) } }, // [!CODE ++]\n  ],\n});\n</code></pre>\n<p>Now the RSS feed is fully complete with your posts’ content as well.</p>\n<h2 id=\"other-approach-i-have-considered\">Other approach I have considered</h2>\n<p>The alternative idea I had to solve this issue was using one of the Nuxt hooks after the build. You can write a custom script to parse the <code>/blog</code> generated HTML to get the links, and then parse the each post generated file to get the text part.</p>\n<p>This will likely do the job, but depending on your custom components, you might get a dirty HTML than just parsing the Markdown. For example, some of the <code>&#x3C;pre></code> blocks I use here are customized to include a fancier window around it with the file name and also a button to copy the code content. If you don’t handle these edge cases, the RSS readers might not render the content in a desirable way.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>Would be nice if the Nuxt team provides an easier way to do this, as RSS feeds are still very helpful and used by some people. Maybe with a potential rewrite of the module to use Comark they implement it? Who knows.</p>","date_published":"2026-06-16T00:00:00.000Z","date_modified":"2026-06-19T18:36:00.000Z","tags":["Programming"],"language":"en-US"}]}