├── .eleventy.js ├── .gitignore ├── .npmignore ├── README.md ├── demo ├── .eleventy.js ├── _includes │ └── base.njk ├── author.njk ├── index.njk ├── package.json ├── page.njk ├── post.njk └── tag.njk └── package.json /.eleventy.js: -------------------------------------------------------------------------------- 1 | const EleventyFetch = require("@11ty/eleventy-fetch"); 2 | const jwt = require("jsonwebtoken"); 3 | 4 | // Generate JWT 5 | const generateJwt = ({ adminKey, version = "v4" }) => { 6 | const [id, secret] = adminKey.split(":"); 7 | 8 | return jwt.sign({}, Buffer.from(secret, "hex"), { 9 | keyid: id, 10 | algorithm: "HS256", 11 | expiresIn: "5m", 12 | audience: `/${version}/admin/`, 13 | }); 14 | }; 15 | 16 | // Get all post data 17 | const getPosts = async ({ url, key, adminKey, version = "v4" }) => { 18 | const data = await EleventyFetch( 19 | `${url}/ghost/api/${version}/${ 20 | adminKey ? "admin" : "content" 21 | }/posts/?key=${key}&limit=all&include=tags,authors&formats=html`, 22 | { 23 | duration: "1m", 24 | type: "json", 25 | ...(adminKey && { 26 | fetchOptions: { 27 | headers: { 28 | Authorization: `Ghost ${generateJwt({ adminKey, version })}`, 29 | }, 30 | }, 31 | }), 32 | } 33 | ); 34 | 35 | const updatedPosts = await data.posts.map((post) => { 36 | // Filter tags to just show public tags 37 | // Reassign internal tags to an internalTags key 38 | const publicTags = post.tags.filter((tag) => tag.visibility !== "internal"); 39 | const internalTags = post.tags.filter( 40 | (tag) => tag.visibility === "internal" 41 | ); 42 | 43 | return { 44 | ...post, 45 | tags: publicTags, 46 | internalTags: internalTags, 47 | }; 48 | }); 49 | 50 | return updatedPosts; 51 | }; 52 | 53 | // Get all page data 54 | const getPages = async ({ url, key, adminKey, version = "v4" }) => { 55 | const data = await EleventyFetch( 56 | `${url}/ghost/api/${version}/${ 57 | adminKey ? "admin" : "content" 58 | }/pages/?key=${key}&limit=all&include=tags,authors&formats=html`, 59 | { 60 | duration: "1m", 61 | type: "json", 62 | ...(adminKey && { 63 | fetchOptions: { 64 | headers: { 65 | Authorization: `Ghost ${generateJwt({ adminKey, version })}`, 66 | }, 67 | }, 68 | }), 69 | } 70 | ); 71 | 72 | const updatedPages = await data.pages.map((page) => { 73 | // Filter tags to just show public tags 74 | // Reassign internal tags to an internalTags key 75 | const publicTags = page.tags.filter((tag) => tag.visibility !== "internal"); 76 | const internalTags = page.tags.filter( 77 | (tag) => tag.visibility === "internal" 78 | ); 79 | 80 | return { 81 | ...page, 82 | tags: publicTags, 83 | internalTags: internalTags, 84 | }; 85 | }); 86 | 87 | return updatedPages; 88 | }; 89 | 90 | // Get all tag data 91 | const getTags = async ({ url, key, adminKey, version = "v4" }) => { 92 | const data = await EleventyFetch( 93 | `${url}/ghost/api/${version}/${ 94 | adminKey ? "admin" : "content" 95 | }/tags/?key=${key}&limit=all&include=count.posts&filter=visibility:public`, 96 | { 97 | duration: "1m", 98 | type: "json", 99 | ...(adminKey && { 100 | fetchOptions: { 101 | headers: { 102 | Authorization: `Ghost ${generateJwt({ adminKey, version })}`, 103 | }, 104 | }, 105 | }), 106 | } 107 | ); 108 | 109 | return data.tags; 110 | }; 111 | 112 | // Get all author data 113 | const getAuthors = async ({ url, key, adminKey, version = "v4" }) => { 114 | const data = await EleventyFetch( 115 | `${url}/ghost/api/${version}/${adminKey ? "admin" : "content"}/${ 116 | adminKey ? "users" : "authors" 117 | }/?key=${key}&limit=all&include=count.posts`, 118 | { 119 | duration: "1m", 120 | type: "json", 121 | ...(adminKey && { 122 | fetchOptions: { 123 | headers: { 124 | Authorization: `Ghost ${generateJwt({ adminKey, version })}`, 125 | }, 126 | }, 127 | }), 128 | } 129 | ); 130 | 131 | return adminKey ? data.users : data.authors; 132 | }; 133 | 134 | // Get all settings data 135 | const getSettings = async ({ url, key, version = "v4" }) => { 136 | const data = await EleventyFetch( 137 | `${url}/ghost/api/${version}/content/settings/?key=${key}`, 138 | { 139 | duration: "1m", 140 | type: "json", 141 | } 142 | ); 143 | 144 | return data.settings; 145 | }; 146 | 147 | const getContent = async (params) => { 148 | return { 149 | posts: await getPosts(params), 150 | pages: await getPages(params), 151 | tags: await getTags(params), 152 | authors: await getAuthors(params), 153 | settings: await getSettings(params), 154 | }; 155 | }; 156 | 157 | module.exports = (eleventyConfig, options) => { 158 | eleventyConfig.addGlobalData("ghost", async () => { 159 | if (!options.url || !options.key) { 160 | console.log("Invalid Ghost API key or URL"); 161 | } 162 | 163 | return await getContent(options); 164 | }); 165 | 166 | eleventyConfig.addFilter("filterPosts", (posts, key, value) => { 167 | // Check for exclamation before the second parameter… 168 | if (value.startsWith("!")) { 169 | // Snip off that exclamation 170 | const unprefixedValue = value.substring(1); 171 | 172 | // Filter posts that don't include this value 173 | return posts.filter((post) => 174 | post[key].every((item) => item.slug !== unprefixedValue) 175 | ); 176 | } 177 | 178 | // Filter posts that have the value 179 | return posts.filter((post) => 180 | post[key].some((item) => item.slug === value) 181 | ); 182 | }); 183 | }; 184 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | 4 | demo/.env 5 | demo/node_modules 6 | demo/_site 7 | demo/package-lock.json 8 | demo/.cache 9 | 10 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-plugin-ghost 2 | 3 | [![npm](https://img.shields.io/npm/v/eleventy-plugin-ghost)](https://www.npmjs.com/package/eleventy-plugin-ghost) 4 | 5 | Import your [Ghost](https://ghost.org) content directly into [Eleventy](https://github.com/11ty/eleventy) as global data. Compatible with Eleventy v1.0.0 and above. 6 | 7 | [Check out the live demo](https://eleventy-plugin-ghost.netlify.app) and the [demo directory in the repo](https://github.com/daviddarnes/eleventy-plugin-ghost/tree/main/demo) to see it all in action. 8 | 9 | ## Installation 10 | 11 | 1. Install the plugin using npm: 12 | 13 | ``` 14 | npm install eleventy-plugin-ghost 15 | ``` 16 | 17 | 2. Add the plugin to your `.eleventy.js` config, ensuring to add your Ghost URL, Content API key and optionally your Admin API key. Check out the Ghost docs for [how to create a Content](http://www.ghost.org/docs/content-api/) or [Admin API key](https://ghost.org/docs/admin-api/): 18 | 19 | ```js 20 | const pluginGhost = require("eleventy-plugin-ghost"); 21 | 22 | require("dotenv").config(); 23 | const { 24 | GHOST_URL, 25 | // GHOST_ADMIN_KEY, 26 | GHOST_KEY, 27 | } = process.env; 28 | 29 | module.exports = (eleventyConfig) => { 30 | eleventyConfig.addPlugin(pluginGhost, { 31 | url: GHOST_URL, 32 | key: GHOST_KEY, 33 | // apiKey: GHOST_ADMIN_KEY, 34 | version: "v4", // "v4" is the default 35 | }); 36 | }; 37 | ``` 38 | 39 | The example above is using `dotenv` with a `.env` file to ensure credentials are **not** stored in the source code. Here's an example of the `.env` file: 40 | 41 | ```text 42 | GHOST_URL=https://demo.ghost.io 43 | GHOST_KEY=22444f78447824223cefc48062 44 | ``` 45 | 46 | 3. Run or build your Eleventy project and use the global `ghost` data variable to access `posts`, `pages`, `tags`, `authors` and `settings`. 47 | 48 | ## Usage 49 | 50 | The API will default to the latest stable version, which is `v4`. However passing `version` into the plugin options will set the version returned, as shown in the above code sample. 51 | 52 | After installing and running you'll be provided with a global `ghost` key as well as a `filtersPosts()` function. See [API](#API) and [filtering posts](#Filtering-posts) sections respectively for more information. 53 | 54 | ## API 55 | 56 | - `ghost.posts`: An array of all posts in Ghost, including their tags and authors. Note that the `posts` array will include draft posts if you use the Admin API 57 | - `ghost.pages`: An array of all pages in Ghost. Note that the `pages` array will include draft pages if you use the Admin API 58 | - `ghost.tags`: An array of all tags in Ghost, including the number of posts within each tag but filtered to only contain public tags 59 | - `ghost.authors`: An array of all authors in Ghost, including the number of posts within each author. Note that using the Admin API will cause `authors` to actually return `users` which comes with additional data 60 | - `ghost.settings`: All settings set in Ghost 61 | 62 | All data is cached using [`@11ty/eleventy-fetch`](https://www.11ty.dev/docs/plugins/fetch/) with a duration of 1 minute. This keeps the local builds fast while still inheriting newly applied content. 63 | 64 | ## Admin API 65 | 66 | Passing in an Admin API key will cause the plugin to use the Ghost Admin API. This means all global data objects except for `settings` will return additional data. For example `posts` and `pages` will include draft posts and pages. For more information on what additional data is exposed check out the [official Ghost docs](https://ghost.org/docs/admin-api/#posts). 67 | 68 | If you're looking to get additional data from the Admin API, such as `tiers`, `offers` and `members` then feel free to follow the [development guide](#development) guide down below and submit a pull request. 69 | 70 | ## Internal tags 71 | 72 | When posts are retrieved all tags are applied, including [internal tags](https://ghost.org/docs/publishing/#internal-tag). Internal tags are very useful for grouping posts without exposing the tag in the front-end. To assist with this the plugin filters out internal tags from the `tags` key on posts and applies them to a new `internalTags` key. Internal tag slugs are prefixed with `hash-` to mirror the `#` applied in the UI to define them. 73 | 74 | ## Filtering posts 75 | 76 | The plugin comes with a custom filter called `filterPosts`, this can be used to filter posts by attributes such as `authors`, `tags` and `internalTags` using the attributes slug. The following example will list posts that are tagged with "portfolio": 77 | 78 | ```nunjucks 79 | {% for post in ghost.posts | filterPosts("tags", "portfolio") %} 80 |
  • {{ post.title }}
  • 81 | {% endfor %} 82 | ``` 83 | 84 | It's also possible to filter _out_ posts with a certain tag by prefixing the parameter with `!`: 85 | 86 | ```nunjucks 87 | {% for post in ghost.posts | filterPosts("tags", "!blog") %} 88 |
  • {{ post.title }}
  • 89 | {% endfor %} 90 | ``` 91 | 92 | The filter works for `authors` as well as `internalTags`: 93 | 94 | ```nunjucks 95 | {% for post in ghost.posts | filterPosts("internalTags", "!hash-internal-tag") %} 96 |
  • {{ post.title }}
  • 97 | {% endfor %} 98 | ``` 99 | 100 | ```nunjucks 101 | {% for post in ghost.posts | filterPosts("authors", "david") %} 102 |
  • {{ post.title }}
  • 103 | {% endfor %} 104 | ``` 105 | 106 | ## Creating pages 107 | 108 | Rendering pages for posts, pages, authors and tags can be done by making use of the [eleventy pagination](https://www.11ty.dev/docs/pagination/) feature. In the following example post pages are being created in a `post.njk` file: 109 | 110 | ```nunjucks 111 | --- 112 | pagination: 113 | data: ghost.posts 114 | size: 1 115 | alias: post 116 | permalink: "/{{ post.slug }}/" 117 | --- 118 | 119 |

    {{ post.title }}

    120 | {{ post.html | safe }} 121 | ``` 122 | 123 | The same can be done for authors and tags in combination with the `filterPosts` function to list out posts by that author or tagged with that tag: 124 | 125 | ```nunjucks 126 | --- 127 | pagination: 128 | data: ghost.tags 129 | size: 1 130 | alias: tag 131 | permalink: "/{{ tag.slug }}/" 132 | --- 133 | 134 |

    {{ tag.name }}

    135 | 140 | ``` 141 | 142 | A more advanced use case is if you want to render post pages but filter _out_ posts with a certain attribute. The below example makes use of [`gray-matter` in eleventy](https://www.11ty.dev/docs/data-frontmatter/#alternative-front-matter-formats) and checks if a post has an internal tag with the slug `hash-internal-tag` and prevents the post from rendering: 143 | 144 | ```nunjucks 145 | ---js 146 | { 147 | pagination: { 148 | data: "ghost.posts", 149 | size: 1, 150 | alias: "post", 151 | before: function(data) { 152 | return data.filter(post => post.internalTags.every(tag => tag.slug !== "hash-internal-tag")); 153 | } 154 | }, 155 | permalink: "/{{ post.slug }}/" 156 | } 157 | --- 158 | 159 |

    {{ post.title }}

    160 | {{ post.html | safe }} 161 | ``` 162 | 163 | Check out the demo directory of this project for more extensive examples. 164 | 165 | ## Development 166 | 167 | 1. Create a `.env` file inside of `demo` with the following credentials: 168 | 169 | ```text 170 | GHOST_URL=https://demo.ghost.io 171 | GHOST_KEY=22444f78447824223cefc48062 172 | ``` 173 | 174 | 2. Amend the `.eleventy.js` file within `demo` so it points to the source code in the parent directory: 175 | 176 | ```js 177 | // const pluginGhost = require("../"); 178 | const pluginGhost = require("eleventy-plugin-ghost"); 179 | ``` 180 | 181 | 3. Install the demo dependencies: 182 | 183 | ```text 184 | cd demo 185 | npm install 186 | ``` 187 | 188 | 4. Run the demo locally: 189 | ```text 190 | npm run dev 191 | ``` 192 | -------------------------------------------------------------------------------- /demo/.eleventy.js: -------------------------------------------------------------------------------- 1 | // const pluginGhost = require("../"); // For local development 2 | 3 | const pluginGhost = require("eleventy-plugin-ghost"); 4 | 5 | require("dotenv").config(); 6 | const { GHOST_URL, GHOST_KEY } = process.env; 7 | 8 | module.exports = (eleventyConfig) => { 9 | eleventyConfig.addPlugin(pluginGhost, { 10 | url: GHOST_URL, 11 | key: GHOST_KEY, 12 | version: "v4", 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /demo/_includes/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{{ ghost.settings.title }}{% endblock %} 7 | 8 | 9 | 10 | {% block main %}{% endblock %} 11 | 12 | -------------------------------------------------------------------------------- /demo/author.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: ghost.authors 4 | size: 1 5 | alias: author 6 | permalink: "/{{ author.slug }}/" 7 | --- 8 | 9 | {% extends "base.njk" %} 10 | 11 | {% block title %}{{ author.name }}{% endblock %} 12 | 13 | {% block main %} 14 |

    {{ author.name }}

    15 | 16 |

    {{ author.bio }}

    17 | 18 | 23 | {% endblock %} -------------------------------------------------------------------------------- /demo/index.njk: -------------------------------------------------------------------------------- 1 | {% extends "base.njk" %} 2 | 3 | {% block main %} 4 |

    {{ ghost.settings.title }}

    5 | 6 |

    {{ ghost.settings.description }}

    7 | 8 |

    Posts

    9 | 14 | 15 |

    Pages

    16 | 21 | 22 |

    Tags

    23 | 28 | 29 |

    Authors

    30 | 35 | 36 |

    Example internally tagged posts

    37 | 42 | 43 |

    Example internally tagged pages

    44 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "npx @11ty/eleventy --serve", 4 | "build": "npx @11ty/eleventy" 5 | }, 6 | "dependencies": { 7 | "@11ty/eleventy": "^1.0.1", 8 | "dotenv": "^8.2.0", 9 | "eleventy-plugin-ghost": "^2.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: ghost.pages 4 | size: 1 5 | alias: doc 6 | permalink: "/{{ doc.slug }}/" 7 | --- 8 | 9 | {% extends "base.njk" %} 10 | 11 | {% block title %}{{ doc.title }}{% endblock %} 12 | 13 | {% block main %} 14 |

    {{ doc.title }}

    15 | {{ doc.html | safe }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /demo/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: ghost.posts 4 | size: 1 5 | alias: post 6 | permalink: "/{{ post.slug }}/" 7 | --- 8 | 9 | {% extends "base.njk" %} 10 | 11 | {% block title %}{{ post.title }}{% endblock %} 12 | 13 | {% block main %} 14 |

    {{ post.title }}

    15 | {{ post.html | safe }} 16 | {% endblock %} -------------------------------------------------------------------------------- /demo/tag.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: ghost.tags 4 | size: 1 5 | alias: tag 6 | permalink: "/{{ tag.slug }}/" 7 | --- 8 | 9 | {% extends "base.njk" %} 10 | 11 | {% block title %}{{ tag.name }}{% endblock %} 12 | 13 | {% block main %} 14 |

    {{ tag.name }}

    15 | 16 |

    {{ tag.description }}

    17 | 18 | 23 | {% endblock %} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-ghost", 3 | "version": "2.1.0", 4 | "description": "Access the Ghost Content API in Eleventy 👻🎛", 5 | "homepage": "https://github.com/daviddarnes/eleventy-plugin-ghost", 6 | "main": ".eleventy.js", 7 | "scripts": { 8 | "build": "npx @11ty/eleventy", 9 | "publish": "git push origin && git push origin --tags", 10 | "release:patch": "npm version patch && npm publish", 11 | "release:minor": "npm version minor && npm publish", 12 | "release:major": "npm version major && npm publish" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/daviddarnes/eleventy-plugin-ghost.git" 17 | }, 18 | "keywords": [ 19 | "11ty", 20 | "11ty-plugin", 21 | "eleventy", 22 | "eleventy-plugin", 23 | "ghost", 24 | "ghost-api", 25 | "headless-cms" 26 | ], 27 | "author": { 28 | "name": "daviddarnes", 29 | "url": "https://darn.es" 30 | }, 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/daviddarnes/eleventy-plugin-ghost/issues" 34 | }, 35 | "dependencies": { 36 | "@11ty/eleventy": "^1.0.1", 37 | "@11ty/eleventy-fetch": "^3.0.0", 38 | "jsonwebtoken": "^8.5.1" 39 | } 40 | } 41 | --------------------------------------------------------------------------------