├── .gitattributes ├── .travis.yml ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── CODE_OF_CONDUCT.md ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.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 | - 'stable' 15 | 16 | 17 | install: 18 | - npm install 19 | 20 | 21 | deploy: 22 | provider: npm 23 | email: $NPM_EMAIL 24 | api_key: $NPM_TOKEN 25 | skip_cleanup: true 26 | on: 27 | tags: true 28 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 [issue template](ISSUE_TEMPLATE.md). 16 | 17 | 18 | ## Submitting a Pull Request 19 | 20 | - Open a new issue in the [issue tracker][issue tracker url] if needed. 21 | - Fork the repo. 22 | - Create a new branch based off the master branch. 23 | - Make sure all tests pass. 24 | - Submit a pull request, referencing any issues it addresses. 25 | 26 | [issue tracker url]: https://github.com/webmasterish/vuepress-plugin-autometa/issues 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Description 9 | 10 | 11 | 12 | 13 | ## Expected Behavior 14 | 15 | 16 | 17 | 18 | ## Actual Behavior 19 | 20 | 21 | 22 | 23 | ## Steps to Reproduce 24 | 25 | 26 | 31 | 32 | 33 | ## Your Environment 34 | 35 | - vuepress-plugin-autometa version: 36 | - vuepress version : 37 | - node version : 38 | - npm version : 39 | - OS : 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuepress-plugin-autometa", 3 | "version": "0.1.13", 4 | "description": "Auto meta tags plugin for VuePress", 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-autometa.git" 13 | }, 14 | "keywords": [ 15 | "vue", 16 | "vuepress", 17 | "plugin", 18 | "vuepress-plugin", 19 | "meta", 20 | "meta-tags", 21 | "social", 22 | "social-metas", 23 | "page", 24 | "post", 25 | "head" 26 | ], 27 | "author": { 28 | "name": "webmasterish", 29 | "email": "webmasterish@gmail.com", 30 | "url": "https://webmasterish.com" 31 | }, 32 | "license": "MIT", 33 | "homepage": "https://github.com/webmasterish/vuepress-plugin-autometa", 34 | "bugs": { 35 | "url": "https://github.com/webmasterish/vuepress-plugin-autometa/issues" 36 | }, 37 | "engines": { 38 | "node": ">=8" 39 | }, 40 | "dependencies": { 41 | "lodash.defaultsdeep": "4.6.1", 42 | "lodash.findindex": "4.6.0", 43 | "lodash.isempty": "4.4.0", 44 | "lodash.trimend": "^4.5.1", 45 | "lodash.trimstart": "^4.5.1", 46 | "remove-markdown": "0.3.0", 47 | "striptags": "3.1.1" 48 | }, 49 | "files": [ 50 | "index.js", 51 | "README.md", 52 | "LICENSE" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VuePress Plugin Auto Meta 2 | 3 | > Auto meta tags plugin for [VuePress](https://vuepress.vuejs.org/) 1.x 4 | 5 | [![Build Status](https://img.shields.io/travis/webmasterish/vuepress-plugin-autometa/master.svg?style=flat-square)](https://travis-ci.org/webmasterish/vuepress-plugin-autometa) 6 | [![npm version](https://img.shields.io/npm/v/vuepress-plugin-autometa.svg?style=flat-square)](http://npm.im/vuepress-plugin-autometa) 7 | [![Greenkeeper badge](https://badges.greenkeeper.io/webmasterish/vuepress-plugin-autometa.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 | ## What 12 | 13 | This is a Plug-and-Forget VuePress plugin that will auto generate the meta tags 14 | for VuePress pages or posts. 15 | 16 | 17 | ## Install 18 | 19 | 20 | ```sh 21 | $ npm install -D vuepress-plugin-autometa 22 | 23 | # or 24 | 25 | $ yarn add -D vuepress-plugin-autometa 26 | ``` 27 | 28 | 29 | ## Usage 30 | 31 | Add `vuepress-plugin-autometa` in your site or theme config file. 32 | 33 | > See [official docs on using a plugin](https://vuepress.vuejs.org/plugin/using-a-plugin.html) 34 | 35 | 36 | ```js 37 | // .vuepress/config.js 38 | // or 39 | // .vuepress/theme/index.js 40 | 41 | // set your global autometa options - override in page frontmatter 42 | const autometa_options = { 43 | site: { 44 | name : 'Webmasterish', 45 | twitter: 'webmasterish', 46 | }, 47 | canonical_base: 'https://webmasterish.com', 48 | }; 49 | 50 | module.exports = { 51 | plugins: [ 52 | [ 'autometa', autometa_options ] 53 | ] 54 | } 55 | ``` 56 | 57 | 58 | ## Options 59 | 60 | > See Plugin Option API [official docs](https://vuepress.vuejs.org/plugin/option-api.html) 61 | 62 | 63 | ### Default options 64 | 65 | You can override default options in 2 ways: 66 | 67 | 1. Global plugin options set in `.vuepress/config.js` or `.vuepress/theme/index.js` 68 | as described in [Usage](#usage) 69 | 2. Individual page/post `frontmatter` as shown in [Examples](#examples) 70 | 71 | 72 | ```js 73 | const default_options = { 74 | 75 | enable : true, // enables/disables everything - control per page using frontmatter 76 | image : true, // regular meta image used by search engines 77 | twitter: true, // twitter card 78 | og : true, // open graph: facebook, pinterest, google+ 79 | schema : true, // schema.org for google 80 | 81 | // ------------------------------------------------------------------------- 82 | 83 | // canonical_base is the canonical url base - best to set once in config.js 84 | // if set it will be used to prepend page path and add it to the following: 85 | // - twitter:url 86 | // - og:url 87 | // - canonical link (not yet supported) 88 | 89 | canonical_base: '', 90 | 91 | // @todo 92 | //canonical_link: true, 93 | // 94 | // having only started with vuepress a few days ago, 95 | // so far, i couldn't figure out a proper way to extend config head 96 | // and add 97 | // feel free to tip-in 98 | 99 | // --------------------------------------------------------------------------- 100 | 101 | author: { 102 | name : '', 103 | twitter: '', 104 | }, 105 | 106 | // --------------------------------------------------------------------------- 107 | 108 | site: { 109 | name : '', 110 | twitter: '', 111 | }, 112 | 113 | // --------------------------------------------------------------------------- 114 | 115 | // order of what gets the highest priority: 116 | // 117 | // 1. frontmatter 118 | // 2. page excerpt 119 | // 3. content markdown paragraph 120 | // 4. content regular html

