diff --git a/package-lock.json b/package-lock.json index a2dbf383..3d7beb5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "angular-twitter-timeline": "^15.0.0", "animate.css": "^4.1.1", "clipboard": "^2.0.11", + "feed": "^4.2.2", "front-matter": "^4.0.2", "marked": "^11.1.1", "marked-gfm-heading-id": "^3.1.0", @@ -14415,6 +14416,17 @@ "bser": "2.1.1" } }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/figures": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", @@ -23411,8 +23423,7 @@ "node_modules/sax": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "optional": true + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/saxes": { "version": "6.0.0", @@ -26865,6 +26876,17 @@ "node": ">= 6" } }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index adaca1d9..bbd39ff7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "angular-twitter-timeline": "^15.0.0", "animate.css": "^4.1.1", "clipboard": "^2.0.11", + "feed": "^4.2.2", "front-matter": "^4.0.2", "marked": "^11.1.1", "marked-gfm-heading-id": "^3.1.0", diff --git a/src/server/routes/rss.xml.ts b/src/server/routes/rss.xml.ts new file mode 100644 index 00000000..29745a2c --- /dev/null +++ b/src/server/routes/rss.xml.ts @@ -0,0 +1,90 @@ +import { Feed } from 'feed'; +import * as fs from 'fs'; +import { defineEventHandler } from 'h3'; +import { marked } from 'marked'; +import * as path from 'path'; +import fm from 'front-matter'; +import { PostAttributes } from 'src/app/types'; +import { createHash } from 'crypto'; + +function generateReproducibleHash(input: string): string { + const hash = createHash('md5'); // You can use other algorithms like 'sha256', 'sha1', etc. + + // Update the hash with the input data + hash.update(input); + + // Generate a hexadecimal digest + const digest = hash.digest('hex'); + + // Format the digest as a GUID + const formattedGuid = `${digest.substring(0, 8)}-${digest.substring( + 8, + 12, + )}-${digest.substring(12, 16)}-${digest.substring(16, 20)}-${digest.substring( + 20, + )}`; + + return formattedGuid; +} + +export default defineEventHandler((event) => { + const baseDir = './src/content/blog'; + const baseUrl = 'https://k9n.dev'; + function generateRssFeed(): string { + const mainFeed = new Feed({ + title: 'k9n.dev Blog', + id: `${baseUrl}`, // replace with your site URL + link: `${baseUrl}/api/rss.xml`, // replace with your feed URL + language: 'de', + copyright: '2024 by Danny Koppenhagen', + }); + + const files = fs.readdirSync(baseDir); + files.forEach((file) => { + const filePath = path.join(baseDir, file); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const frontmatter = fm(fileContent); + + const content = marked(frontmatter.body) as string; + + const headerImage = frontmatter.attributes.thumbnail?.header; + const cardImage = frontmatter.attributes.thumbnail?.header; + let image = + typeof headerImage === 'string' + ? headerImage + : typeof cardImage === 'string' + ? cardImage + : undefined; + image = + image && /^https?:\/\//i.test(image) ? image : `${baseUrl}/${image}`; + // Add item to the main feed + mainFeed.addItem({ + title: frontmatter.attributes.title, + id: generateReproducibleHash(file), // replace with your item URL + link: `${baseUrl}/blog/${file.replace(/\.md$/, '')}`, // replace with your item URL + description: frontmatter.attributes.description, + date: new Date( + frontmatter.attributes.updated || frontmatter.attributes.created, + ), + image, + content: content, + author: [ + { + name: frontmatter.attributes.author.name, + email: frontmatter.attributes.author.mail, + link: baseUrl, + }, + ], + published: new Date(frontmatter.attributes.created), + }); + }); + + return mainFeed.rss2(); + } + + // Output the combined RSS feed as a string + const rssFeedString = generateRssFeed(); + + event.node.res.setHeader('content-type', 'text/xml'); + event.node.res.end(rssFeedString); +}); diff --git a/vite.config.mts b/vite.config.mts index 8004c30b..df831559 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -20,6 +20,7 @@ export default defineConfig(({ mode }) => ({ analog({ prerender: { routes: async () => [ + '/api/rss.xml', '/', '/blog', '/contact',