├── .eleventy.js ├── .eleventyignore ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── FUNDING.yml ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── metagen_spec.js ├── plugins │ └── index.js └── videos │ └── metagen_spec.js.mp4 ├── package-lock.json ├── package.json └── tests └── index.njk /.eleventy.js: -------------------------------------------------------------------------------- 1 | const metagen = require('meta-generator'); 2 | 3 | module.exports = (eleventyConfig, pluginNamespace) => { 4 | eleventyConfig.namespace(pluginNamespace, () => { 5 | eleventyConfig.addShortcode('metagen', (data) => { 6 | const head = metagen(data); 7 | if (Array.isArray(head)) { 8 | return head.join('\n'); 9 | } else if (typeof head === 'string') { 10 | return head; 11 | } else { 12 | return ''; 13 | } 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | # To avoid using raw blocks 2 | README.md 3 | CONTRIBUTING.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eleventy Site Output 2 | _site 3 | 4 | # node modules 5 | node_modules 6 | 7 | # npm rc 8 | .npmrc 9 | 10 | # DS Store 11 | .DS_STORE -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Cypress tests 2 | cypress/ 3 | cypress.json 4 | tests/ 5 | 6 | # 11ty site output 7 | _site/ 8 | 9 | # Ignore 10 | .eleventyignore 11 | .gitignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | If you notice an issue or there is metadata that you need generated which isn't currently supported, feel free to open an issue. New feature requests are always welcome. 3 | 4 | 1. Fork this repository 5 | 2. `git clone git@github.com:/eleventy-plugin-metagen.git` 6 | 3. `npm install` 7 | 4. `npm run build` 8 | 5. `npm run dev` 9 | 10 | When submitting a feature request or bugfix, feel free to use whatever branch naming you prefer as long as it associates your work with a given issue or reason for the changes. 11 | 12 | ## Testing 13 | This project uses [cypress.io](https://www.cypress.io/). 14 | 15 | ``` 16 | npm run test 17 | npm run test:ui 18 | ``` 19 | 20 | ## Resources 21 | - [Open Graph](https://ogp.me/) 22 | - [Twitter Card](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup) 23 | - [Metagen Docs](https://metagendocs.netlify.app/docs/intro) -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [tannerdolby] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Tanner Dolby @tannerdolby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-plugin-metagen 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/eleventy-plugin-metagen.svg?style=flat)](https://www.npmjs.org/package/eleventy-plugin-metagen) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/eleventy-plugin-metagen.svg?style=flat)](https://npmcharts.com/compare/eleventy-plugin-metagen?minimal=true) 5 | [![Install Size](https://packagephobia.now.sh/badge?p=eleventy-plugin-metagen)](https://packagephobia.com/result?p=eleventy-plugin-metagen) 6 | 7 | An Eleventy [shortcode](https://www.11ty.dev/docs/shortcodes/) that generates document metadata for the `` of a webpage supporting: Open Graph, Twitter card, generic meta tags, CSS, JS, canonical link, and custom tags. Metadata generated by [meta-generator](https://github.com/tannerdolby/meta-generator). Check out [metagen docs](https://metagendocs.netlify.app/docs/eleventy/intro) for more details on plugin usage. 8 | 9 | ## Installation 10 | Install the plugin from [npm](https://www.npmjs.com/package/eleventy-plugin-metagen): 11 | 12 | ``` 13 | npm install eleventy-plugin-metagen 14 | ``` 15 | 16 | Add it to your [Eleventy Config](https://www.11ty.dev/docs/config/) file: 17 | 18 | ```js 19 | import metagen from 'eleventy-plugin-metagen'; 20 | 21 | export default async function(eleventyConfig) { 22 | eleventyConfig.addPlugin(metagen); 23 | }; 24 | ``` 25 | 26 |
CommonJS Usage 27 | 28 | ```js 29 | const metagen = require('eleventy-plugin-metagen'); 30 | 31 | module.exports = (eleventyConfig) => { 32 | eleventyConfig.addPlugin(metagen); 33 | }; 34 | ``` 35 | 36 |
37 | 38 | 39 | 40 | ## What does it do? 41 | The plugin turns [11ty shortcodes](https://www.11ty.dev/docs/shortcodes/) in a Nunjucks template: 42 | 43 | ```njk 44 | {% metagen 45 | title='Eleventy Plugin Meta Generator', 46 | desc='An eleventy shortcode for generating meta tags.', 47 | url='https://tannerdolby.com', 48 | img='https://tannerdolby.com/images/arch-spiral-large.jpg', 49 | img_alt='Archimedean Spiral', 50 | twitter_card_type='summary_large_image', 51 | twitter_handle='tannerdolby', 52 | name='Tanner Dolby', 53 | generator='eleventy', 54 | comments=true, 55 | css=['style.css', 'design.css'], 56 | js=['foo.js', 'bar.js:async'], 57 | inline_css='h1 { color: #f06; }', 58 | inline_js='console.log("hello, world.");' 59 | %} 60 | ``` 61 | 62 | into `` tags and other document metadata: 63 | 64 | ```html 65 | 66 | 67 | 68 | Eleventy Plugin Meta Generator 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ``` 97 | 98 | ## Use Your Template Data 99 | To make your metadata dynamic, you can use template data as arguments to the shortcode without quotes or braces in a Nunjucks template. 100 | 101 | ```njk 102 | --- 103 | title: Some title 104 | desc: Some description 105 | metadata: 106 | title: Some other title 107 | desc: Some other description 108 | url: https://tannerdolby.com 109 | image: https://tannerdolby.com/images/arch-spiral-large.jpg 110 | alt: Archimedean spiral 111 | type: summary_large_image 112 | twitter: tannerdolby 113 | name: Tanner Dolby 114 | --- 115 | {% metagen 116 | title=title or metadata.title, 117 | desc=desc or metadata.desc, 118 | url=url + page.url, 119 | img=image, 120 | img_alt=alt, 121 | twitter_card_type=type, 122 | twitter_handle=twitter, 123 | name=name 124 | %} 125 | ``` 126 | 127 | Shorthand syntax: 128 | 129 | ```njk 130 | --- 131 | metadata: 132 | title: foo bar 133 | desc: some desc 134 | --- 135 | {% metagen metadata %} 136 | ``` 137 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewportHeight": 600, 3 | "viewportWidth": 800, 4 | "component": { 5 | "viewportHeight": 500, 6 | "viewportWidth": 500 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent metadata returned from eleventy-plugin-metagen", 3 | "body": "Testing generated metadata from eleventy-plugin-metagen" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/integration/metagen_spec.js: -------------------------------------------------------------------------------- 1 | describe('create document metadata', () => { 2 | before(() => { 3 | cy.visit('../../_site/tests/index.html'); 4 | }); 5 | 6 | it(' tag is generated and matches', () => { 7 | cy.title().should('eq', 'Eleventy Plugin Meta Generator'); 8 | }); 9 | 10 | it('generic meta tags are generated', () => { 11 | const map = { 12 | charset: 'utf-8', 13 | 'http-equiv': 'IE=edge', 14 | viewport: 'width=device-width, initial-scale=1', 15 | author: 'Tanner Dolby', 16 | title: 'Eleventy Plugin Meta Generator', 17 | description: 'An eleventy shortcode for generating meta tags.', 18 | generator: '11ty', 19 | }; 20 | cy.get('meta[charset]').should('have.attr', 'charset', 'utf-8'); 21 | cy.get('meta[http-equiv]').should('have.attr', 'http-equiv', 'X-UA-Compatible'); 22 | cy.get('meta[http-equiv]').should('have.attr', 'content', 'IE=edge'); 23 | cy.get(`meta[name='viewport']`).should('have.attr', 'content', map.viewport); 24 | cy.get('title').should('have.text', map.title); 25 | cy.get(`meta[name='author']`).should('have.attr', 'content', map.author); 26 | cy.get(`meta[name='description']`).should('have.attr', 'content', map.description); 27 | cy.get(`meta[name='generator']`).should('have.attr', 'content', map.generator); 28 | }); 29 | 30 | it('robots tag is generated', () => { 31 | cy.get(`meta[name='robots']`).should('have.attr', 'content', 'noindex'); 32 | }); 33 | 34 | it('preconnect and dns-prefetch tags are generated', () => { 35 | const map = { 36 | preconnect: [ 37 | { url: 'https://fonts.googleapis.com/', crossorigin: true }, 38 | 'https://google.com', 39 | ], 40 | 'dns-prefetch': ['https://fonts.googleapis.com/', 'https://google.com'], 41 | }; 42 | 43 | map['preconnect'].forEach((link, i) => { 44 | if (typeof link === 'string') { 45 | cy.get(`link[rel='preconnect']`).eq(i).should('have.attr', 'href', link); 46 | } else { 47 | cy.get(`link[rel='preconnect']`) 48 | .should('have.attr', 'href', link.url) 49 | .should('have.attr', 'crossorigin'); 50 | } 51 | }); 52 | 53 | map['dns-prefetch'].forEach((link, i) => { 54 | cy.get(`link[rel='dns-prefetch']`).eq(i).should('have.attr', 'href', link); 55 | }); 56 | }); 57 | 58 | it('custom crawler tags are generated', () => { 59 | const map = { 60 | googlebot: 'noindex', 61 | 'googlebot-news': 'nosnippet', 62 | }; 63 | for (const prop in map) { 64 | cy.get(`meta[name='${prop}']`).should('have.attr', 'content', `${map[prop]}`); 65 | } 66 | }); 67 | 68 | it('twitter meta tags are generated', () => { 69 | const map = { 70 | card: 'summary_large_image', 71 | site: '@tannerdolby', 72 | creator: '@tannerdolby', 73 | url: 'https://tannerdolby.com', 74 | title: 'Eleventy Plugin Meta Generator', 75 | description: 'An eleventy shortcode for generating meta tags.', 76 | image: 'https://tannerdolby.com/images/arch-spiral-large.jpg', 77 | 'image:alt': 'Archimedean Spiral', 78 | }; 79 | for (const prop in map) { 80 | cy.get(`meta[name='twitter:${prop}']`).each((el) => { 81 | cy.get(el).should('have.attr', 'content', map[prop]); 82 | }); 83 | } 84 | }); 85 | 86 | it('open graph meta tags are generated', () => { 87 | const map = { 88 | type: 'website', 89 | url: 'https://tannerdolby.com', 90 | locale: 'en_US', 91 | title: 'Eleventy Plugin Meta Generator', 92 | description: 'An eleventy shortcode for generating meta tags.', 93 | image: 'https://tannerdolby.com/images/arch-spiral-large.jpg', 94 | 'image:alt': 'Archimedean Spiral', 95 | }; 96 | for (const prop in map) { 97 | cy.get(`meta[property='og:${prop}']`).each((el) => { 98 | cy.get(el).should('have.attr', 'content', map[prop]); 99 | }); 100 | } 101 | }); 102 | 103 | it('canonical link generated and matches', () => { 104 | cy.get(`link[rel='canonical']`).should( 105 | 'have.attr', 106 | 'href', 107 | 'https://tannerdolby.com', 108 | ); 109 | }); 110 | 111 | it('alternate og:locales generated if specified', () => { 112 | const alternateLocales = ['es', 'zh', 'ja']; 113 | alternateLocales.forEach((locale, i) => { 114 | cy.get(`meta[property='og:locale:alternate']`) 115 | .eq(i) 116 | .should('have.attr', 'content', locale); 117 | }); 118 | }); 119 | 120 | it('custom user-defined tags generated', () => { 121 | cy.get(`meta[name='foobar']`).should('have.attr', 'content', 'fizz'); 122 | cy.get(`link[href='print.css']`) 123 | .should('have.attr', 'rel', 'stylesheet') 124 | .should('have.attr', 'media', 'print'); 125 | cy.get(`link[href='myFont.woff2']`) 126 | .should('have.attr', 'rel', 'preload') 127 | .should('have.attr', 'as', 'font') 128 | .should('have.attr', 'type', 'font/woff2') 129 | .should('have.attr', 'crossorigin', 'anonymous'); 130 | }); 131 | 132 | it('css stylesheets generated', () => { 133 | cy.get(`link[href='style.css']`).should('have.attr', 'href', 'style.css'); 134 | cy.get(`link[rel='preload'][href='foo.css']`) 135 | .should('have.attr', 'href', 'foo.css') 136 | .should('have.attr', 'as', 'style'); 137 | }); 138 | 139 | it('inline-css generated', () => { 140 | cy.get('style').should('have.text', 'h1 {color: #f06}'); 141 | }); 142 | 143 | it('js scripts generated', () => { 144 | const scripts = ['foo.js', 'bar.js:async:type="module"']; 145 | scripts.forEach((script, i) => { 146 | const [src, async] = script.split(':'); 147 | cy.get('script[src]').eq(i).should('have.attr', 'src', src); 148 | if (script.includes(':async')) { 149 | cy.get('script[async]') 150 | .should('have.attr', 'async', async) 151 | .should('have.attr', 'type', 'module'); 152 | } 153 | }); 154 | }); 155 | 156 | it('inline-js generated', () => { 157 | cy.get('script:not([src])').eq(1).should('have.text', `console.log("hello, world");`); 158 | cy.get('script[type="application/json"]') 159 | .should('have.attr', 'id', 'some-id') 160 | .should('have.text', '{"data": "hello"}'); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// <reference types="cypress" /> 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /cypress/videos/metagen_spec.js.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerdolby/eleventy-plugin-metagen/042ff9269028f78508fc5791dc9b04a8deeef5b0/cypress/videos/metagen_spec.js.mp4 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-metagen", 3 | "version": "1.8.3", 4 | "description": "An Eleventy shortcode that generates document metadata containing: Open Graph, Twitter card, generic meta tags and a canonical link.", 5 | "main": ".eleventy.js", 6 | "publishConfig": { 7 | "registry": "https://registry.npmjs.org/" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/tannerdolby/eleventy-plugin-metagen.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/tannerdolby/eleventy-plugin-metagen/issues/" 15 | }, 16 | "homepage": "https://github.com/tannerdolby/eleventy-plugin-metagen#readme", 17 | "scripts": { 18 | "build": "eleventy", 19 | "dev": "eleventy --serve", 20 | "test": "npm run build && cypress run", 21 | "test:ui": "npm run build && cypress open" 22 | }, 23 | "keywords": [ 24 | "eleventy", 25 | "eleventy-plugin", 26 | "11ty", 27 | "metatag", 28 | "metadata", 29 | "meta-tag-generator", 30 | "social-share" 31 | ], 32 | "author": { 33 | "name": "Tanner Dolby", 34 | "url": "https://tannerdolby.com" 35 | }, 36 | "license": "MIT", 37 | "dependencies": { 38 | "@11ty/eleventy": "^2.0.1", 39 | "meta-generator": "^0.1.5" 40 | }, 41 | "devDependencies": { 42 | "cypress": "^8.6.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | title: Eleventy Plugin Meta Generator 4 | desc: An eleventy shortcode for generating meta tags. 5 | url: https://tannerdolby.com 6 | img: https://tannerdolby.com/images/arch-spiral-large.jpg 7 | img_alt: Archimedean Spiral 8 | twitter_card_type: summary_large_image 9 | twitter_handle: tannerdolby 10 | name: Tanner Dolby 11 | generator: 11ty 12 | comments: true 13 | site_name: Metagen 14 | robots: noindex 15 | minified: false 16 | crawlers: 17 | googlebot: noindex 18 | googlebot-news: nosnippet 19 | preconnect: [{url: https://fonts.googleapis.com/, crossorigin: true}, https://google.com] 20 | dns_prefetch: [https://fonts.googleapis.com/, https://google.com] 21 | og_alternate_locales: [es, zh, ja] 22 | css: [style.css, foo.css:rel="preload":as="style"] 23 | js: [foo.js, bar.js:async:type="module", fizz.js:defer] 24 | inline_css: 'h1 {color: #f06}' 25 | inline_js: [ 26 | 'console.log("hello, world");', 27 | {type: 'application/json', id: 'some-id', js: '{"data": "hello"}'} 28 | ] 29 | custom: [ 30 | {tag: 'meta', attrs: {name: 'foobar', content: 'fizz'}}, 31 | {tag: 'link', attrs: {rel: 'stylesheet', href: 'print.css', media: 'print'}}, 32 | {tag: 'link', attrs: {rel: 'preload', href: 'myFont.woff2', as: 'font', type: 'font/woff2', crossorigin: 'anonymous'}} 33 | ] 34 | --- 35 | <!DOCTYPE html> 36 | <html lang="en"> 37 | <head> 38 | {% metagen metadata %} 39 | </head> 40 | <body> 41 | <h1>Test Fixture</h1> 42 | <p>Inspect the page with DevTools and checkout the <code><head></code></p> 43 | </body> 44 | </html> --------------------------------------------------------------------------------