121 | 122 | description_sources: [ 123 | 124 | 'frontmatter', 125 | 'excerpt', 126 | 127 | // markdown paragraph regex 128 | // 129 | /^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/img, 130 | // 131 | // this excludes blockquotes using `(?!^>)` 132 | ///^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^>)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/img, 133 | 134 | // html paragraph regex 135 | /(.*?)<\/p>/i, 136 | 137 | ], 138 | 139 | // --------------------------------------------------------------------------- 140 | 141 | // order of what gets the highest priority: 142 | // 143 | // 1. frontmatter 144 | // 2. content markdown image such as `![alt text](http://url)` 145 | // 3. content regular html img 146 | 147 | image_sources: [ 148 | 149 | 'frontmatter', 150 | 151 | /!\[.*?\]\((.*?)\)/i, // markdown image regex 152 | / 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | ``` 238 | 239 | 240 | ## Reference 241 | 242 | - VuePress official [plugin docs](https://vuepress.vuejs.org/plugin/) 243 | - VuePress official [Front Matter](https://vuepress.vuejs.org/guide/frontmatter.html) 244 | - [HEAD guide](https://github.com/joshbuchea/HEAD) 245 | - [Twitter Cards](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards) 246 | - [Open Graph protocol](http://ogp.me/) 247 | - [Schema.org](https://schema.org/) 248 | 249 | 250 | ## License 251 | 252 | MIT © [webmasterish](https://webmasterish.com) 253 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = { 4 | defaultsDeep: require('lodash.defaultsdeep'), 5 | findIndex : require('lodash.findindex'), 6 | isEmpty : require('lodash.isempty'), 7 | trimStart: require('lodash.trimstart'), 8 | trimEnd: require('lodash.trimend') 9 | }; 10 | 11 | // ----------------------------------------------------------------------------- 12 | 13 | const REMOVE_MARKDOWN = require('remove-markdown'); 14 | const STRIPTAGS = require('striptags'); 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | /** 19 | * holds relevant functions and data 20 | */ 21 | const PLUGIN = { 22 | name : 'autometa', // the would also be the key used in frontmatter 23 | default_metas : {} 24 | }; 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | const resolveURL = ( base, path ) => 29 | `${_.trimEnd( base, '/' )}/${_.trimStart( path, '/' )}` 30 | 31 | /** 32 | * @return {object} 33 | */ 34 | PLUGIN.get_options_defaults = () => 35 | { 36 | 37 | const out = { 38 | 39 | enable : true, // enables/disables everything - control per page using frontmatter 40 | image : true, // regular meta image used by search engines 41 | twitter : true, // twitter card 42 | og : true, // open graph: facebook, pinterest, google+ 43 | schema : true, // schema.org for google 44 | 45 | // ------------------------------------------------------------------------- 46 | 47 | // canonical_base is the canonical url base - best to set once in config.js 48 | // if set it will be used to prepend page path and add it to the following: 49 | // - canonical link 50 | // - twitter:url 51 | // - og:url 52 | 53 | canonical_base: '', 54 | 55 | // @todo 56 | //canonical_link: true, 57 | // 58 | // having only started with vuepress a few days ago, 59 | // so far, i couldn't figure out a proper way to extend config head 60 | // and add 61 | 62 | // ------------------------------------------------------------------------- 63 | 64 | author: { 65 | name : '', 66 | twitter : '', 67 | //url : '', // not used currently 68 | }, 69 | 70 | // ------------------------------------------------------------------------- 71 | 72 | // @notes: 73 | // 74 | // it's more logical to have this one set once in 75 | // `.vuepress/config.js` or `.vuepress/theme/index.js` `head` 76 | 77 | site: { 78 | name : '', 79 | twitter : '', 80 | //url : '', // not used currently 81 | }, 82 | 83 | // ------------------------------------------------------------------------- 84 | 85 | // not sure if these should be allowed to be set in frontmatter 86 | 87 | // order of what gets the highest priority: 88 | // 89 | // 1. frontmatter 90 | // 2. page excerpt 91 | // 3. content markdown paragraph 92 | // 4. content regular html

93 | 94 | description_sources: [ 95 | 96 | 'frontmatter', 97 | 'excerpt', 98 | 99 | // markdown paragraph regex 100 | // @todo: needs work 101 | // 102 | /^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/img, 103 | // 104 | // this excludes blockquotes using `(?!^>)` 105 | ///^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^!\[.*?\]\((.*?)\))(?!^>)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n/img, 106 | 107 | // html paragraph regex 108 | /(.*?)<\/p>/i, 109 | 110 | // ----------------------------------------------------------------------- 111 | 112 | // @notes: setting as array require escaping `\` 113 | 114 | //['^((?:(?!^#)(?!^\-|\+)(?!^[0-9]+\.)(?!^\[\[.*?\]\])(?!^\{\{.*?\}\})[^\n]|\n(?! *\n))+)(?:\n *)+\n', 'img'], 115 | //['(.*?)<\/p>', 'i'], 116 | 117 | ], 118 | 119 | // ------------------------------------------------------------------------- 120 | 121 | // @consider description max words/char 122 | 123 | // ------------------------------------------------------------------------- 124 | 125 | // order of what gets the highest priority: 126 | // 127 | // 1. frontmatter 128 | // 2. content markdown image such as `![alt text](http://url)` 129 | // 3. content regular html img 130 | 131 | image_sources: [ 132 | 133 | 'frontmatter', 134 | 135 | /!\[.*?\]\((.*?)\)/i, // markdown image regex 136 | / 162 | { 163 | 164 | const { frontmatter } = $page; 165 | 166 | // --------------------------------------------------------------------------- 167 | 168 | // order of options override: 169 | // - defaults -> gets set in this file by `PLUGIN.get_default_options()` 170 | // - plugin options -> gets set in `config.js` 171 | // - frontmatter -> gets set in each page 172 | 173 | const options = _.defaultsDeep( 174 | frontmatter[ PLUGIN.name ], 175 | plugin_options, 176 | PLUGIN.get_options_defaults() 177 | ); 178 | 179 | // --------------------------------------------------------------------------- 180 | 181 | return options; 182 | 183 | }; 184 | // PLUGIN.get_options() 185 | 186 | 187 | 188 | /** 189 | * @return {string} 190 | */ 191 | PLUGIN.strip_markup = str => STRIPTAGS( REMOVE_MARKDOWN( str, { useImgAltText: false } ) ); 192 | 193 | 194 | 195 | /** 196 | * @return {RegExp} 197 | */ 198 | PLUGIN.get_regex = re => ( Array.isArray( re ) ) ? new RegExp( ...re ) : re; 199 | 200 | 201 | 202 | /** 203 | * check if string is a valid url 204 | * 205 | * @param {string} maybe_url 206 | * @return {bool} 207 | */ 208 | PLUGIN.is_url = ( maybe_url ) => 209 | { 210 | 211 | if ( ! maybe_url || typeof maybe_url !== 'string' ) 212 | { 213 | return false; 214 | } 215 | 216 | // --------------------------------------------------------------------------- 217 | 218 | const re_protocol_and_domain = /^(?:\w+:)?\/\/(\S+)$/; 219 | 220 | const match = maybe_url.match( re_protocol_and_domain ); 221 | 222 | if ( ! match ) 223 | { 224 | return false; 225 | } 226 | 227 | // --------------------------------------------------------------------------- 228 | 229 | const all_after_protocol = match[1]; 230 | 231 | if ( ! all_after_protocol ) 232 | { 233 | return false; 234 | } 235 | 236 | // --------------------------------------------------------------------------- 237 | 238 | const re_domain_localhost = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/ 239 | const re_domain_non_localhost = /^[^\s\.]+\.\S{2,}$/; 240 | 241 | return ( re_domain_localhost.test( all_after_protocol ) 242 | || re_domain_non_localhost.test( all_after_protocol ) ); 243 | 244 | } 245 | // PLUGIN.is_url() 246 | 247 | /** 248 | * @return {string} 249 | */ 250 | PLUGIN.get_canonical_url = ( $page, options ) => 251 | { 252 | 253 | if ( options.canonical_base && $page.path ) 254 | { 255 | return resolveURL( options.canonical_base, $page.path ); 256 | } 257 | 258 | }; 259 | // PLUGIN.get_canonical_url() 260 | 261 | 262 | 263 | /** 264 | * @return {string} 265 | */ 266 | PLUGIN.get_default_date = ( $page, options ) => 267 | { 268 | 269 | const { frontmatter } = $page; 270 | 271 | if ( frontmatter.date ) 272 | { 273 | return frontmatter.date; 274 | } 275 | 276 | }; 277 | // PLUGIN.get_default_date() 278 | 279 | 280 | 281 | /** 282 | * @return {string} 283 | */ 284 | PLUGIN.get_default_title = ( $page, options ) => 285 | { 286 | 287 | // default to page title 288 | 289 | let out = $page.title || ''; 290 | 291 | // --------------------------------------------------------------------------- 292 | 293 | // special handling for home 294 | 295 | const { frontmatter, _computed } = $page; 296 | 297 | if ( frontmatter.home ) 298 | { 299 | const site = _computed.$site; 300 | 301 | // @notes 302 | // highly unlikely, but i'm falling back to empty string 303 | // in case it was intentionally set so that no home title is added 304 | out = site.title || ''; 305 | } 306 | 307 | // --------------------------------------------------------------------------- 308 | 309 | return out; 310 | 311 | }; 312 | // PLUGIN.get_default_title() 313 | 314 | 315 | 316 | /** 317 | * @return {string} 318 | */ 319 | PLUGIN.get_default_description = ( $page, options ) => 320 | { 321 | 322 | // special handling for home 323 | 324 | const { frontmatter, _computed } = $page; 325 | 326 | if ( frontmatter.home ) 327 | { 328 | const site = _computed.$site; 329 | const description = site.description || ''; 330 | 331 | return description; 332 | } 333 | 334 | // --------------------------------------------------------------------------- 335 | 336 | if ( _.isEmpty( options['description_sources'] ) ) 337 | { 338 | return; 339 | } 340 | 341 | // --------------------------------------------------------------------------- 342 | 343 | let out = ''; 344 | 345 | for ( const source of options['description_sources'] ) 346 | { 347 | let found = ''; 348 | 349 | // ------------------------------------------------------------------------- 350 | 351 | switch ( source ) 352 | { 353 | case 'frontmatter': 354 | 355 | if ( $page.frontmatter.description ) 356 | { 357 | found = $page.frontmatter.description; 358 | } 359 | 360 | break; 361 | 362 | // ----------------------------------------------------------------------- 363 | 364 | case 'excerpt': 365 | 366 | if ( $page.excerpt ) 367 | { 368 | found = $page.excerpt; 369 | } 370 | 371 | break; 372 | 373 | // ----------------------------------------------------------------------- 374 | 375 | default: 376 | 377 | // content without frontmatter - used with regex 378 | 379 | const content = $page._strippedContent || ''; 380 | 381 | if ( content ) 382 | { 383 | const regex = PLUGIN.get_regex( source ); 384 | 385 | let match; 386 | 387 | if ( ( match = regex.exec( content ) ) !== null ) 388 | { 389 | if ( match[1] ) 390 | { 391 | found = match[1]; 392 | } 393 | } 394 | } 395 | 396 | break; 397 | } 398 | 399 | // ------------------------------------------------------------------------- 400 | 401 | found = PLUGIN.strip_markup( found.trim() ); 402 | 403 | // ------------------------------------------------------------------------- 404 | 405 | if ( found ) 406 | { 407 | out = found; 408 | 409 | break; 410 | } 411 | } 412 | 413 | // --------------------------------------------------------------------------- 414 | 415 | return out; 416 | 417 | }; 418 | // PLUGIN.get_default_description() 419 | 420 | 421 | 422 | /** 423 | * @return {string} 424 | */ 425 | PLUGIN.get_default_image_url = ( $page, options ) => 426 | { 427 | 428 | if ( _.isEmpty( options['image_sources'] ) ) 429 | { 430 | return; 431 | } 432 | 433 | // --------------------------------------------------------------------------- 434 | 435 | let out = ''; 436 | 437 | for ( const source of options['image_sources'] ) 438 | { 439 | if ( 'frontmatter' === source ) 440 | { 441 | if ( $page.frontmatter.image ) 442 | { 443 | out = $page.frontmatter.image; 444 | 445 | break; 446 | } 447 | } 448 | else 449 | { 450 | // content without frontmatter - used with regex 451 | 452 | const content = $page._strippedContent || ''; 453 | 454 | if ( ! content ) 455 | { 456 | break; 457 | } 458 | 459 | // ----------------------------------------------------------------------- 460 | 461 | const regex = PLUGIN.get_regex( source ); 462 | 463 | let match; 464 | 465 | if ( ( match = regex.exec( content ) ) !== null ) 466 | { 467 | if ( match[1] ) 468 | { 469 | out = match[1]; 470 | 471 | break; 472 | } 473 | } 474 | } 475 | } 476 | 477 | // --------------------------------------------------------------------------- 478 | 479 | out = out.trim(); 480 | 481 | if ( ! out ) 482 | { 483 | return; 484 | } 485 | 486 | // --------------------------------------------------------------------------- 487 | 488 | // support for image url as relative path 489 | 490 | if ( PLUGIN.is_url( out ) ) 491 | { 492 | return out; 493 | } 494 | else 495 | { 496 | // only return it if we have a base url, 497 | // otherwise it's meaningless to add it 498 | 499 | if ( options.canonical_base ) 500 | { 501 | out = resolveURL( options.canonical_base, out ); 502 | 503 | return out; 504 | } 505 | } 506 | 507 | }; 508 | // PLUGIN.get_default_image_url() 509 | 510 | 511 | 512 | /** 513 | * @return {object} 514 | */ 515 | PLUGIN.get_default_author = ( $page, options ) => 516 | { 517 | 518 | // @consider 519 | // 520 | // if author data is empty to default to git author if it has relevant details 521 | 522 | return options.author; 523 | 524 | }; 525 | // PLUGIN.get_default_author() 526 | 527 | 528 | 529 | /** 530 | * @return {object} 531 | */ 532 | PLUGIN.get_default_site = ( $page, options ) => 533 | { 534 | 535 | // @consider to default site name to _computed.$site.title 536 | 537 | return options.site; 538 | 539 | }; 540 | // PLUGIN.get_default_site() 541 | 542 | 543 | 544 | /** 545 | * @return {array} 546 | */ 547 | PLUGIN.default_metas.image = ( $page, default_values ) => 548 | { 549 | 550 | const out = []; 551 | 552 | // --------------------------------------------------------------------------- 553 | 554 | if ( default_values.image ) 555 | { 556 | out.push({ 557 | name : 'image', 558 | content : default_values.image, 559 | }); 560 | } 561 | 562 | // --------------------------------------------------------------------------- 563 | 564 | return out; 565 | 566 | }; 567 | // PLUGIN.default_metas.image() 568 | 569 | 570 | 571 | /** 572 | * @return {array} 573 | */ 574 | PLUGIN.default_metas.twitter = ( $page, default_values ) => 575 | { 576 | 577 | const out = [ 578 | { 579 | name : 'twitter:title', 580 | content : default_values.title, 581 | }, 582 | { 583 | name : 'twitter:description', 584 | content : default_values.description, 585 | }, 586 | ]; 587 | 588 | // --------------------------------------------------------------------------- 589 | 590 | if ( default_values.image ) 591 | { 592 | out.push({ 593 | name : 'twitter:card', 594 | content : 'summary_large_image', 595 | }); 596 | 597 | out.push({ 598 | name : 'twitter:image', 599 | content : default_values.image, 600 | }); 601 | } 602 | else 603 | { 604 | out.push({ 605 | name : 'twitter:card', 606 | content : 'summary', 607 | }); 608 | } 609 | 610 | // --------------------------------------------------------------------------- 611 | 612 | if ( default_values.canonical_url ) 613 | { 614 | out.push({ 615 | name : 'twitter:url', 616 | content : default_values.canonical_url, 617 | }); 618 | } 619 | 620 | // --------------------------------------------------------------------------- 621 | 622 | if ( default_values.author.twitter ) 623 | { 624 | out.push({ 625 | name : 'twitter:creator', 626 | content : `@${default_values.author.twitter.replace('@', '')}`, // @username 627 | }); 628 | } 629 | 630 | // --------------------------------------------------------------------------- 631 | 632 | if ( default_values.site.twitter ) 633 | { 634 | out.push({ 635 | name : 'twitter:site', 636 | content : `@${default_values.site.twitter.replace('@', '')}`, // @site_account 637 | }); 638 | } 639 | 640 | // --------------------------------------------------------------------------- 641 | 642 | return out; 643 | 644 | }; 645 | // PLUGIN.default_metas.twitter() 646 | 647 | 648 | 649 | /** 650 | * @return {array} 651 | */ 652 | PLUGIN.default_metas.og = ( $page, default_values ) => 653 | { 654 | 655 | let type = 'article'; 656 | 657 | const { frontmatter } = $page; 658 | 659 | if ( frontmatter.home ) 660 | { 661 | type = 'website'; 662 | } 663 | 664 | // --------------------------------------------------------------------------- 665 | 666 | let out = [ 667 | { 668 | property: 'og:type', 669 | content : type, 670 | }, 671 | { 672 | property: 'og:title', 673 | content : default_values.title, 674 | }, 675 | { 676 | property: 'og:description', 677 | content : default_values.description, 678 | }, 679 | ]; 680 | 681 | // --------------------------------------------------------------------------- 682 | 683 | if ( default_values.image ) 684 | { 685 | out.push({ 686 | property: 'og:image', 687 | content : default_values.image, 688 | }); 689 | } 690 | 691 | // --------------------------------------------------------------------------- 692 | 693 | if ( default_values.canonical_url ) 694 | { 695 | out.push({ 696 | property: 'og:url', 697 | content : default_values.canonical_url, 698 | }); 699 | } 700 | 701 | // --------------------------------------------------------------------------- 702 | 703 | if ( default_values.site.name ) 704 | { 705 | out.push({ 706 | property: 'og:site_name', 707 | content : default_values.site.name, 708 | }); 709 | } 710 | 711 | // --------------------------------------------------------------------------- 712 | 713 | // og article related 714 | 715 | if ( 'article' === type ) 716 | { 717 | if ( default_values.author.name ) 718 | { 719 | out.push({ 720 | property: 'article:author', 721 | content : default_values.author.name, 722 | }); 723 | } 724 | 725 | // ------------------------------------------------------------------------- 726 | 727 | if ( default_values.date ) 728 | { 729 | out.push({ 730 | property: 'article:published_time', 731 | content : default_values.date, 732 | }); 733 | } 734 | 735 | // ------------------------------------------------------------------------- 736 | 737 | // @consider article:modified_time 738 | 739 | // ------------------------------------------------------------------------- 740 | 741 | if ( ! _.isEmpty( frontmatter.tags ) 742 | && Array.isArray( frontmatter.tags ) ) 743 | { 744 | for ( const tag of frontmatter.tags ) 745 | { 746 | out.push({ 747 | property: 'article:tag', 748 | content : tag, 749 | }); 750 | } 751 | } 752 | } 753 | 754 | // --------------------------------------------------------------------------- 755 | 756 | return out; 757 | 758 | }; 759 | // PLUGIN.default_metas.og() 760 | 761 | 762 | 763 | /** 764 | * @return {array} 765 | */ 766 | PLUGIN.default_metas.schema = ( $page, default_values ) => 767 | { 768 | 769 | const out = [ 770 | { 771 | itemprop: 'name', 772 | content : default_values.title, 773 | }, 774 | { 775 | itemprop: 'description', 776 | content : default_values.description, 777 | }, 778 | ]; 779 | 780 | // --------------------------------------------------------------------------- 781 | 782 | if ( default_values.image ) 783 | { 784 | out.push({ 785 | itemprop: 'image', 786 | content : default_values.image, 787 | }); 788 | } 789 | 790 | // --------------------------------------------------------------------------- 791 | 792 | // @todo: 793 | // check if these meta tags require the `itemscope` and `itemtype` attributes 794 | // to be added to tag 795 | 796 | // --------------------------------------------------------------------------- 797 | 798 | return out; 799 | 800 | }; 801 | // PLUGIN.default_metas.schema() 802 | 803 | 804 | 805 | /** 806 | * @return {array} 807 | */ 808 | PLUGIN.default_meta_tags = ( $page, default_values, options ) => 809 | { 810 | 811 | let out = []; 812 | 813 | // --------------------------------------------------------------------------- 814 | 815 | const keys = Object.keys( PLUGIN.default_metas ); 816 | 817 | for ( const key of keys ) 818 | { 819 | if ( options[ key ] ) 820 | { 821 | out = out.concat( PLUGIN.default_metas[ key ]( $page, default_values ) ); 822 | } 823 | } 824 | 825 | // --------------------------------------------------------------------------- 826 | 827 | return out.filter( e => e ); 828 | 829 | }; 830 | // PLUGIN.default_meta_tags() 831 | 832 | // ----------------------------------------------------------------------------- 833 | 834 | module.exports = ( plugin_options, context ) => ({ 835 | 836 | extendPageData ( $page ) { 837 | 838 | const { frontmatter } = $page; 839 | 840 | // ------------------------------------------------------------------------- 841 | 842 | const options = PLUGIN.get_options( $page, plugin_options ); 843 | 844 | if ( ! options.enable ) 845 | { 846 | return; 847 | } 848 | 849 | // ------------------------------------------------------------------------- 850 | 851 | frontmatter.description = frontmatter.description || PLUGIN.get_default_description( $page, options ); 852 | 853 | // ------------------------------------------------------------------------- 854 | 855 | const default_values = { 856 | title : PLUGIN.get_default_title( $page, options ), 857 | date : PLUGIN.get_default_date( $page, options ), 858 | description : frontmatter.description, 859 | image_url : PLUGIN.get_default_image_url( $page, options ), 860 | canonical_url : PLUGIN.get_canonical_url( $page, options ), 861 | author : PLUGIN.get_default_author( $page, options ), 862 | site : PLUGIN.get_default_site( $page, options ), 863 | }; 864 | 865 | default_values.image = default_values.image_url; 866 | default_values.canonical = default_values.canonical_url; 867 | 868 | // ------------------------------------------------------------------------- 869 | 870 | const default_metas = PLUGIN.default_meta_tags( $page, default_values, options ); 871 | 872 | if ( _.isEmpty( default_metas ) ) 873 | { 874 | return; 875 | } 876 | 877 | // ------------------------------------------------------------------------- 878 | 879 | frontmatter.meta = frontmatter.meta || []; 880 | 881 | for ( const meta of default_metas ) 882 | { 883 | // only add it if not already set in frontmatter 884 | 885 | const index = _.findIndex( frontmatter.meta, ['name', meta.name] ); 886 | 887 | if ( index === -1 ) 888 | { 889 | frontmatter.meta.push( meta ); 890 | } 891 | } 892 | 893 | } 894 | 895 | }); 896 | --------------------------------------------------------------------------------