├── .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 | [](https://travis-ci.org/webmasterish/vuepress-plugin-autometa)
6 | [](http://npm.im/vuepress-plugin-autometa)
7 | [](https://greenkeeper.io/)
8 | [](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 ``
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 ``
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 |
--------------------------------------------------------------------------------