├── .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 | [![Build Status](https://img.shields.io/travis/webmasterish/vuepress-plugin-feed/master.svg?style=flat-square)](https://travis-ci.org/webmasterish/vuepress-plugin-feed) 6 | [![npm version](https://img.shields.io/npm/v/vuepress-plugin-feed.svg?style=flat-square)](http://npm.im/vuepress-plugin-feed) 7 | [![Greenkeeper badge](https://badges.greenkeeper.io/webmasterish/vuepress-plugin-feed.svg?style=flat-square)](https://greenkeeper.io/) 8 | [![MIT License](https://img.shields.io/npm/l/express.svg?style=flat-square)](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 `![alt text](http://url)` 260 | // 3. content regular html img 261 | 262 | image_sources: [ 263 | 264 | 'frontmatter', 265 | 266 | /!\[.*?\]\((.*?)\)/i, // markdown image regex 267 | / _.reverse( _.sortBy( entries, 'date' ) ), 293 | // Don't forget to do a `const _ = require('lodash');` to be able to use `_`! 294 | 295 | sort: entries => entries, 296 | 297 | // --------------------------------------------------------------------------- 298 | 299 | // supported - use in config as needed 300 | 301 | // category 302 | // contributor 303 | 304 | }; 305 | ``` 306 | 307 | 308 | ## Reference 309 | 310 | - VuePress official [plugin docs](https://vuepress.vuejs.org/plugin/) 311 | - VuePress official [Front Matter](https://vuepress.vuejs.org/guide/frontmatter.html) 312 | - [jpmonette/feed](https://github.com/jpmonette/feed) 313 | - [RSS 2.0 specificatiion](https://validator.w3.org/feed/docs/rss2.html) 314 | - [Atom feed](https://validator.w3.org/feed/docs/atom.html) 315 | - [JSON feed](https://jsonfeed.org/) 316 | 317 | 318 | ## Related Plugins 319 | 320 | - [VuePress Plugin Auto Meta](https://github.com/webmasterish/vuepress-plugin-autometa) 321 | - [VuePress Plugin Auto Nav](https://github.com/webmasterish/vuepress-plugin-autonav) 322 | - [VuePress Plugin Minimal Google Analytics](https://github.com/webmasterish/vuepress-plugin-minimal-analytics) 323 | 324 | ## License 325 | 326 | MIT © [webmasterish](https://webmasterish.com) 327 | -------------------------------------------------------------------------------- /lib/Page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = { isEmpty: require('lodash.isempty') }; 4 | 5 | // ----------------------------------------------------------------------------- 6 | 7 | const LIB = { 8 | UTIL: require('./UTIL'), 9 | LOG : require('./LOG'), 10 | }; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | /** 15 | * Class responsible for preparing a page data to be used as feed item 16 | */ 17 | class Page 18 | { 19 | 20 | /** 21 | * constructor 22 | * 23 | * @param {object} $page 24 | * @param {object} options 25 | * @param {object} context 26 | */ 27 | constructor( $page, options = {}, context ) 28 | { 29 | 30 | if ( ! options.canonical_base ) 31 | { 32 | throw new Error('canonical_base required'); 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | 37 | const { path, regularPath } = $page; 38 | 39 | this.path = path || regularPath; 40 | 41 | if ( ! this.path ) 42 | { 43 | throw new Error('path required'); 44 | } 45 | 46 | // ------------------------------------------------------------------------- 47 | 48 | const { 49 | 50 | key, // page's unique hash key 51 | frontmatter, // page's frontmatter object 52 | //regularPath, // current page's default link (follow the file hierarchy) 53 | //path, // current page's real link (use regularPath when permalink does not exist) 54 | 55 | //title, 56 | //date, 57 | excerpt, 58 | 59 | //_filePath, // file's absolute path 60 | //_computed, // access the client global computed mixins at build time, e.g _computed.$localePath. 61 | //_content, // file's raw content string 62 | _strippedContent, // file's content string without frontmatter 63 | //_meta, 64 | 65 | } = $page; 66 | 67 | 68 | this.$page = $page; 69 | this.key = key; 70 | this.frontmatter = frontmatter || {}; 71 | this.feed_settings = this.frontmatter.feed || {}; 72 | this.excerpt = excerpt; 73 | this._strippedContent = _strippedContent; 74 | 75 | // ------------------------------------------------------------------------- 76 | 77 | this.options = options; 78 | this.canonical_base = this.options.canonical_base; 79 | this.feed_options = this.options.feed_options || {}; 80 | 81 | // ------------------------------------------------------------------------- 82 | 83 | this.context = context || {}; 84 | 85 | } 86 | // constructor() 87 | 88 | 89 | 90 | /** 91 | * @return {string} 92 | */ 93 | url_resolve( path ) 94 | { 95 | 96 | if ( this.canonical_base && path ) 97 | { 98 | return LIB.UTIL.resolve_url( this.canonical_base, path ); 99 | } 100 | 101 | } 102 | // url_resolve() 103 | 104 | 105 | 106 | /** 107 | * @return {string} 108 | */ 109 | extract_from_stripped_content( re ) 110 | { 111 | 112 | if ( ! this._strippedContent ) 113 | { 114 | return; 115 | } 116 | 117 | // ------------------------------------------------------------------------- 118 | 119 | const regex = LIB.UTIL.get_regex( re ); 120 | 121 | if ( ! ( regex instanceof RegExp ) ) 122 | { 123 | return; 124 | } 125 | 126 | let match; 127 | 128 | if ( ( match = regex.exec( this._strippedContent ) ) !== null ) 129 | { 130 | if ( match[1] ) 131 | { 132 | return match[1]; 133 | } 134 | } 135 | 136 | } 137 | // extract_from_stripped_content() 138 | 139 | 140 | 141 | /** 142 | * @return {string} 143 | */ 144 | get_feed_setting( key, fallback = true ) 145 | { 146 | 147 | try { 148 | 149 | if ( this.feed_settings.hasOwnProperty( key ) ) 150 | { 151 | return this.feed_settings[ key ]; 152 | } 153 | 154 | // ----------------------------------------------------------------------- 155 | 156 | if ( fallback && ! _.isEmpty( this.feed_options[ key ] ) ) 157 | { 158 | return this.feed_options[ key ]; 159 | } 160 | 161 | } catch ( err ) { 162 | 163 | LIB.LOG.error( err.message ); 164 | 165 | } 166 | 167 | } 168 | // get_feed_setting() 169 | 170 | 171 | 172 | /** 173 | * @return {string} 174 | */ 175 | get title() 176 | { 177 | 178 | const { title } = this.$page; 179 | 180 | return this.feed_settings.title || title; 181 | 182 | } 183 | // get title() 184 | 185 | 186 | 187 | /** 188 | * @return {Date} 189 | */ 190 | get date() 191 | { 192 | 193 | const { date } = this.$page; 194 | 195 | return ( date ) ? new Date( date ) : new Date(); 196 | 197 | } 198 | // get date() 199 | 200 | 201 | 202 | /** 203 | * @return {string} 204 | */ 205 | get url() 206 | { 207 | 208 | return this.url_resolve( this.path ); 209 | 210 | } 211 | // get url() 212 | 213 | 214 | 215 | /** 216 | * @return {string} 217 | */ 218 | get description() 219 | { 220 | 221 | try { 222 | 223 | if ( this.feed_settings.hasOwnProperty('description') ) 224 | { 225 | return this.feed_settings.description; 226 | } 227 | 228 | // ----------------------------------------------------------------------- 229 | 230 | if ( _.isEmpty( this.options.description_sources ) ) 231 | { 232 | return; 233 | } 234 | 235 | // ----------------------------------------------------------------------- 236 | 237 | let out = ''; 238 | 239 | for ( const source of this.options.description_sources ) 240 | { 241 | switch ( source ) 242 | { 243 | case 'frontmatter': 244 | 245 | out = this.frontmatter.description || ''; 246 | 247 | break; 248 | 249 | // ------------------------------------------------------------------- 250 | 251 | case 'excerpt': 252 | 253 | out = this.excerpt || ''; 254 | 255 | break; 256 | 257 | // ------------------------------------------------------------------- 258 | 259 | default: 260 | 261 | // content without frontmatter - used with regex 262 | 263 | out = this.extract_from_stripped_content( source ); 264 | 265 | break; 266 | } 267 | 268 | // --------------------------------------------------------------------- 269 | 270 | if ( ! out ) 271 | { 272 | continue; 273 | } 274 | 275 | // --------------------------------------------------------------------- 276 | 277 | out = LIB.UTIL.strip_markup( out.trim() ); 278 | 279 | // --------------------------------------------------------------------- 280 | 281 | if ( out ) 282 | { 283 | break; 284 | } 285 | } 286 | 287 | // ----------------------------------------------------------------------- 288 | 289 | return out; 290 | 291 | } catch ( err ) { 292 | 293 | LIB.LOG.error( err.message ); 294 | 295 | } 296 | 297 | } 298 | // get description() 299 | 300 | 301 | 302 | /** 303 | * @wip 304 | * @return {string} 305 | */ 306 | get content() 307 | { 308 | 309 | try { 310 | 311 | if ( this.feed_settings.hasOwnProperty('content') ) 312 | { 313 | return this.feed_settings.content; 314 | } 315 | 316 | // ----------------------------------------------------------------------- 317 | 318 | if ( _.isEmpty( this.context.markdown ) ) 319 | { 320 | return; 321 | } 322 | 323 | // @todo: should be generated html from markdown 324 | 325 | if ( this._strippedContent ) 326 | { 327 | const { html } = this.context.markdown.render( this._strippedContent ); 328 | 329 | // --------------------------------------------------------------------- 330 | 331 | if ( ! html ) 332 | { 333 | return; 334 | } 335 | 336 | // --------------------------------------------------------------------- 337 | 338 | /* 339 | // @todo: 340 | // render vue; {{ }}, vue components, etc... 341 | // convert relative urls to full urls 342 | */ 343 | 344 | // --------------------------------------------------------------------- 345 | 346 | return html; 347 | } 348 | 349 | // ----------------------------------------------------------------------- 350 | 351 | // @consider falling back to excerpt or description 352 | 353 | } catch ( err ) { 354 | 355 | LIB.LOG.error( err.message ); 356 | 357 | } 358 | 359 | } 360 | // get content() 361 | 362 | 363 | 364 | /** 365 | * @return {string} image url 366 | */ 367 | get image() 368 | { 369 | 370 | try { 371 | 372 | if ( this.feed_settings.hasOwnProperty('image') ) 373 | { 374 | return this.feed_settings.image; 375 | } 376 | 377 | // ----------------------------------------------------------------------- 378 | 379 | if ( _.isEmpty( this.options.image_sources ) ) 380 | { 381 | return; 382 | } 383 | 384 | // ----------------------------------------------------------------------- 385 | 386 | let out = ''; 387 | 388 | for ( const source of this.options.image_sources ) 389 | { 390 | switch ( source ) 391 | { 392 | case 'frontmatter': 393 | 394 | out = this.frontmatter.image || ''; 395 | 396 | break; 397 | 398 | // ------------------------------------------------------------------- 399 | 400 | default: 401 | 402 | // content without frontmatter - used with regex 403 | 404 | out = this.extract_from_stripped_content( source ); 405 | 406 | break; 407 | } 408 | 409 | // --------------------------------------------------------------------- 410 | 411 | if ( out ) 412 | { 413 | out = out.trim(); 414 | 415 | break; 416 | } 417 | } 418 | 419 | if ( ! out ) 420 | { 421 | return; 422 | } 423 | 424 | // ----------------------------------------------------------------------- 425 | 426 | // image url as relative path is supported 427 | 428 | return ( LIB.UTIL.is_url( out ) ) ? out : this.url_resolve( out ); 429 | 430 | } catch ( err ) { 431 | 432 | LIB.LOG.error( err.message ); 433 | 434 | } 435 | 436 | } 437 | // get image() 438 | 439 | 440 | 441 | /** 442 | * @return {object} 443 | */ 444 | get author() 445 | { 446 | 447 | return this.get_feed_setting('author'); 448 | 449 | } 450 | // get author() 451 | 452 | 453 | 454 | /** 455 | * @return {object} 456 | */ 457 | get contributor() 458 | { 459 | 460 | return this.get_feed_setting('contributor'); 461 | 462 | } 463 | // get contributor() 464 | 465 | 466 | 467 | /** 468 | * @return {object} 469 | */ 470 | async get_feed_item() 471 | { 472 | 473 | try { 474 | 475 | // we need at least title or description 476 | 477 | const title = this.title; 478 | const description = this.description; 479 | 480 | if ( ! title && ! description ) 481 | { 482 | return; 483 | } 484 | 485 | // ----------------------------------------------------------------------- 486 | 487 | const url = this.url; 488 | const out = { 489 | 490 | title, 491 | description, 492 | id : url, // @notes: i considered using key, but url is more relevant 493 | link : url, 494 | date : this.date, 495 | image : this.image, 496 | 497 | // --------------------------------------------------------------------- 498 | 499 | // @todo: 500 | // all content is included in item 501 | // still a wip; needs rendering all vue related syntax 502 | 503 | //content: this.content, 504 | 505 | // --------------------------------------------------------------------- 506 | 507 | // @notes: the following are handled below 508 | 509 | /* 510 | author : [], 511 | contributor : [], 512 | */ 513 | }; 514 | 515 | // ----------------------------------------------------------------------- 516 | 517 | const keys = ['author', 'contributor']; 518 | 519 | for ( const key of keys ) 520 | { 521 | const res = this[ key ]; 522 | 523 | if ( ! _.isEmpty( res ) ) 524 | { 525 | out[ key ] = Array.isArray( res ) ? res : [ res ]; 526 | } 527 | } 528 | 529 | // ----------------------------------------------------------------------- 530 | 531 | return out; 532 | 533 | } catch ( err ) { 534 | 535 | LIB.LOG.error( err.message ); 536 | 537 | } 538 | 539 | } 540 | // get_feed_item() 541 | 542 | } 543 | // class Page 544 | 545 | // ----------------------------------------------------------------------------- 546 | 547 | module.exports = Page; 548 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = { 4 | defaultsDeep: require('lodash.defaultsdeep'), 5 | isEmpty : require('lodash.isempty'), 6 | }; 7 | 8 | // ----------------------------------------------------------------------------- 9 | 10 | const LIB = require('./lib'); 11 | const UTIL = require('./lib/UTIL'); 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | const { 16 | name : PLUGIN_NAME, 17 | homepage: HOMEPAGE, 18 | } = require('./package.json'); 19 | 20 | /** 21 | * holds relevant functions and data 22 | */ 23 | const PLUGIN = { 24 | name : PLUGIN_NAME, 25 | homepage : HOMEPAGE, 26 | key : PLUGIN_NAME.replace('vuepress-plugin-', ''), // used in frontmatter 27 | allowed_feed_types: ['rss2', 'atom1', 'json1'], 28 | pages : [], 29 | options : {}, 30 | }; 31 | 32 | // ----------------------------------------------------------------------------- 33 | 34 | /** 35 | * @return {object} 36 | */ 37 | PLUGIN.get_options_defaults = ( context ) => 38 | { 39 | 40 | const { 41 | title, 42 | description 43 | } = context.getSiteData ? context.getSiteData() : context; 44 | 45 | // --------------------------------------------------------------------------- 46 | 47 | // Feed class options 48 | // @see: https://github.com/jpmonette/feed#example 49 | 50 | const feed_options = { 51 | 52 | title, 53 | description, 54 | generator: PLUGIN.homepage, 55 | 56 | // ------------------------------------------------------------------------- 57 | 58 | // the following are auto populated in PLUGIN.get_options() 59 | // if they are not set as options 60 | /* 61 | id, 62 | link, 63 | feedLinks, 64 | */ 65 | 66 | // ------------------------------------------------------------------------- 67 | 68 | // ref: 69 | /* 70 | title: "Feed Title", 71 | description: "This is my personal feed!", 72 | id: "http://example.com/", 73 | link: "http://example.com/", 74 | image: "http://example.com/image.png", 75 | favicon: "http://example.com/favicon.ico", 76 | copyright: "All rights reserved 2013, John Doe", 77 | updated: new Date(2013, 6, 14), // optional, default = today 78 | generator: "awesome", // optional, default = 'Feed for Node.js' 79 | feedLinks: { 80 | json: "https://example.com/json", 81 | atom: "https://example.com/atom" 82 | }, 83 | author: { 84 | name: "John Doe", 85 | email: "johndoe@example.com", 86 | link: "https://example.com/johndoe" 87 | } 88 | */ 89 | 90 | }; 91 | 92 | // --------------------------------------------------------------------------- 93 | 94 | const out = { 95 | 96 | // required; it can also be used as enable/disable 97 | 98 | canonical_base: '', 99 | 100 | // ------------------------------------------------------------------------- 101 | 102 | // Feed class options 103 | 104 | feed_options, 105 | 106 | // ------------------------------------------------------------------------- 107 | 108 | // @notes: 109 | // property name is also the name of the FEED package function 110 | 111 | feeds: { 112 | 113 | rss2: { 114 | enable : true, 115 | file_name : 'rss.xml', 116 | head_link : { 117 | enable: true, 118 | type : 'application/rss+xml', 119 | title : '%%site_title%% RSS Feed', 120 | } 121 | }, 122 | 123 | // ----------------------------------------------------------------------- 124 | 125 | atom1: { 126 | enable : true, 127 | file_name : 'feed.atom', 128 | head_link : { 129 | enable: true, 130 | type : 'application/atom+xml', 131 | title : '%%site_title%% Atom Feed', 132 | } 133 | }, 134 | 135 | // ----------------------------------------------------------------------- 136 | 137 | json1: { 138 | enable : true, 139 | file_name : 'feed.json', 140 | head_link : { 141 | enable: true, 142 | type : 'application/json', 143 | title : '%%site_title%% JSON Feed', 144 | } 145 | }, 146 | 147 | }, 148 | 149 | // ------------------------------------------------------------------------- 150 | 151 | // order of what gets the highest priority: 152 | // 153 | // 1. frontmatter 154 | // 2. page excerpt 155 | // 3. content markdown paragraph 156 | // 4. content regular html

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 `![alt text](http://url)` 193 | // 3. content regular html img 194 | 195 | image_sources: [ 196 | 197 | 'frontmatter', 198 | 199 | /!\[.*?\]\((.*?)\)/i, // markdown image regex 200 | / _.reverse( _.sortBy( entries, 'date' ) ), 233 | sort: entries => entries, // defaults to just returning it as it is 234 | 235 | // ------------------------------------------------------------------------- 236 | 237 | // supported - use in config as needed 238 | 239 | // category 240 | // contributor 241 | 242 | }; 243 | 244 | // --------------------------------------------------------------------------- 245 | 246 | return out; 247 | 248 | }; 249 | // PLUGIN.get_options_defaults() 250 | 251 | 252 | 253 | /** 254 | * @return {object} 255 | */ 256 | PLUGIN.get_options = ( plugin_options, context ) => 257 | { 258 | 259 | if ( _.isEmpty( PLUGIN.options ) ) 260 | { 261 | PLUGIN.options = _.defaultsDeep( 262 | plugin_options, 263 | PLUGIN.get_options_defaults( context ) 264 | ); 265 | 266 | // ------------------------------------------------------------------------- 267 | 268 | // default link and id 269 | 270 | if ( ! PLUGIN.options.feed_options.hasOwnProperty('link') ) 271 | { 272 | PLUGIN.options.feed_options.link = plugin_options.canonical_base; 273 | } 274 | 275 | if ( ! PLUGIN.options.feed_options.hasOwnProperty('id') ) 276 | { 277 | PLUGIN.options.feed_options.id = plugin_options.canonical_base; 278 | } 279 | 280 | // ------------------------------------------------------------------------- 281 | 282 | // default feedLinks 283 | 284 | if ( ! PLUGIN.options.feed_options.hasOwnProperty('feedLinks') 285 | && ! _.isEmpty( PLUGIN.options.feeds ) ) 286 | { 287 | PLUGIN.options.feed_options.feedLinks = {}; 288 | 289 | const feeds = PLUGIN.options.feeds || {}; 290 | 291 | for ( let key of Object.keys( feeds ) ) 292 | { 293 | if ( ! PLUGIN.allowed_feed_types.includes( key ) ) 294 | { 295 | continue; 296 | } 297 | 298 | // --------------------------------------------------------------------- 299 | 300 | const url = PLUGIN.get_feed_url( feeds[ key ] ); 301 | 302 | if ( ! url ) 303 | { 304 | continue; 305 | } 306 | 307 | // --------------------------------------------------------------------- 308 | 309 | key = key.replace(/[0-9]/g, ''); // remove numbers from key; 310 | 311 | PLUGIN.options.feed_options.feedLinks[ key ] = url; 312 | } 313 | } 314 | 315 | // ------------------------------------------------------------------------- 316 | 317 | // internal - used in other files/classes 318 | 319 | PLUGIN.options._internal = { 320 | name : PLUGIN.name, 321 | homepage : PLUGIN.homepage, 322 | key : PLUGIN.key, 323 | allowed_feed_types: PLUGIN.allowed_feed_types, 324 | }; 325 | 326 | } 327 | 328 | // --------------------------------------------------------------------------- 329 | 330 | return PLUGIN.options; 331 | 332 | }; 333 | // PLUGIN.get_options() 334 | 335 | 336 | 337 | /** 338 | * @return {bool} 339 | */ 340 | PLUGIN.good_to_go = ( plugin_options, context ) => 341 | { 342 | 343 | const options = PLUGIN.get_options( plugin_options, context ); 344 | 345 | // --------------------------------------------------------------------------- 346 | 347 | return ( options.canonical_base 348 | && ! _.isEmpty( options.feeds ) 349 | && ! _.isEmpty( PLUGIN.pages ) ); 350 | 351 | }; 352 | // PLUGIN.good_to_go() 353 | 354 | 355 | 356 | /** 357 | * @return {string} 358 | */ 359 | PLUGIN.get_feed_url = feed => 360 | { 361 | 362 | if ( feed.enable && feed.file_name ) 363 | { 364 | return UTIL.resolve_url(PLUGIN.options.canonical_base, feed.file_name); 365 | } 366 | 367 | }; 368 | // PLUGIN.get_feed_url() 369 | 370 | 371 | 372 | /** 373 | * @return {bool} 374 | */ 375 | PLUGIN.get_page_feed_settings = frontmatter => frontmatter.feed || {}; 376 | 377 | 378 | 379 | /** 380 | * @return {bool} 381 | */ 382 | PLUGIN.get_page_type = frontmatter => frontmatter.type || ''; 383 | 384 | 385 | 386 | /** 387 | * @return {bool} 388 | */ 389 | PLUGIN.is_page_type_post = frontmatter => ( 'post' === PLUGIN.get_page_type( frontmatter ).toLowerCase() ); 390 | 391 | 392 | 393 | /** 394 | * @return {bool} 395 | */ 396 | PLUGIN.is_feed_page = ( page ) => 397 | { 398 | 399 | const { frontmatter, path } = page; 400 | 401 | // --------------------------------------------------------------------------- 402 | 403 | if ( ! _.isEmpty( frontmatter ) ) 404 | { 405 | // use `frontmatter.feed.enable` to exclude a particular page/post 406 | // bailout if it is set to false 407 | 408 | const page_feed_settings = PLUGIN.get_page_feed_settings( frontmatter ); 409 | 410 | /* 411 | if ( page_feed_settings.hasOwnProperty('enable') 412 | && ! page_feed_settings.enable ) 413 | { 414 | return false; 415 | } 416 | */ 417 | 418 | // @notes: 419 | // as opposed to the above way of bailing out if set to false 420 | // the following means that any page that has `frontmatter.feed.enable` 421 | // set to true will be added 422 | 423 | if ( page_feed_settings.hasOwnProperty('enable') ) 424 | { 425 | return ( page_feed_settings.enable ); 426 | } 427 | 428 | // ------------------------------------------------------------------------- 429 | 430 | if ( PLUGIN.is_page_type_post( frontmatter ) ) 431 | { 432 | return true; 433 | } 434 | } 435 | 436 | // --------------------------------------------------------------------------- 437 | 438 | const directories = PLUGIN.options.posts_directories || []; 439 | 440 | if ( ! _.isEmpty( directories ) ) 441 | { 442 | for ( const dir of directories ) 443 | { 444 | if ( path.startsWith(`${dir}`) ) 445 | { 446 | return true; 447 | } 448 | } 449 | } 450 | 451 | // --------------------------------------------------------------------------- 452 | 453 | return false; 454 | 455 | }; 456 | // PLUGIN.is_feed_page() 457 | 458 | // ----------------------------------------------------------------------------- 459 | 460 | module.exports = ( plugin_options, context ) => ({ 461 | 462 | /** 463 | * used for collecting pages that would be used in feed; 464 | * the reason i'm using this, is that `getSiteData` only gets `page.toJson()`, 465 | * which only assigns preperties that don't start with '_' 466 | * and what i need is the $page._strippedContent to get content for the feed 467 | */ 468 | extendPageData ( $page ) { 469 | 470 | try { 471 | 472 | if ( PLUGIN.get_options( plugin_options, context ).is_feed_page( $page ) ) 473 | { 474 | PLUGIN.pages.push( $page ); 475 | } 476 | 477 | } catch ( err ) { 478 | 479 | LIB.LOG.error( err.message ); 480 | 481 | } 482 | 483 | }, 484 | 485 | // --------------------------------------------------------------------------- 486 | 487 | /** 488 | * used for adding head links 489 | */ 490 | async ready() { 491 | 492 | try { 493 | 494 | if ( PLUGIN.good_to_go( plugin_options, context ) ) 495 | { 496 | await new LIB.Head( PLUGIN.options, context ).add_links(); 497 | } 498 | 499 | } catch ( err ) { 500 | 501 | LIB.LOG.error( err.message ); 502 | 503 | } 504 | 505 | }, 506 | 507 | // --------------------------------------------------------------------------- 508 | 509 | /** 510 | * used for generating the feed files 511 | */ 512 | async generated ( pagePaths ) { 513 | 514 | try { 515 | 516 | if ( PLUGIN.good_to_go( plugin_options, context ) ) 517 | { 518 | await new LIB.Generator( PLUGIN.pages, PLUGIN.options, context ).generate(); 519 | } 520 | 521 | } catch ( err ) { 522 | 523 | LIB.LOG.error( err.message ); 524 | 525 | } 526 | 527 | } 528 | 529 | }); 530 | --------------------------------------------------------------------------------