├── .gitattributes ├── .editorconfig ├── .travis.yml ├── lib ├── index.js ├── LOG.js ├── UTIL.js ├── Head.js ├── Generator.js └── Page.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 2 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [{*.json,.travis.yml}] 18 | indent_style = space 19 | indent_size = 2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | 4 | language: node_js 5 | 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | 12 | node_js: 13 | - '8' 14 | - '10' 15 | - 'stable' 16 | 17 | 18 | install: 19 | - npm install 20 | 21 | 22 | script: 23 | - echo "skipping tests" 24 | 25 | 26 | deploy: 27 | provider: npm 28 | email: $NPM_EMAIL 29 | api_key: $NPM_TOKEN 30 | skip_cleanup: true 31 | on: 32 | tags: true 33 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FS = require('fs'); 4 | const PATH = require('path'); 5 | 6 | // ----------------------------------------------------------------------------- 7 | 8 | FS.readdirSync( __dirname ) 9 | .filter( e => e.match(/.*\.js/gi) ) 10 | .filter( e => ! [ PATH.basename( __filename ) ].includes( e ) ) 11 | .filter( e => ! e.match(/^__/gi) ) 12 | .forEach( file => exports[ PATH.parse( file ).name ] = require(`./${file}`) ); 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Description 5 | 6 | 7 | 8 | 9 | 10 | ## Checklist 11 | 12 | 13 | 14 | - [ ] All tests are passing 15 | - [ ] My code follows the code style and structure of this project 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/LOG.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | chalk : CHALK, 5 | logger: LOGGER 6 | } = require('@vuepress/shared-utils'); 7 | 8 | // ----------------------------------------------------------------------------- 9 | 10 | const { name: PLUGIN_NAME } = require('../package.json'); 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | const LOG = { plugin_name: CHALK.magenta( PLUGIN_NAME ) }; 15 | 16 | ['wait', 'success', 'tip', 'warn', 'error'] 17 | .forEach( e => LOG[e] = ( ...args ) => LOGGER[e]( LOG.plugin_name, ...args ) ); 18 | 19 | // ----------------------------------------------------------------------------- 20 | 21 | module.exports = LOG; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Feature request 4 | 5 | about: Suggest an idea for this project 6 | 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## Feature request 18 | 19 | 20 | ### Is your feature request related to a problem? Please describe. 21 | 22 | 23 | 24 | 25 | ### Describe the solution you'd like 26 | 27 | 28 | 29 | 30 | ### Describe alternatives you've considered 31 | 32 | 33 | 34 | 35 | ### Are you willing to work on this yourself? 36 | 37 | 38 | 39 | 40 | ### Additional context 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for taking the time to contribute! :tada::+1: 4 | 5 | Please note that this project is released with a 6 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this 7 | project you agree to abide by its terms. 8 | 9 | This is a simple package, so I'll keep the guidelines simple and straightforward. 10 | 11 | 12 | ## Reporting Issues and Asking Questions 13 | 14 | - Search the [issue tracker][issue tracker url] to make sure your issue hasn’t already been reported. 15 | - Follow the instructions contained within the relevant issue template; 16 | [Bug Report template](ISSUE_TEMPLATE/bug_report.md) | [Feature Request template](ISSUE_TEMPLATE/feature_request.md). 17 | 18 | 19 | ## Submitting a Pull Request 20 | 21 | - Open a new issue in the [issue tracker][issue tracker url] if needed. 22 | - Fork the repo. 23 | - Create a new branch based off the master branch. 24 | - Make sure all tests pass. 25 | - Submit a pull request, referencing any issues it addresses. 26 | 27 | [issue tracker url]: https://github.com/webmasterish/vuepress-plugin-feed/issues 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - present webmasterish 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. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Bug report 4 | 5 | about: Create a report to help us improve 6 | 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## Description 18 | 19 | 20 | 21 | 22 | ## Expected Behavior 23 | 24 | 25 | 26 | 27 | ## Actual Behavior 28 | 29 | 30 | 31 | 32 | ## Steps to Reproduce 33 | 34 | 35 | 40 | 41 | 42 | ## Your Environment 43 | 44 | | Description | Value | 45 | |-----------------------|-------------------------------------------------| 46 | | vuepress-plugin-feed version | | 47 | | node version | | 48 | | npm version | | 49 | | browser | | 50 | | OS | | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuepress-plugin-feed", 3 | "version": "0.1.9", 4 | "description": "RSS, Atom, and JSON feeds generator plugin for VuePress 1.x", 5 | "main": "index.js", 6 | "scripts": { 7 | "version": "git add --all", 8 | "postversion": "git push && git push --tags" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/webmasterish/vuepress-plugin-feed.git" 13 | }, 14 | "keywords": [ 15 | "vue", 16 | "vuepress", 17 | "plugin", 18 | "vuepress-plugin", 19 | "blog", 20 | "feed", 21 | "xml", 22 | "rss", 23 | "atom", 24 | "json", 25 | "json-feed", 26 | "syndication" 27 | ], 28 | "author": { 29 | "name": "webmasterish", 30 | "email": "webmasterish@gmail.com", 31 | "url": "https://webmasterish.com" 32 | }, 33 | "license": "MIT", 34 | "homepage": "https://github.com/webmasterish/vuepress-plugin-feed", 35 | "bugs": { 36 | "url": "https://github.com/webmasterish/vuepress-plugin-feed/issues" 37 | }, 38 | "engines": { 39 | "node": ">=8" 40 | }, 41 | "dependencies": { 42 | "feed": "2.0.4", 43 | "lodash.defaultsdeep": "4.6.1", 44 | "lodash.isempty": "4.4.0", 45 | "lodash.trimend": "^4.5.1", 46 | "lodash.trimstart": "^4.5.1", 47 | "remove-markdown": "0.3.0", 48 | "striptags": "3.1.1" 49 | }, 50 | "peerDependencies": { 51 | "vuepress": "1.x" 52 | }, 53 | "files": [ 54 | "lib", 55 | "index.js", 56 | "README.md", 57 | "LICENSE" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /lib/UTIL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ----------------------------------------------------------------------------- 4 | 5 | const REMOVE_MARKDOWN = require('remove-markdown'); 6 | const STRIPTAGS = require('striptags'); 7 | const _ = { 8 | trimEnd : require('lodash.trimend'), 9 | trimStart : require('lodash.trimstart'), 10 | }; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | const UTIL = {}; 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | 19 | /** 20 | * @return {string} 21 | */ 22 | UTIL.resolve_url = ( base, path ) => `${_.trimEnd( base, '/' )}/${_.trimStart( path, '/' )}`; 23 | 24 | 25 | /** 26 | * @return {string} 27 | */ 28 | UTIL.strip_markup = str => STRIPTAGS( REMOVE_MARKDOWN( str, { useImgAltText: false } ) ); 29 | 30 | 31 | 32 | /** 33 | * @return {RegExp} 34 | */ 35 | UTIL.get_regex = re => ( Array.isArray( re ) ) ? new RegExp( ...re ) : re; 36 | 37 | 38 | 39 | /** 40 | * check if string is a valid url 41 | * 42 | * @param {string} maybe_url 43 | * @return {bool} 44 | */ 45 | UTIL.is_url = ( maybe_url ) => 46 | { 47 | 48 | if ( ! maybe_url || typeof maybe_url !== 'string' ) 49 | { 50 | return false; 51 | } 52 | 53 | // --------------------------------------------------------------------------- 54 | 55 | const re_protocol_and_domain = /^(?:\w+:)?\/\/(\S+)$/; 56 | 57 | const match = maybe_url.match( re_protocol_and_domain ); 58 | 59 | if ( ! match ) 60 | { 61 | return false; 62 | } 63 | 64 | // --------------------------------------------------------------------------- 65 | 66 | const all_after_protocol = match[1]; 67 | 68 | if ( ! all_after_protocol ) 69 | { 70 | return false; 71 | } 72 | 73 | // --------------------------------------------------------------------------- 74 | 75 | const re_domain_localhost = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/ 76 | const re_domain_non_localhost = /^[^\s\.]+\.\S{2,}$/; 77 | 78 | return ( re_domain_localhost.test( all_after_protocol ) 79 | || re_domain_non_localhost.test( all_after_protocol ) ); 80 | 81 | } 82 | // UTIL.is_url() 83 | 84 | // ----------------------------------------------------------------------------- 85 | 86 | module.exports = UTIL; 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | # GENERIC FILES TO IGNORE 3 | # ############################################################################## 4 | 5 | # ------------------------------------------------------------------------------ 6 | # Linux 7 | # ------------------------------------------------------------------------------ 8 | *~ 9 | .Trash-* 10 | 11 | # ------------------------------------------------------------------------------ 12 | # OSX 13 | # ------------------------------------------------------------------------------ 14 | *.DS_Store 15 | ._* 16 | 17 | # ------------------------------------------------------------------------------ 18 | # Windows 19 | # ------------------------------------------------------------------------------ 20 | Desktop.ini 21 | Thumbs.db 22 | 23 | # ------------------------------------------------------------------------------ 24 | # SVN 25 | # ------------------------------------------------------------------------------ 26 | .svn/ 27 | 28 | # ------------------------------------------------------------------------------ 29 | # Log files and databases 30 | # ------------------------------------------------------------------------------ 31 | *.log 32 | *.sql 33 | *.sqlite 34 | 35 | # ------------------------------------------------------------------------------ 36 | # CSS pre-processors 37 | # ------------------------------------------------------------------------------ 38 | *.sass-cache* 39 | 40 | # ------------------------------------------------------------------------------ 41 | # Packages such as Zip files 42 | # ------------------------------------------------------------------------------ 43 | *.7z 44 | *.dmg 45 | *.gz 46 | *.iso 47 | *.rar 48 | *.tar 49 | *.zip 50 | 51 | # ------------------------------------------------------------------------------ 52 | # Node 53 | # ------------------------------------------------------------------------------ 54 | 55 | # generic node ignores 56 | node_modules 57 | /dist 58 | /build 59 | 60 | # testing and coverage related 61 | coverage 62 | .nyc_output 63 | 64 | # locks 65 | package-lock.json 66 | yarn.lock 67 | 68 | # file containing environment variables such as secret api keys 69 | # used in conjunction with dotenv package 70 | *.env 71 | 72 | # ------------------------------------------------------------------------------ 73 | # Misc 74 | # ------------------------------------------------------------------------------ 75 | 76 | # ignore any file or folder that starts with double underscore 77 | __* 78 | 79 | # ide 80 | .idea 81 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [webmasterish@gmail.com](mailto:webmasterish@gmail.com). 59 | All complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /lib/Head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = { isEmpty: require('lodash.isempty') }; 4 | 5 | // ----------------------------------------------------------------------------- 6 | 7 | const { chalk: CHALK } = require('@vuepress/shared-utils'); 8 | 9 | // ----------------------------------------------------------------------------- 10 | 11 | const LIB = { 12 | LOG : require('./LOG'), 13 | UTIL: require('./UTIL'), 14 | }; 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | /** 19 | * Class responsible for adding links to head 20 | */ 21 | class Head 22 | { 23 | 24 | /** 25 | * constructor 26 | * 27 | * @param {object} options 28 | * @param {object} context 29 | */ 30 | constructor( options = {}, context ) 31 | { 32 | 33 | if ( ! options.canonical_base ) 34 | { 35 | throw new Error('canonical_base required'); 36 | } 37 | 38 | // ------------------------------------------------------------------------- 39 | 40 | this.options = options; 41 | this.canonical_base = this.options.canonical_base; 42 | this.feeds = this.options.feeds || {}; 43 | this._internal = this.options._internal || {}; 44 | 45 | // ------------------------------------------------------------------------- 46 | 47 | this.context = context || {}; 48 | 49 | } 50 | // constructor() 51 | 52 | 53 | 54 | /** 55 | * @return {string} 56 | */ 57 | get_feed_url( feed ) 58 | { 59 | 60 | if ( feed.head_link.enable && feed.enable && feed.file_name ) 61 | { 62 | return LIB.UTIL.resolve_url( this.canonical_base, feed.file_name ); 63 | } 64 | 65 | } 66 | // get_feed_url() 67 | 68 | 69 | 70 | /** 71 | * @return {array} 72 | */ 73 | get_link_item( feed, site_title = '' ) 74 | { 75 | 76 | try { 77 | 78 | const href = this.get_feed_url( feed ); 79 | 80 | if ( ! href ) 81 | { 82 | return; 83 | } 84 | 85 | // ----------------------------------------------------------------------- 86 | 87 | const { type, title } = feed.head_link; 88 | 89 | return [ 90 | 'link', 91 | { 92 | rel : 'alternate', 93 | type, 94 | href, 95 | title : title.replace( '%%site_title%%', site_title ), 96 | } 97 | ]; 98 | 99 | } catch ( err ) { 100 | 101 | LIB.LOG.error( err.message ); 102 | 103 | } 104 | 105 | } 106 | // get_link_item() 107 | 108 | 109 | 110 | /** 111 | * @return {array|undefined} 112 | */ 113 | async add_links() 114 | { 115 | 116 | try { 117 | 118 | if ( _.isEmpty( this.feeds ) ) 119 | { 120 | return; 121 | } 122 | 123 | // ----------------------------------------------------------------------- 124 | 125 | const { siteConfig = {} } = this.context; 126 | 127 | siteConfig.head = siteConfig.head || []; 128 | const site_title = siteConfig.title || ''; 129 | 130 | // ----------------------------------------------------------------------- 131 | 132 | const out = []; 133 | 134 | for ( const key of Object.keys( this.feeds ) ) 135 | { 136 | if ( ! this._internal.allowed_feed_types.includes( key ) ) 137 | { 138 | continue; 139 | } 140 | 141 | // --------------------------------------------------------------------- 142 | 143 | const item = this.get_link_item( this.feeds[ key ], site_title ); 144 | 145 | if ( _.isEmpty( item ) ) 146 | { 147 | continue; 148 | } 149 | 150 | siteConfig.head.push( item ); 151 | 152 | LIB.LOG.success(`${key} link added to ${CHALK.cyan('siteConfig.head')}`); 153 | 154 | // --------------------------------------------------------------------- 155 | 156 | out.push( item ); 157 | } 158 | 159 | // ----------------------------------------------------------------------- 160 | 161 | return out; 162 | 163 | } catch ( err ) { 164 | 165 | LIB.LOG.error( err.message ); 166 | 167 | } 168 | 169 | } 170 | // add_links() 171 | 172 | } 173 | // class Head 174 | 175 | // ----------------------------------------------------------------------------- 176 | 177 | module.exports = Head; 178 | -------------------------------------------------------------------------------- /lib/Generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = { 4 | isEmpty: require('lodash.isempty'), 5 | }; 6 | 7 | // ----------------------------------------------------------------------------- 8 | 9 | const { 10 | path : PATH, 11 | fs : FSE, 12 | chalk : CHALK 13 | } = require('@vuepress/shared-utils'); 14 | 15 | // ----------------------------------------------------------------------------- 16 | 17 | const LIB = { 18 | UTIL: require('./UTIL'), 19 | LOG : require('./LOG'), 20 | Page: require('./Page'), 21 | }; 22 | 23 | // ----------------------------------------------------------------------------- 24 | 25 | const FEED = require('feed').Feed; 26 | 27 | // ----------------------------------------------------------------------------- 28 | 29 | /** 30 | * Class responsible for generating the feed xml/json files 31 | */ 32 | class Generator 33 | { 34 | 35 | /** 36 | * constructor 37 | * 38 | * @param {array} pages 39 | * @param {object} options 40 | * @param {object} context 41 | */ 42 | constructor( pages, options = {}, context ) 43 | { 44 | 45 | if ( _.isEmpty( pages ) ) 46 | { 47 | throw new Error('pages required'); 48 | } 49 | 50 | if ( ! options.canonical_base ) 51 | { 52 | throw new Error('canonical_base required'); 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | 57 | this.pages = pages; 58 | 59 | // ------------------------------------------------------------------------- 60 | 61 | this.options = options; 62 | this.canonical_base = this.options.canonical_base; 63 | this.feed_options = this.options.feed_options || {}; 64 | this.feeds = this.options.feeds || {}; 65 | this._internal = this.options._internal || {}; 66 | 67 | // ------------------------------------------------------------------------- 68 | 69 | this.context = context || {}; 70 | 71 | // ------------------------------------------------------------------------- 72 | 73 | this.feed_generator = new FEED( this.feed_options ); 74 | 75 | } 76 | // constructor() 77 | 78 | 79 | 80 | /** 81 | * @return null 82 | */ 83 | async add_items() 84 | { 85 | 86 | try { 87 | 88 | const pages = this.options.sort(this.pages).slice( 0, this.options.count ); 89 | 90 | LIB.LOG.wait('Adding pages/posts as feed items...'); 91 | 92 | const out = []; 93 | 94 | for ( const page of pages ) 95 | { 96 | const item = await new LIB.Page( page, this.options, this.context ).get_feed_item(); 97 | 98 | if ( ! _.isEmpty( item ) ) 99 | { 100 | this.feed_generator.addItem( item ); 101 | 102 | out.push( item ); 103 | } 104 | } 105 | 106 | // ----------------------------------------------------------------------- 107 | 108 | if ( ! _.isEmpty( out ) ) 109 | { 110 | LIB.LOG.success(`added ${CHALK.cyan( out.length + ' page(s)' )} as feed item(s)`); 111 | } 112 | 113 | // ----------------------------------------------------------------------- 114 | 115 | return out; 116 | 117 | } catch ( err ) { 118 | 119 | LIB.LOG.error( err.message ); 120 | 121 | } 122 | 123 | } 124 | // add_items() 125 | 126 | 127 | 128 | /** 129 | * @return null 130 | */ 131 | add_categories() 132 | { 133 | 134 | try { 135 | 136 | const { category } = this.options; 137 | 138 | if ( category ) 139 | { 140 | const categories = Array.isArray( category ) ? category : [ category ]; 141 | 142 | categories.map( e => this.feed_generator.addCategory( e ) ); 143 | } 144 | 145 | } catch ( err ) { 146 | 147 | LIB.LOG.error( err.message ); 148 | 149 | } 150 | 151 | } 152 | // add_categories() 153 | 154 | 155 | 156 | /** 157 | * @return null 158 | */ 159 | add_contributors() 160 | { 161 | 162 | try { 163 | 164 | const { contributor } = this.options; 165 | 166 | if ( contributor ) 167 | { 168 | const contributors = Array.isArray( contributor ) ? contributor : [ contributor ]; 169 | 170 | contributors.map( e => this.feed_generator.addContributor( e ) ); 171 | } 172 | 173 | } catch ( err ) { 174 | 175 | LIB.LOG.error( err.message ); 176 | 177 | } 178 | 179 | } 180 | // add_contributors() 181 | 182 | 183 | 184 | /** 185 | * @return {array} 186 | */ 187 | async generate_files() 188 | { 189 | 190 | try { 191 | 192 | LIB.LOG.wait('Checking feeds that need to be generated...'); 193 | 194 | if ( _.isEmpty( this.feeds ) ) 195 | { 196 | LIB.LOG.warn('no feeds set - aborting'); 197 | 198 | return; 199 | } 200 | 201 | // ----------------------------------------------------------------------- 202 | 203 | const { outDir, cwd } = this.context; 204 | 205 | const feeds = this.feeds; 206 | const out = []; 207 | 208 | for ( const key of Object.keys( feeds ) ) 209 | { 210 | if ( ! this._internal.allowed_feed_types.includes( key ) ) 211 | { 212 | continue; 213 | } 214 | 215 | // --------------------------------------------------------------------- 216 | 217 | const feed = feeds[ key ]; 218 | 219 | if ( ! feed.enable || ! feed.file_name ) 220 | { 221 | continue; 222 | } 223 | 224 | // --------------------------------------------------------------------- 225 | 226 | const content = this.feed_generator[ key ](); 227 | const file = PATH.resolve( outDir, feed.file_name ); 228 | const relative = PATH.relative( cwd, file ); 229 | 230 | await FSE.outputFile( file, content ); 231 | 232 | LIB.LOG.success(`${key} feed file generated and saved to ${CHALK.cyan( relative )}`); 233 | 234 | // --------------------------------------------------------------------- 235 | 236 | out.push( file ); 237 | } 238 | 239 | // ----------------------------------------------------------------------- 240 | 241 | return out; 242 | 243 | } catch ( err ) { 244 | 245 | LIB.LOG.error( err.message ); 246 | 247 | } 248 | 249 | } 250 | // generate_files() 251 | 252 | 253 | 254 | /** 255 | * @return {array} 256 | */ 257 | async generate() 258 | { 259 | 260 | try { 261 | 262 | await this.add_items(); 263 | 264 | this.add_categories(); 265 | 266 | this.add_contributors(); 267 | 268 | const files = await this.generate_files(); 269 | 270 | return files; 271 | 272 | } catch ( err ) { 273 | 274 | LIB.LOG.error( err.message ); 275 | 276 | } 277 | 278 | } 279 | // generate() 280 | 281 | } 282 | // class Generator 283 | 284 | // ----------------------------------------------------------------------------- 285 | 286 | module.exports = Generator; 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VuePress Plugin Feed 2 | 3 | > RSS, Atom, and JSON feeds generator plugin for VuePress 1.x 4 | 5 | [](https://travis-ci.org/webmasterish/vuepress-plugin-feed) 6 | [](http://npm.im/vuepress-plugin-feed) 7 | [](https://greenkeeper.io/) 8 | [](http://opensource.org/licenses/MIT) 9 | 10 | 11 | ## Install 12 | 13 | 14 | ```sh 15 | $ npm install -D vuepress-plugin-feed 16 | 17 | # or 18 | 19 | $ yarn add -D vuepress-plugin-feed 20 | ``` 21 | 22 | 23 | ## Usage 24 | 25 | Add `vuepress-plugin-feed` in your site or theme config file. 26 | 27 | > See [official docs on using a plugin](https://vuepress.vuejs.org/plugin/using-a-plugin.html) 28 | 29 | 30 | ```js 31 | // .vuepress/config.js 32 | // or 33 | // .vuepress/theme/index.js 34 | 35 | // set your global feed options - override in page frontmatter `feed` 36 | const feed_options = { 37 | canonical_base: 'https://webmasterish.com', 38 | }; 39 | 40 | module.exports = { 41 | plugins: [ 42 | [ 'feed', feed_options ] 43 | ] 44 | } 45 | ``` 46 | 47 | 48 | ### Page `frontmatter` 49 | 50 | Page `frontmatter.feed` is optional. It can be used to override the defaults. 51 | 52 | Check the [`Page class`](lib/Page.js) for more details. 53 | 54 | 55 | ```md 56 | --- 57 | 58 | title: Page Title 59 | 60 | feed: 61 | enable: true 62 | title: Title used in feed 63 | description: Description used in feed 64 | image: /public/image.png 65 | author: 66 | - 67 | name: Author 68 | email: author@doamin.tld 69 | link: http://doamin.tld 70 | contributor: 71 | - 72 | name: Contributor 73 | email: contributor@doamin.tld 74 | link: http://doamin.tld 75 | 76 | --- 77 | 78 | ``` 79 | 80 | 81 | ## How pages are added as feed items 82 | 83 | A page is auto added as a feed item if one the following conditions is met: 84 | 85 | - `frontmatter.feed.enable === true` 86 | - `frontmatter.type === 'post'` 87 | - it resides in whatever the `posts_directories` are set to (the defaults are `blog` and `_posts`) 88 | 89 | if you need to exclude a particular page that meets one of the conditions above, 90 | you can use `frontmatter.feed.enable === false`. 91 | 92 | Details on how pages are filtered can be found in [`PLUGIN.is_feed_page()`](index.js). 93 | 94 | The `PLUGIN.is_feed_page()` function is the default way of filtering the pages, 95 | you can override it using `is_feed_page` option (see [Options section](#options) below). 96 | 97 | 98 | ## Options 99 | 100 | > See Plugin Option API [official docs](https://vuepress.vuejs.org/plugin/option-api.html) 101 | 102 | 103 | ### Default options 104 | 105 | You can override default options in 2 ways: 106 | 107 | 1. Global plugin options set in `.vuepress/config.js` or `.vuepress/theme/index.js` 108 | as described in [Usage](#usage) 109 | 2. Individual page/post `frontmatter` as shown in [Page `frontmatter`](#page-frontmatter) 110 | 111 | 112 | ```js 113 | const { 114 | title, 115 | description 116 | } = context.getSiteData ? context.getSiteData() : context; 117 | 118 | // ----------------------------------------------------------------------------- 119 | 120 | // Feed class options 121 | // @see: https://github.com/jpmonette/feed#example 122 | 123 | const feed_options = { 124 | 125 | title, 126 | description, 127 | generator: PLUGIN.homepage, 128 | 129 | // --------------------------------------------------------------------------- 130 | 131 | // the following are auto populated in PLUGIN.get_options() 132 | // if they are not set as options 133 | /* 134 | id, 135 | link, 136 | feedLinks, 137 | */ 138 | 139 | // --------------------------------------------------------------------------- 140 | 141 | // ref: 142 | /* 143 | title: "Feed Title", 144 | description: "This is my personal feed!", 145 | id: "http://example.com/", 146 | link: "http://example.com/", 147 | image: "http://example.com/image.png", 148 | favicon: "http://example.com/favicon.ico", 149 | copyright: "All rights reserved 2013, John Doe", 150 | updated: new Date(2013, 6, 14), // optional, default = today 151 | generator: "awesome", // optional, default = 'Feed for Node.js' 152 | feedLinks: { 153 | json: "https://example.com/json", 154 | atom: "https://example.com/atom" 155 | }, 156 | author: { 157 | name: "John Doe", 158 | email: "johndoe@example.com", 159 | link: "https://example.com/johndoe" 160 | } 161 | */ 162 | 163 | }; 164 | 165 | // ----------------------------------------------------------------------------- 166 | 167 | const default_options = { 168 | 169 | // required; it can also be used as enable/disable 170 | 171 | canonical_base: '', 172 | 173 | // --------------------------------------------------------------------------- 174 | 175 | // Feed class options - @see: https://github.com/jpmonette/feed#example 176 | // optional - auto-populated based on context.getSiteData() 177 | 178 | feed_options, 179 | 180 | // --------------------------------------------------------------------------- 181 | 182 | // @notes: 183 | // property name is also the name of the jpmonette/feed package function 184 | 185 | feeds: { 186 | 187 | rss2: { 188 | enable : true, 189 | file_name : 'rss.xml', 190 | head_link : { 191 | enable: true, 192 | type : 'application/rss+xml', 193 | title : '%%site_title%% RSS Feed', 194 | } 195 | }, 196 | 197 | // ------------------------------------------------------------------------- 198 | 199 | atom1: { 200 | enable : true, 201 | file_name : 'feed.atom', 202 | head_link : { 203 | enable: true, 204 | type : 'application/atom+xml', 205 | title : '%%site_title%% Atom Feed', 206 | } 207 | }, 208 | 209 | // ------------------------------------------------------------------------- 210 | 211 | json1: { 212 | enable : true, 213 | file_name : 'feed.json', 214 | head_link : { 215 | enable: true, 216 | type : 'application/json', 217 | title : '%%site_title%% JSON Feed', 218 | } 219 | }, 220 | 221 | }, 222 | 223 | // --------------------------------------------------------------------------- 224 | 225 | // page/post description sources 226 | 227 | // order of what gets the highest priority: 228 | // 229 | // 1. frontmatter 230 | // 2. page excerpt 231 | // 3. content markdown paragraph 232 | // 4. content regular html
233 | 234 | description_sources: [ 235 | 236 | 'frontmatter', 237 | 'excerpt', 238 | 239 | // markdown paragraph regex 240 | // @todo: needs work 241 | // 242 | /^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/gim, 243 | // 244 | // this excludes blockquotes using `(?!^>)` 245 | ///^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^>)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/gim, 246 | 247 | // html paragraph regex 248 | /
(.*?)<\/p>/i,
249 |
250 | ],
251 |
252 | // ---------------------------------------------------------------------------
253 |
254 | // page/post image sources
255 |
256 | // order of what gets the highest priority:
257 | //
258 | // 1. frontmatter
259 | // 2. content markdown image such as ``
260 | // 3. content regular html img
261 |
262 | image_sources: [
263 |
264 | 'frontmatter',
265 |
266 | /!\[.*?\]\((.*?)\)/i, // markdown image regex
267 | /
157 |
158 | description_sources: [
159 |
160 | 'frontmatter',
161 | 'excerpt',
162 |
163 | // markdown paragraph regex
164 | // @todo: needs work
165 | //
166 | /^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/gim,
167 | //
168 | // this excludes blockquotes using `(?!^>)`
169 | ///^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^>)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/gim,
170 |
171 | // html paragraph regex
172 | / (.*?)<\/p>/i,
173 |
174 | // -----------------------------------------------------------------------
175 |
176 | // @notes: setting as array require escaping `\`
177 |
178 | //['^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n', 'gim'],
179 | //[' (.*?)<\/p>', 'i'],
180 |
181 | ],
182 |
183 | // -------------------------------------------------------------------------
184 |
185 | // @consider description max words/char
186 |
187 | // -------------------------------------------------------------------------
188 |
189 | // order of what gets the highest priority:
190 | //
191 | // 1. frontmatter
192 | // 2. content markdown image such as ``
193 | // 3. content regular html img
194 |
195 | image_sources: [
196 |
197 | 'frontmatter',
198 |
199 | /!\[.*?\]\((.*?)\)/i, // markdown image regex
200 | /