├── .eleventyignore ├── .gitignore ├── demo ├── posts │ ├── posts.json │ ├── blog-d.md │ ├── blog-e.md │ ├── blog-f.md │ ├── blog-g.md │ ├── blog-b.md │ ├── blog-c.md │ └── blog-a.md ├── articles │ ├── articles.json │ ├── blog-a.md │ └── blog-b.md ├── _includes │ └── base.html ├── assets │ └── style.css ├── .eleventy.js ├── index.html ├── category.njk └── articleCategories.njk ├── src └── getCategoryKeys.js ├── package.json ├── LICENSE ├── .eleventy.js └── README.md /.eleventyignore: -------------------------------------------------------------------------------- 1 | demo 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _site -------------------------------------------------------------------------------- /demo/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": ["posts"] 3 | } -------------------------------------------------------------------------------- /demo/articles/articles.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": ["articles"] 3 | } -------------------------------------------------------------------------------- /demo/articles/blog-a.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Article 1 3 | --- 4 | 5 | Hello world -------------------------------------------------------------------------------- /demo/articles/blog-b.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Article B 3 | articleCategories: ['dev'] 4 | --- 5 | 6 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-d.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fourth post 3 | categories: ['dev', '11ty'] 4 | date: 2021-01-04 5 | --- 6 | 7 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-e.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fifth post 3 | categories: ['dev', 'astro'] 4 | date: 2021-01-05 5 | --- 6 | 7 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-f.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sixth blog 3 | categories: ['dev'] 4 | date: 2021-01-06 5 | --- 6 | 7 | Hello world 8 | 9 | -------------------------------------------------------------------------------- /demo/posts/blog-g.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Seventh post 3 | categories: ['dev', 'astro'] 4 | date: 2021-01-07 5 | --- 6 | 7 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-b.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Second Blog post 3 | categories: ['travel', 'photography'] 4 | date: 2021-01-02 5 | 6 | --- 7 | 8 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-c.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Third blog post 3 | categories: ['dev', 'javascript', '11ty'] 4 | date: 2021-01-03 5 | 6 | --- 7 | 8 | Hello world -------------------------------------------------------------------------------- /demo/posts/blog-a.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Blog post number 1 3 | categories: ['dev', 'javascript', 'html', 'multi word category'] 4 | date: 2021-01-01 5 | --- 6 | 7 | Hello world -------------------------------------------------------------------------------- /src/getCategoryKeys.js: -------------------------------------------------------------------------------- 1 | module.exports = function(posts, options={}) { 2 | const tagSet = new Set(posts.flatMap((post) => post.data[options.categoryVar] || [])); 3 | return [...tagSet] 4 | } -------------------------------------------------------------------------------- /demo/_includes/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | {{ title }} 12 | 13 | {{ content }} 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-dynamic-categories", 3 | "version": "0.1.5", 4 | "description": "", 5 | "main": ".eleventy.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@11ty/eleventy": "^1.0.2", 14 | "lodash": "^4.17.21", 15 | "slugify": "^1.6.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/assets/style.css: -------------------------------------------------------------------------------- 1 | nav { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .pagination { 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | display: flex; 11 | } 12 | 13 | .pagination-page { 14 | margin: 0 1px; 15 | padding: .5ch 1ch; 16 | border: 1px solid #333; 17 | } 18 | .pagination-page.currentPage { 19 | background: #333; 20 | color: white; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /demo/.eleventy.js: -------------------------------------------------------------------------------- 1 | const categoryPlugin = require('../.eleventy.js') 2 | 3 | module.exports = function(eleventyConfig) { 4 | eleventyConfig.addPlugin(categoryPlugin, { 5 | categoryVar: "categories", 6 | itemsCollection: "posts", 7 | perPageCount: 4 8 | }) 9 | eleventyConfig.addPlugin(categoryPlugin, { 10 | categoryVar: "articleCategories", 11 | itemsCollection: "articles", 12 | perPageCount: 2 13 | }) 14 | eleventyConfig.addPassthroughCopy('assets') 15 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "base.html" 3 | --- 4 | 5 | 6 |

Categories

7 | {% for category in collections.categories %} 8 |
9 |

{{ category.title }}

10 | 15 |
16 | 17 | {% endfor %} 18 | 19 | 20 |

Testing second implementation

21 | {% for post in collections.articles %} 22 |
  • {{post.data.title}}
  • 23 | {% endfor %} 24 |

    Categories

    25 | {% for category in collections.articleCategories %} 26 |
    27 |

    {{ category.title }}

    28 | 33 |
    34 | 35 | {% endfor %} -------------------------------------------------------------------------------- /demo/category.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.html 3 | # Default permalink scheme (still able to be customized) 4 | permalink: /posts/{{category.permalinkScheme}} 5 | pagination: 6 | data: collections.categoriesByPage 7 | size: 1 8 | alias: category 9 | addAllPagesToCollections: true 10 | eleventyComputed: 11 | title: Blog entries with category "{{ category.slug }}" {% if tcategoryag.pageNumber > 0 %}, (Page {{ category.pageNumber + 1 }}) {% endif %} 12 | --- 13 | 14 | 15 |
  • Slug: {{ category.slug }}
  • 16 |
  • category title: {{ category.title }}
  • 17 |
  • posts {{ category.posts }}
  • 18 |
  • page number: {{ category.pages.current }}
  • 19 |
  • total pages: {{ category.totalPages }}
  • 20 | 21 |

    {{ title }}

    22 | {% for post in category.posts %} 23 |
  • 24 | {{ post.data.title }} 25 |
  • 26 | {% endfor %} 27 | 28 | 29 | {# This can still be customized, but then there's markup for basic pagination #} 30 | {% pagination category %} 31 | -------------------------------------------------------------------------------- /demo/articleCategories.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.html 3 | # Default permalink scheme (still able to be customized) 4 | permalink: /articles/{{category.permalinkScheme}} 5 | pagination: 6 | data: collections.articleCategoriesByPage 7 | size: 1 8 | alias: category 9 | addAllPagesToCollections: true 10 | eleventyComputed: 11 | title: Blog entries with category "{{ category.slug }}" {% if category.pageNumber > 0 %}, (Page {{ category.pageNumber + 1 }}) {% endif %} 12 | --- 13 | 14 | 15 |
  • Slug: {{ category.slug }}
  • 16 |
  • category title: {{ category.title }}
  • 17 |
  • posts {{ category.posts }}
  • 18 |
  • page number: {{ category.pages.current }}
  • 19 |
  • total pages: {{ category.totalPages }}
  • 20 | 21 |

    {{ title }}

    22 | {% for post in category.posts %} 23 |
  • 24 | {{ post.data.title }} 25 |
  • 26 | {% endfor %} 27 | 28 | 29 | {# This can still be customized, but then there's markup for basic pagination #} 30 | {% pagination category %} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bryan Robinson 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 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const slugify = require('slugify'); 2 | let _ = require("lodash"); 3 | const getCategoryKeys = require('./src/getCategoryKeys.js') 4 | 5 | /* 6 | List all options from this plugin 7 | categoryVar 8 | categoryCollection 9 | perPageCount 10 | itemsCollection 11 | 12 | */ 13 | 14 | module.exports = function(eleventyConfig, options={ 15 | categoryVar: "categories", 16 | itemCollection: "posts", 17 | }) { 18 | const categoryCollection = options.categoryCollection || options.categoryVar; 19 | const perPageCount = options.perPageCount || 5 20 | 21 | // Creates the collection 22 | eleventyConfig.addCollection(categoryCollection, function(collections) { 23 | 24 | const posts = collections.getFilteredByTag(options.itemsCollection) 25 | let tagArray = getCategoryKeys(posts, options); 26 | 27 | const categoriesWithPosts = tagArray.map(category => { 28 | let filteredPosts = posts.filter(post => { 29 | if (!post.data[categoryCollection]) return false 30 | return post.data[categoryCollection].includes(category)} 31 | ).flat(); 32 | console.log(slugify(category)) 33 | return { 34 | 'title': category, 35 | 'slug': slugify(category), 36 | 'posts': [ ...filteredPosts ], 37 | }; 38 | }) 39 | console.log(`\x1b[32m[Dynamic Categories] Created Collection ${categoryCollection} with ${categoriesWithPosts.length} items`, '\x1b[0m') 40 | return categoriesWithPosts; 41 | }) 42 | 43 | eleventyConfig.addCollection(`${categoryCollection}ByPage`, function(collection) { 44 | // Get unique list of all tags currently in use 45 | const posts = collection.getFilteredByTag(options.itemsCollection) 46 | 47 | // Get each item that matches the tag and add it to the tag's array, chunked by paginationSize 48 | let paginationSize = perPageCount; 49 | let tagMap = []; 50 | let tagArray = getCategoryKeys(posts, options); 51 | 52 | for(let tagName of tagArray) { 53 | const filteredPosts = posts.filter(post => { 54 | if (!post.data[categoryCollection]) return false 55 | return post.data[categoryCollection].includes(tagName)} 56 | ).flat(); 57 | 58 | let tagItems = filteredPosts.reverse(); 59 | let pagedItems = _.chunk(tagItems, paginationSize); 60 | const totalPages = Math.ceil(filteredPosts.length / perPageCount) 61 | for( let pageNumber = 0, max = pagedItems.length; pageNumber < max; pageNumber++) { 62 | const currentNumber = pageNumber + 1 63 | const slug = slugify(tagName) 64 | tagMap.push({ 65 | slug, 66 | title: tagName, 67 | totalPages, 68 | posts: pagedItems[pageNumber], 69 | permalinkScheme: `${slug}${currentNumber > 1 ? `/${currentNumber}` : ''}/index.html`, 70 | pages: { 71 | current: currentNumber, 72 | next: currentNumber != totalPages && currentNumber + 1, 73 | previous: currentNumber > 1 && currentNumber - 1 74 | } 75 | }); 76 | } 77 | } 78 | // Return a two-dimensional array of items, chunked by paginationSize 79 | return tagMap; 80 | }); 81 | 82 | eleventyConfig.addShortcode('pagination', function(page) { 83 | const {pages} = page 84 | const {current, next, previous} = pages 85 | const allNumbers = Array.from(Array(page.totalPages).keys()) 86 | 87 | const pageList = allNumbers.map(number => { 88 | const pageNumber = number+1; 89 | const urlBase = current == 1 ? './' : '../' 90 | const url = pageNumber === 1 ? `${urlBase}` : `${urlBase}${pageNumber}` 91 | const isCurrent = pageNumber === current; 92 | 93 | return ` 94 | 95 | ${isCurrent ? pageNumber : `${pageNumber}`} 96 | 97 | `}).join('') 98 | 99 | const nextHref = next ? `Next Page`: 'Next Page' 100 | const previousHref = previous ? `Previous Page`: 'Previous Page' 101 | const markup = `` 102 | return markup 103 | }) 104 | 105 | 106 | 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eleventy Dynamic Categories 2 | 3 | This plugin is super alpha! 4 | 5 | This plugin will accept a category name and a collection name and create data that can be used to create or display category lists for content. 6 | 7 | It creates two collections. One is named either the `categoryVar` or `categoryCollection` configuration string. This has all posts in the category. The other is named that same string with `ByPage` appended to create a category collection that is paginated. 8 | 9 | Example: 10 | 11 | When you initialize with a `categoryVar` or `categoryCollection` of `categories`, the plugin will create two collections: `categories` and `categoriesByPage`. ``categories` is a collection where each item has the following data: 12 | 13 | ```js 14 | { 15 | "title": "Category Name", 16 | "slug": "category-name", 17 | "posts": [ /* full array of posts in the category */ ] 18 | } 19 | ``` 20 | 21 | This is great for simple loops or for categories with small amounts of content. 22 | 23 | You also get `categoriesByPage` which allows you to use 11ty's Pagination functionality to go deeper and paginate posts per category, as well, with more data and helper functions. Each category page has the following information (required for making the pages) 24 | 25 | ```js 26 | { 27 | "title": "Category Name", 28 | "slug": "category-name", 29 | "posts": [ /* array of posts in the category on this page */ ], 30 | "permalinkScheme": "category-name/:num/", 31 | "totalPages": 4, 32 | "pages": { 33 | "current": 1, 34 | "next": 2, 35 | "previous": false // (or num) 36 | } 37 | } 38 | 39 | ``` 40 | This is pretty abstract. There are examples below that should hopefully clarify. 41 | 42 | ## Installation 43 | 44 | Install the plugin with 45 | 46 | ```sh 47 | npm install eleventy-plugin-dynamic-categories 48 | ``` 49 | ### Configure 50 | 51 | Add the plugin to your `.eleventy.js` config file. Provide the plugin with the name of the variable that you use in your frontmatter to assign categories to your content. Use `itemsCollection` to specify the key for which collection you want to use. 52 | 53 | |property|description|type|default| 54 | |---|---|---|---| 55 | |`categoryVar`|The name of the variable in your frontmatter that you use to assign categories to your content.|`string`|`categories`| 56 | |`itemsCollection`|The name of the collection you want to categorize.|`string`|`posts`| 57 | |`perPageCount`|The number of items to display per page.|`int`|`5`| 58 | |`categoryCollection`|The name of the collection that will be created by the plugin (must be unique).|`string`|`categories`| 59 | 60 | 61 | ```js 62 | const dynamicCategories = require('eleventy-plugin-dynamic-categories'); 63 | 64 | module.exports = function(eleventyConfig) { 65 | eleventyConfig.addPlugin(dynamicCategories, { 66 | categoryVar: "categories", // Name of your category variable from your frontmatter (default: categories) 67 | itemsCollection: "posts", // Name of your collection to use for the items (default: posts) 68 | categoryCollection: "categories", // Name of the new collection to use for the categories (default: value in categoryVar) 69 | // categoryCollection MUST be unique currently 70 | perPageCount: 5 // Number of items to display per page of categoriesByPage (default: 5) 71 | }) 72 | } 73 | ``` 74 | 75 | ## Build or loop through your categories 76 | 77 | The plugin creates a data structure of an array of categories that contain a title (based on the string for each category), a slug to be used for URLs (slugified from the category name), and an array of items that are assigned to that category. The data is stored as an 11ty Collection with the key of the `categoryVar` you specified or overridden by the `categoryCollection` you specified. The collection name must be unique. 78 | 79 | 80 | ### Usage for pagination: 81 | 82 | ```html 83 | --- 84 | pagination: 85 | data: collections.categories 86 | alias: category 87 | size: 1 88 | permalink: /blog/category/{{ category.slug }}/ 89 | --- 90 | 91 | {% for post in category.posts %} 92 |
  • 93 | {{ post.data.title }} 94 |
  • 95 | {% endfor %} 96 | ``` 97 | 98 | ### Usage for a loop: 99 | 100 | ```html 101 |

    Categories

    102 | {% for category in collections.categories %} 103 |
    104 |

    {{ category.title }}

    105 | 110 |
    111 | {% endfor %} 112 | ``` 113 | 114 | ## Paginated Category template example 115 | 116 | ```html 117 | --- 118 | layout: base.html 119 | # Default permalink scheme (still able to be customized) 120 | permalink: /posts/{{category.permalinkScheme}} 121 | pagination: 122 | data: collections.categoriesByPage 123 | size: 1 124 | alias: category 125 | addAllPagesToCollections: true 126 | eleventyComputed: 127 | title: Blog entries with category "{{ category.slug }}" {% if tcategoryag.pageNumber > 0 %}, (Page {{ category.pageNumber + 1 }}) {% endif %} 128 | --- 129 | 130 | {% for post in category.posts %} 131 |
  • 132 | {{ post.data.title }} yo 133 |
  • 134 | {% endfor %} 135 | 136 | 137 | {# This can still be customized, but then there's markup for basic pagination #} 138 | {% pagination category %} 139 | ``` 140 | 141 | 142 | ## Multiple Category usage 143 | If you need to create multiple categories out of multiple collections, you can add the plugin multiple times with different configruations. 144 | 145 | ```js 146 | const dynamicCategories = require('eleventy-plugin-dynamic-categories'); 147 | 148 | module.exports = function(eleventyConfig) { 149 | eleventyConfig.addPlugin(dynamicCategories, { 150 | categoryVar: "categories", // Name of your category variable from your frontmatter (default: categories) 151 | itemsCollection: "posts", // Name of your collection to use for the items (default: posts) 152 | categoryCollection: "categories" // Name of the new collection to use for the categories (default: value in categoryVar) 153 | // categoryCollection MUST be unique currently 154 | }) 155 | eleventyConfig.addPlugin(dynamicCategories, { 156 | categoryVar: "categories2", // Name of your category variable from your frontmatter (default: categories) 157 | itemsCollection: "posts", // Name of your collection to use for the items (default: posts) 158 | categoryCollection: "categories2" // Name of the new collection to use for the categories (default: value in categoryVar) 159 | // categoryCollection MUST be unique currently 160 | }) 161 | } 162 | ``` 163 | 164 | ## Pagination template tag 165 | The pagination template tag is a helper tag that generates markup for basic pagination to save template overhead. It accepts the page information from the pagination item (usually aliased to something like `category`). 166 | 167 | For each page, this will generate pagination that includes next and previous links as well as a list of page numbers. The current page will be styled as active. 168 | 169 | ### Usage 170 | ```html 171 | {% pagination category %} 172 | ``` 173 | 174 | --------------------------------------------------------------------------------