├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .nuxtrc ├── .stackblitz ├── .gitignore ├── .npmrc ├── README.md ├── app.vue ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── server │ └── plugins │ │ └── feed.ts └── tsconfig.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── package.json ├── playground ├── app.vue ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── server │ ├── plugins │ │ └── feed.ts │ └── tsconfig.json └── tsconfig.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── module.ts ├── runtime │ └── server │ │ └── feed.ts └── types.ts ├── test ├── __snapshots__ │ └── feed.test.ts.snap ├── feed.test.ts ├── fixtures │ └── feed │ │ ├── app.vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ └── server │ │ └── plugins │ │ └── feed.ts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nuxt/eslint-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["*"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | # Build job 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Setup Node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: "18" 22 | cache: ${{ steps.detect-package-manager.outputs.manager }} 23 | - name: Restore cache 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | dist 28 | .nuxt 29 | key: ${{ runner.os }}-nuxt-build-${{ hashFiles('dist') }} 30 | restore-keys: | 31 | ${{ runner.os }}-nuxt-build- 32 | - name: Install pnpm 33 | run: npm install -g pnpm 34 | - name: Install dependencies 35 | run: pnpm install 36 | - name: Prepare env 37 | run: pnpm run dev:prepare 38 | - name: Lint 39 | run: pnpm run lint 40 | - name: Tests 41 | run: pnpm run test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit ${1} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | typescript.tsConfig.compilerOptions.moduleResolution=Bundler 2 | -------------------------------------------------------------------------------- /.stackblitz/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.stackblitz/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.stackblitz/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /.stackblitz/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.stackblitz/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: ['nuxt-module-feed'], 4 | feed: { 5 | sources: [ 6 | { 7 | path: '/feed.xml', 8 | cacheTime: 60 * 15, 9 | type: 'rss2' 10 | }, 11 | { 12 | path: '/feed.atom', 13 | cacheTime: 60 * 15, 14 | type: 'atom1' 15 | }, 16 | { 17 | path: '/feed.json', 18 | cacheTime: 60 * 15, 19 | type: 'json1' 20 | } 21 | ] 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /.stackblitz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "devDependencies": { 11 | "nuxt": "3.10.3" 12 | }, 13 | "dependencies": { 14 | "nuxt-module-feed": "1.1.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.stackblitz/server/plugins/feed.ts: -------------------------------------------------------------------------------- 1 | import type { NitroCtx } from 'nuxt-module-feed' 2 | 3 | export default defineNitroPlugin((nitroApp) => { 4 | nitroApp.hooks.hook('feed:generate', async ({ feed }: NitroCtx) => { 5 | feed.options = { 6 | id: 'Test Feed', 7 | title: 'Test Feed', 8 | copyright: 'Test company', 9 | updated: new Date("2023-01-01T00:00:00Z") 10 | } 11 | 12 | type Post = { 13 | title: string; 14 | url: string; 15 | description: string; 16 | content: string; 17 | date: Date; 18 | image: string; 19 | }; 20 | 21 | const posts: Post[] = [ 22 | { 23 | title: "Post 1", 24 | url: "https://example.com/post-1", 25 | description: "This is the first post", 26 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 27 | date: new Date("2022-01-01"), 28 | image: "https://example.com/images/post1.jpg", 29 | }, 30 | { 31 | title: "Post 2", 32 | url: "https://example.com/post-2", 33 | description: "This is the second post", 34 | content: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 35 | date: new Date("2022-01-05"), 36 | image: "https://example.com/images/post2.jpg", 37 | }, 38 | { 39 | title: "Post 3", 40 | url: "https://example.com/post-3", 41 | description: "This is the third post", 42 | content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 43 | date: new Date("2022-01-10"), 44 | image: "https://example.com/images/post3.jpg", 45 | }, 46 | { 47 | title: "Post 4", 48 | url: "https://example.com/post-4", 49 | description: "This is the fourth post", 50 | content: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 51 | date: new Date("2022-01-15"), 52 | image: "https://example.com/images/post4.jpg", 53 | }, 54 | { 55 | title: "Post 5", 56 | url: "https://example.com/post-5", 57 | description: "This is the fifth post", 58 | content: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 59 | date: new Date("2022-01-20"), 60 | image: "https://example.com/images/post5.jpg", 61 | }, 62 | ]; 63 | 64 | posts.forEach(post => { 65 | feed.addItem({ 66 | title: post.title, 67 | id: post.url, 68 | link: post.url, 69 | description: post.description, 70 | content: post.content, 71 | date: post.date 72 | }) 73 | }) 74 | 75 | feed.addCategory('Nuxt.js') 76 | 77 | feed.addContributor({ 78 | name: 'Miha Sedej', 79 | email: 'sedej.miha@gmail.com', 80 | link: 'https://tresko.dev/' 81 | }) 82 | }) 83 | }) -------------------------------------------------------------------------------- /.stackblitz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v1.1.4 5 | 6 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.1.2...v1.1.4) 7 | 8 | ### 🩹 Fixes 9 | 10 | - **deps:** Update nuxtjs monorepo to v3.10.3 ([3a9cb4a](https://github.com/tresko/nuxt-module-feed/commit/3a9cb4a)) 11 | - **deps:** Update dependency nuxt-module-feed to v1.1.0 ([f3076fa](https://github.com/tresko/nuxt-module-feed/commit/f3076fa)) 12 | 13 | ### 🏡 Chore 14 | 15 | - Update stackblitz deps ([0a070df](https://github.com/tresko/nuxt-module-feed/commit/0a070df)) 16 | - Fix types ([26a486d](https://github.com/tresko/nuxt-module-feed/commit/26a486d)) 17 | 18 | ### ❤️ Contributors 19 | 20 | - Miha Sedej 21 | 22 | ## v1.1.2 23 | 24 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.1.0...v1.1.2) 25 | 26 | ### 🏡 Chore 27 | 28 | - Update dependencies ([6dfb535](https://github.com/tresko/nuxt-module-feed/commit/6dfb535)) 29 | - Update dependencies ([3ed7361](https://github.com/tresko/nuxt-module-feed/commit/3ed7361)) 30 | - Set schema version in resolutions ([6d8b485](https://github.com/tresko/nuxt-module-feed/commit/6d8b485)) 31 | 32 | ### ❤️ Contributors 33 | 34 | - Miha Sedej 35 | 36 | ## v1.1.0 37 | 38 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.0.3...v1.1.0) 39 | 40 | 41 | ### 🚀 Enhancements 42 | 43 | - Upgrade nuxt ([a5b06a9](https://github.com/tresko/nuxt-module-feed/commit/a5b06a9)) 44 | 45 | ### 🩹 Fixes 46 | 47 | - **deps:** Update dependency @nuxt/kit to v3.3.2 ([e655d7f](https://github.com/tresko/nuxt-module-feed/commit/e655d7f)) 48 | - **deps:** Update dependency @nuxt/kit to v3.3.3 ([f3a6d0c](https://github.com/tresko/nuxt-module-feed/commit/f3a6d0c)) 49 | 50 | ### 🏡 Chore 51 | 52 | - **release:** V1.0.3 ([05da935](https://github.com/tresko/nuxt-module-feed/commit/05da935)) 53 | - Add tsconfig.json ([7db333b](https://github.com/tresko/nuxt-module-feed/commit/7db333b)) 54 | - Add stackblitz ([fd1e991](https://github.com/tresko/nuxt-module-feed/commit/fd1e991)) 55 | - Fix pnpm lock file ([7e3f025](https://github.com/tresko/nuxt-module-feed/commit/7e3f025)) 56 | - Fix lock file ([f9438d0](https://github.com/tresko/nuxt-module-feed/commit/f9438d0)) 57 | - Fix lock file ([a15eb9e](https://github.com/tresko/nuxt-module-feed/commit/a15eb9e)) 58 | - Fix lock file ([b6fd5c7](https://github.com/tresko/nuxt-module-feed/commit/b6fd5c7)) 59 | - Fix lock file ([3726438](https://github.com/tresko/nuxt-module-feed/commit/3726438)) 60 | - Update ci ([56c7917](https://github.com/tresko/nuxt-module-feed/commit/56c7917)) 61 | 62 | ### ✅ Tests 63 | 64 | - Add tests ([6ede6b8](https://github.com/tresko/nuxt-module-feed/commit/6ede6b8)) 65 | - Update date ([faf2b68](https://github.com/tresko/nuxt-module-feed/commit/faf2b68)) 66 | 67 | ### ❤️ Contributors 68 | 69 | - Miha Sedej 70 | 71 | ## v1.0.3 72 | 73 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.0.2...v1.0.3) 74 | 75 | 76 | ### 🩹 Fixes 77 | 78 | - Fix feed impoort error ([0ca9f93](https://github.com/tresko/nuxt-module-feed/commit/0ca9f93)) 79 | 80 | ### 🏡 Chore 81 | 82 | - Update readme ([2cfbf22](https://github.com/tresko/nuxt-module-feed/commit/2cfbf22)) 83 | 84 | ### ❤️ Contributors 85 | 86 | - Miha Sedej 87 | 88 | ## v1.0.2 89 | 90 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.0.1...v1.0.2) 91 | 92 | 93 | ### 🩹 Fixes 94 | 95 | - Readme ([f8a3a1e](https://github.com/tresko/nuxt-module-feed/commit/f8a3a1e)) 96 | 97 | ### ❤️ Contributors 98 | 99 | - Miha Sedej 100 | 101 | ## v1.0.1 102 | 103 | [compare changes](https://github.com/tresko/nuxt-module-feed/compare/v1.0.0...v1.0.1) 104 | 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Miha Sedej 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-module-feed 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | [![Nuxt][nuxt-src]][nuxt-href] 7 | 8 | Feed module enables everyone to have RSS, Atom and JSON. 9 | 10 | - [✨  Release Notes](/CHANGELOG.md) 11 | 12 | 13 | ## Features 14 | 15 | 16 | 17 | - Nuxt 3 ready 18 | - Three different feed types (RSS 2.0, ATOM 1.0 and JSON 1.0) 19 | - Completely customizable 20 | - Multiple feeds 21 | - Works with all modes (SSR, SSG) 22 | 23 | ## Quick Setup 24 | 25 | 1. Add `nuxt-module-feed` dependency to your project 26 | 27 | ```bash 28 | npx nuxi@latest module add module-feed 29 | ``` 30 | 31 | 2. Add `nuxt-module-feed` to the `modules` section of `nuxt.config.ts` 32 | 33 | ```js 34 | export default defineNuxtConfig({ 35 | modules: ["nuxt-module-feed"], 36 | }); 37 | ``` 38 | 39 | That's it! You can now use nuxt-module-feed in your Nuxt app ✨ 40 | 41 | ## Configuration 42 | 43 | You can pass configuration to the module in the `nuxt.config.ts` like following: 44 | 45 | ```js 46 | export default { 47 | feed: { 48 | sources: [ 49 | { 50 | path: "/feed.xml", // The route to your feed. 51 | type: "rss2", // Can be: rss2, atom1, json1 52 | cacheTime: 60 * 15, // How long should the feed be cached 53 | }, 54 | { 55 | path: "/feed2.xml", // The route to your feed. 56 | type: "rss2", // Can be: rss2, atom1, json1 57 | cacheTime: 60 * 15, // How long should the feed be cached 58 | } 59 | ... 60 | ] 61 | }, 62 | }; 63 | ``` 64 | 65 | ## Nitro Hooks 66 | 67 | ### `feed:generate` 68 | 69 | **Type:** `async (ctx: { feed: Feed, options: SourceOptions }) => void | Promise` 70 | 71 | This hook allows you to modify the feed as runtime before it is sent to the client. 72 | 73 | Feed creation is based on the [feed](https://github.com/jpmonette/feed) package. Please use it as reference and further documentation for 74 | modifying the feed object that is passed to the create function. 75 | 76 | Note: It works for SSG and prerendered pages. 77 | 78 | ```ts 79 | import type { NitroCtx, Feed } from "nuxt-module-feed"; 80 | 81 | export default defineNitroPlugin((nitroApp) => { 82 | nitroApp.hooks.hook("feed:generate", async ({ feed, options }: NitroCtx) => { 83 | switch (options.path) { 84 | case "/feed.xml": { 85 | createTestFeed(feed); 86 | break; 87 | } 88 | case "/feed2.xml": { 89 | createTestFeed(feed); 90 | break; 91 | } 92 | ... 93 | } 94 | }); 95 | 96 | function createTestFeed(feed: Feed) { 97 | feed.options = { 98 | id: "Test Feed", 99 | title: "Test Feed", 100 | copyright: "Test company", 101 | }; 102 | 103 | type Post = { 104 | title: string; 105 | url: string; 106 | description: string; 107 | content: string; 108 | date: Date; 109 | image: string; 110 | }; 111 | 112 | const posts: Post[] = [ 113 | { 114 | title: "Post 1", 115 | url: "https://example.com/post-1", 116 | description: "This is the first post", 117 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 118 | date: new Date("2022-01-01"), 119 | image: "https://example.com/images/post1.jpg", 120 | }, 121 | { 122 | title: "Post 2", 123 | url: "https://example.com/post-2", 124 | description: "This is the second post", 125 | content: 126 | "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 127 | date: new Date("2022-01-05"), 128 | image: "https://example.com/images/post2.jpg", 129 | }, 130 | { 131 | title: "Post 3", 132 | url: "https://example.com/post-3", 133 | description: "This is the third post", 134 | content: 135 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 136 | date: new Date("2022-01-10"), 137 | image: "https://example.com/images/post3.jpg", 138 | }, 139 | { 140 | title: "Post 4", 141 | url: "https://example.com/post-4", 142 | description: "This is the fourth post", 143 | content: 144 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 145 | date: new Date("2022-01-15"), 146 | image: "https://example.com/images/post4.jpg", 147 | }, 148 | { 149 | title: "Post 5", 150 | url: "https://example.com/post-5", 151 | description: "This is the fifth post", 152 | content: 153 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 154 | date: new Date("2022-01-20"), 155 | image: "https://example.com/images/post5.jpg", 156 | }, 157 | ]; 158 | 159 | posts.forEach((post) => { 160 | feed.addItem({ 161 | title: post.title, 162 | id: post.url, 163 | link: post.url, 164 | description: post.description, 165 | content: post.content, 166 | date: post.date, 167 | }); 168 | }); 169 | 170 | feed.addCategory("Nuxt.js"); 171 | 172 | feed.addContributor({ 173 | name: "Miha Sedej", 174 | email: "sedej.miha@gmail.com", 175 | link: "https://tresko.dev/", 176 | }); 177 | } 178 | }); 179 | ``` 180 | 181 | Here is an [example](./playground/server/plugins/feed.ts). 182 | 183 | ## Development 184 | 185 | ```bash 186 | # Install dependencies 187 | pnpm install 188 | 189 | # Generate type stubs 190 | pnpm run dev:prepare 191 | 192 | # Develop with the playground 193 | pnpm run dev 194 | 195 | # Build the playground 196 | pnpm run dev:build 197 | 198 | # Run ESLint 199 | pnpm run lint 200 | 201 | # Run Vitest 202 | pnpm run test 203 | pnpm run test:watch 204 | 205 | # Release new version 206 | pnpm run release 207 | ``` 208 | 209 | 210 | 211 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-module-feed/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 212 | [npm-version-href]: https://www.npmjs.com/package/nuxt-module-feed 213 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-module-feed.svg?style=flat&colorA=18181B&colorB=28CF8D 214 | [npm-downloads-href]: https://www.npmjs.com/package/nuxt-module-feed 215 | [license-src]: https://img.shields.io/npm/l/nuxt-module-feed.svg?style=flat&colorA=18181B&colorB=28CF8D 216 | [license-href]: https://www.npmjs.com/package/nuxt-module-feed 217 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 218 | [nuxt-href]: https://nuxt.com 219 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-module-feed", 3 | "version": "1.1.4", 4 | "description": "Nuxt 3 feed module enables everyone to have RSS, Atom and JSON", 5 | "repository": "https://github.com/tresko/nuxt-module-feed", 6 | "license": "MIT", 7 | "contributors": [ 8 | "Miha Sedej " 9 | ], 10 | "type": "module", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/types.d.ts", 14 | "import": "./dist/module.mjs", 15 | "require": "./dist/module.cjs" 16 | } 17 | }, 18 | "main": "./dist/module.cjs", 19 | "types": "./dist/types.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "prepack": "nuxt-module-build build", 25 | "dev": "nuxi dev playground", 26 | "dev:generate": "nuxi generate playground", 27 | "dev:build": "nuxi build playground", 28 | "dev:preview": "nuxi preview playground", 29 | "dev:typecheck": "nuxi typecheck playground", 30 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", 31 | "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags", 32 | "lint": "eslint .", 33 | "test": "vitest run", 34 | "test:watch": "vitest watch", 35 | "prepare": "husky", 36 | "stackblitz": "cd .stackblitz && pnpm i && pnpm run dev" 37 | }, 38 | "dependencies": { 39 | "@nuxt/kit": "3.10.3", 40 | "feed": "4.2.2" 41 | }, 42 | "devDependencies": { 43 | "@commitlint/cli": "19.1.0", 44 | "@commitlint/config-conventional": "19.1.0", 45 | "@nuxt/eslint-config": "0.2.0", 46 | "@nuxt/module-builder": "0.5.5", 47 | "@nuxt/schema": "3.10.3", 48 | "@nuxt/test-utils": "3.11.0", 49 | "changelogen": "0.5.5", 50 | "eslint": "8.57.0", 51 | "husky": "9.0.11", 52 | "lint-staged": "15.2.2", 53 | "nuxt": "3.10.3", 54 | "vitest": "1.3.1" 55 | }, 56 | "lint-staged": { 57 | "*.+(js|ts|vue)": [ 58 | "eslint --fix", 59 | "git add" 60 | ] 61 | }, 62 | "stackblitz": { 63 | "installDependencies": false, 64 | "startCommand": "pnpm run stackblitz" 65 | }, 66 | "resolutions": { 67 | "@nuxt/kit": ">=3.2.3", 68 | "@nuxt/schema": "3.10.3" 69 | } 70 | } -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: [ 3 | '../src/module' 4 | ], 5 | feed: { 6 | sources: [ 7 | { 8 | path: '/feed.xml', 9 | type: 'rss2', 10 | cacheTime: 60 * 15 11 | }, 12 | { 13 | path: '/feed2.xml', 14 | type: 'rss2', 15 | cacheTime: 60 * 15 16 | }, 17 | ] 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt-module-feed-playground", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "preview": "nuxi preview", 9 | "generate": "nuxi generate" 10 | }, 11 | "devDependencies": { 12 | "nuxt": "3.10.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /playground/server/plugins/feed.ts: -------------------------------------------------------------------------------- 1 | import type { NitroCtx, Feed } from '../../../src/module' 2 | 3 | export default defineNitroPlugin((nitroApp) => { 4 | nitroApp.hooks.hook('feed:generate', async ({ feed, options }: NitroCtx) => { 5 | switch(options.path) { 6 | case '/feed.xml': { 7 | createTestFeed(feed) 8 | break 9 | } 10 | case '/feed2.xml': { 11 | createTestFeed(feed) 12 | break 13 | } 14 | } 15 | }) 16 | 17 | function createTestFeed(feed: Feed) { 18 | feed.options = { 19 | id: 'Test Feed', 20 | title: 'Test Feed', 21 | copyright: 'Test company' 22 | } 23 | 24 | type Post = { 25 | title: string; 26 | url: string; 27 | description: string; 28 | content: string; 29 | date: Date; 30 | image: string; 31 | }; 32 | 33 | const posts: Post[] = [ 34 | { 35 | title: "Post 1", 36 | url: "https://example.com/post-1", 37 | description: "This is the first post", 38 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 39 | date: new Date("2022-01-01"), 40 | image: "https://example.com/images/post1.jpg", 41 | }, 42 | { 43 | title: "Post 2", 44 | url: "https://example.com/post-2", 45 | description: "This is the second post", 46 | content: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 47 | date: new Date("2022-01-05"), 48 | image: "https://example.com/images/post2.jpg", 49 | }, 50 | { 51 | title: "Post 3", 52 | url: "https://example.com/post-3", 53 | description: "This is the third post", 54 | content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 55 | date: new Date("2022-01-10"), 56 | image: "https://example.com/images/post3.jpg", 57 | }, 58 | { 59 | title: "Post 4", 60 | url: "https://example.com/post-4", 61 | description: "This is the fourth post", 62 | content: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 63 | date: new Date("2022-01-15"), 64 | image: "https://example.com/images/post4.jpg", 65 | }, 66 | { 67 | title: "Post 5", 68 | url: "https://example.com/post-5", 69 | description: "This is the fifth post", 70 | content: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 71 | date: new Date("2022-01-20"), 72 | image: "https://example.com/images/post5.jpg", 73 | }, 74 | ]; 75 | 76 | posts.forEach(post => { 77 | feed.addItem({ 78 | title: post.title, 79 | id: post.url, 80 | link: post.url, 81 | description: post.description, 82 | content: post.content, 83 | date: post.date 84 | }) 85 | }) 86 | 87 | feed.addCategory('Nuxt.js') 88 | 89 | feed.addContributor({ 90 | name: 'Miha Sedej', 91 | email: 'sedej.miha@gmail.com', 92 | link: 'https://tresko.dev/' 93 | }) 94 | } 95 | }) -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule, createResolver, addServerHandler, addTemplate, addPrerenderRoutes } from '@nuxt/kit' 2 | import type { ModuleOptions, SourceOptions } from './types'; 3 | 4 | export * from './types' 5 | 6 | export default defineNuxtModule({ 7 | meta: { 8 | name: 'nuxt-module-feed', 9 | configKey: 'feed', 10 | }, 11 | defaults: { 12 | sources: [], 13 | }, 14 | setup (options, nuxt) { 15 | const resolver = createResolver(import.meta.url) 16 | const feedOptions: Record = {} 17 | 18 | options.sources.forEach((source) => { 19 | addServerHandler({ 20 | route: source.path, 21 | handler: resolver.resolve('./runtime/server/feed'), 22 | method: 'get', 23 | }) 24 | 25 | // Handle SSG 26 | if (nuxt.options._generate) { 27 | addPrerenderRoutes([source.path]) 28 | } 29 | 30 | feedOptions[source.path] = source 31 | }) 32 | 33 | nuxt.options.alias['#feed'] = (addTemplate({ 34 | filename: 'feedOptions.mjs', 35 | write: true, 36 | getContents: () => `export default ${JSON.stringify(feedOptions, null, 2)}` 37 | }).dst || '') 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /src/runtime/server/feed.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | // @ts-ignore 3 | import { defineEventHandler, setHeader, useNitroApp } from '#imports' 4 | import { Feed } from 'feed' 5 | // @ts-ignore 6 | import feedOptions from '#feed' 7 | import type { SourceOptions, FeedType } from '../../types' 8 | 9 | function resolveContentType(type: FeedType) { 10 | const lookup = { 11 | rss2: 'application/rss+xml', 12 | atom1: 'application/atom+xml', 13 | json1: 'application/json' 14 | } 15 | 16 | return lookup[type] 17 | } 18 | 19 | async function createFeed(options: SourceOptions): Promise { 20 | const feed = new Feed({ 21 | id: '', 22 | title: '', 23 | copyright: '', 24 | }) 25 | 26 | await useNitroApp().hooks.callHook('feed:generate', { feed, options }) 27 | 28 | return feed[options.type]() 29 | } 30 | 31 | export default defineEventHandler(async (event: H3Event) => { 32 | const options = (feedOptions as Record)[event.node.req.url as string] 33 | setHeader(event, 'content-type', resolveContentType(options.type)) 34 | setHeader(event, 'cache-control', `max-age=${options.cacheTime}`) 35 | const feed = await createFeed(options) 36 | return feed 37 | }) -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Feed } from 'feed' 2 | 3 | export type { Feed } from 'feed' 4 | 5 | export type FeedType = 'rss2' | 'atom1' | 'json1' 6 | 7 | export interface SourceOptions { 8 | path: string; 9 | type: FeedType; 10 | cacheTime: number; 11 | } 12 | 13 | export interface ModuleOptions { 14 | sources: SourceOptions[]; 15 | } 16 | 17 | export interface NitroCtx { 18 | feed: Feed; 19 | options: SourceOptions; 20 | } 21 | 22 | declare module 'nitropack' { 23 | interface NitroRuntimeHooks { 24 | 'feed:generate': (ctx: NitroCtx) => void; 25 | } 26 | } -------------------------------------------------------------------------------- /test/__snapshots__/feed.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ssr > renders the atom feed 1`] = ` 4 | " 5 | 6 | Test Feed 7 | Test Feed 8 | 2023-01-01T00:00:00.000Z 9 | https://github.com/jpmonette/feed 10 | Test company 11 | 12 | 13 | Miha Sedej 14 | sedej.miha@gmail.com 15 | https://tresko.dev/ 16 | 17 | 18 | <![CDATA[Post 1]]> 19 | https://example.com/post-1 20 | 21 | 2022-01-01T00:00:00.000Z 22 | 23 | 24 | 25 | 26 | <![CDATA[Post 2]]> 27 | https://example.com/post-2 28 | 29 | 2022-01-05T00:00:00.000Z 30 | 31 | 32 | 33 | 34 | <![CDATA[Post 3]]> 35 | https://example.com/post-3 36 | 37 | 2022-01-10T00:00:00.000Z 38 | 39 | 40 | 41 | 42 | <![CDATA[Post 4]]> 43 | https://example.com/post-4 44 | 45 | 2022-01-15T00:00:00.000Z 46 | 47 | 48 | 49 | 50 | <![CDATA[Post 5]]> 51 | https://example.com/post-5 52 | 53 | 2022-01-20T00:00:00.000Z 54 | 55 | 56 | 57 | " 58 | `; 59 | 60 | exports[`ssr > renders the json feed 1`] = ` 61 | { 62 | "items": [ 63 | { 64 | "content_html": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 65 | "date_modified": "2022-01-01T00:00:00.000Z", 66 | "id": "https://example.com/post-1", 67 | "summary": "This is the first post", 68 | "title": "Post 1", 69 | "url": "https://example.com/post-1", 70 | }, 71 | { 72 | "content_html": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 73 | "date_modified": "2022-01-05T00:00:00.000Z", 74 | "id": "https://example.com/post-2", 75 | "summary": "This is the second post", 76 | "title": "Post 2", 77 | "url": "https://example.com/post-2", 78 | }, 79 | { 80 | "content_html": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 81 | "date_modified": "2022-01-10T00:00:00.000Z", 82 | "id": "https://example.com/post-3", 83 | "summary": "This is the third post", 84 | "title": "Post 3", 85 | "url": "https://example.com/post-3", 86 | }, 87 | { 88 | "content_html": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 89 | "date_modified": "2022-01-15T00:00:00.000Z", 90 | "id": "https://example.com/post-4", 91 | "summary": "This is the fourth post", 92 | "title": "Post 4", 93 | "url": "https://example.com/post-4", 94 | }, 95 | { 96 | "content_html": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 97 | "date_modified": "2022-01-20T00:00:00.000Z", 98 | "id": "https://example.com/post-5", 99 | "summary": "This is the fifth post", 100 | "title": "Post 5", 101 | "url": "https://example.com/post-5", 102 | }, 103 | ], 104 | "title": "Test Feed", 105 | "version": "https://jsonfeed.org/version/1", 106 | } 107 | `; 108 | 109 | exports[`ssr > renders the rss feed 1`] = ` 110 | " 111 | 112 | 113 | Test Feed 114 | undefined 115 | undefined 116 | Sun, 01 Jan 2023 00:00:00 GMT 117 | https://validator.w3.org/feed/docs/rss2.html 118 | https://github.com/jpmonette/feed 119 | Test company 120 | Nuxt.js 121 | 122 | <![CDATA[Post 1]]> 123 | https://example.com/post-1 124 | https://example.com/post-1 125 | Sat, 01 Jan 2022 00:00:00 GMT 126 | 127 | 128 | 129 | 130 | <![CDATA[Post 2]]> 131 | https://example.com/post-2 132 | https://example.com/post-2 133 | Wed, 05 Jan 2022 00:00:00 GMT 134 | 135 | 136 | 137 | 138 | <![CDATA[Post 3]]> 139 | https://example.com/post-3 140 | https://example.com/post-3 141 | Mon, 10 Jan 2022 00:00:00 GMT 142 | 143 | 144 | 145 | 146 | <![CDATA[Post 4]]> 147 | https://example.com/post-4 148 | https://example.com/post-4 149 | Sat, 15 Jan 2022 00:00:00 GMT 150 | 151 | 152 | 153 | 154 | <![CDATA[Post 5]]> 155 | https://example.com/post-5 156 | https://example.com/post-5 157 | Thu, 20 Jan 2022 00:00:00 GMT 158 | 159 | 160 | 161 | 162 | " 163 | `; 164 | -------------------------------------------------------------------------------- /test/feed.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { fileURLToPath } from 'node:url' 3 | import { setup, $fetch } from '@nuxt/test-utils' 4 | 5 | describe('ssr', async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL('./fixtures/feed', import.meta.url)), 8 | server: true 9 | }) 10 | 11 | it('renders the rss feed', async () => { 12 | const html = await $fetch('/feed.xml') 13 | expect(await html.text()).toMatchSnapshot() 14 | }) 15 | 16 | it('renders the atom feed', async () => { 17 | const html = await $fetch('/feed.atom') 18 | expect(await html.text()).toMatchSnapshot() 19 | }) 20 | 21 | it('renders the json feed', async () => { 22 | const html = await $fetch('/feed.json') 23 | expect(html).toMatchSnapshot() 24 | }) 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/feed/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /test/fixtures/feed/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: [ 3 | '../../../src/module' 4 | ], 5 | 6 | feed: { 7 | sources: [ 8 | { 9 | path: '/feed.xml', 10 | cacheTime: 60 * 15, 11 | type: 'rss2' 12 | }, 13 | { 14 | path: '/feed.atom', 15 | cacheTime: 60 * 15, 16 | type: 'atom1' 17 | }, 18 | { 19 | path: '/feed.json', 20 | cacheTime: 60 * 15, 21 | type: 'json1' 22 | } 23 | ] 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /test/fixtures/feed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/feed/server/plugins/feed.ts: -------------------------------------------------------------------------------- 1 | import type { NitroCtx } from '../../../../../src/module' 2 | 3 | export default defineNitroPlugin((nitroApp) => { 4 | nitroApp.hooks.hook('feed:generate', async ({ feed }: NitroCtx) => { 5 | feed.options = { 6 | id: 'Test Feed', 7 | title: 'Test Feed', 8 | copyright: 'Test company', 9 | updated: new Date("2023-01-01T00:00:00Z") 10 | } 11 | 12 | type Post = { 13 | title: string; 14 | url: string; 15 | description: string; 16 | content: string; 17 | date: Date; 18 | image: string; 19 | }; 20 | 21 | const posts: Post[] = [ 22 | { 23 | title: "Post 1", 24 | url: "https://example.com/post-1", 25 | description: "This is the first post", 26 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 27 | date: new Date("2022-01-01"), 28 | image: "https://example.com/images/post1.jpg", 29 | }, 30 | { 31 | title: "Post 2", 32 | url: "https://example.com/post-2", 33 | description: "This is the second post", 34 | content: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 35 | date: new Date("2022-01-05"), 36 | image: "https://example.com/images/post2.jpg", 37 | }, 38 | { 39 | title: "Post 3", 40 | url: "https://example.com/post-3", 41 | description: "This is the third post", 42 | content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 43 | date: new Date("2022-01-10"), 44 | image: "https://example.com/images/post3.jpg", 45 | }, 46 | { 47 | title: "Post 4", 48 | url: "https://example.com/post-4", 49 | description: "This is the fourth post", 50 | content: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 51 | date: new Date("2022-01-15"), 52 | image: "https://example.com/images/post4.jpg", 53 | }, 54 | { 55 | title: "Post 5", 56 | url: "https://example.com/post-5", 57 | description: "This is the fifth post", 58 | content: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 59 | date: new Date("2022-01-20"), 60 | image: "https://example.com/images/post5.jpg", 61 | }, 62 | ]; 63 | 64 | posts.forEach(post => { 65 | feed.addItem({ 66 | title: post.title, 67 | id: post.url, 68 | link: post.url, 69 | description: post.description, 70 | content: post.content, 71 | date: post.date 72 | }) 73 | }) 74 | 75 | feed.addCategory('Nuxt.js') 76 | 77 | feed.addContributor({ 78 | name: 'Miha Sedej', 79 | email: 'sedej.miha@gmail.com', 80 | link: 'https://tresko.dev/' 81 | }) 82 | }) 83 | }) -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------