0.1.4 • Published 19 days ago

eleventy-generate-category-pages v0.1.4

Weekly downloads
-
License
MIT
Repository
github
Last release
19 days ago

Eleventy Generate Category Pages

Node module that generates individual category pages (with pagination) for an Eleventy site. Runs as a JavaScript module executed in the Eleventy before event. The module helps developers implement a categories page with descriptions plus separate paginated pages for each category.

I created this initially as a command-line utility (Eleventy Category Pages) but later learned how to execute this inside of the Eleventy build process (using the Eleventy before event), so I pulled the guts out of the command-line utility into this one.

The module:

  1. Recursively reads all the posts in a specified directory
  2. Generates a global data file containing a JSON object representing the list of categories with the following properties:
    • Category Name
    • Description
    • Post count (for the category)
  3. Using a provided template document as a guide, creates a separate content file for each category's content

What this allows you to do is:

  1. Create a categories page for your site that lists all of your site's categories in alphabetical order with:
    • A link to the generated category page
    • A description for the category
    • The number of posts in the category
  2. Create a separate page for each category with pagination of the articles in the category

Background

Eleventy (today) doesn't allow you to generate nested pages with pagination at both levels (parent (categories) and child (category) for example). If you want to have a categories page with a paginated list of the posts within the category you have to either manually create separate files for your category pages manually or hack something together programmatically to do it for you. This module does the latter through a simple command-line command, a configuration file, and category page template.

Installation

To install the module in an Eleventy project, open a terminal window or command prompt, navigate to the Eleventy project, and execute the following command:

npm install eleventy-generate-category-pages

Usage

Create a before event handler in your project's eleventy.config.js file using the defaults:

var firstRun = true;
eleventyConfig.on('eleventy.before', async ({ dir, runMode, outputMode }) => {
  if (firstRun) {
    firstRun = false;
    generateCategoryPages({});
  }
});

The firstRun check exists because of an infinite loop situation created when executing eleventy with the --watch or --serve flags. Generating category pages in these situations causes the category pages to generate with every change in the site and the generation process causes another rebuild - hence infinite loop.

The default options are defined in the following constant (because this is what works for my sites :-):

const configDefaults: ConfigObject = {
  categoriesFolder: 'src/categories',
  dataFileName: 'category-meta.json',
  dataFolder: 'src/_data',
  postExtensions: ['.md', '.njk'],
  postsFolder: 'src/posts',
  templateFileName: '11ty-cat-pages.liquid',
  quitOnError: false,
  debugMode: false,
  imageProperties: false
};

If you wnt, you can use the properties from the dir object passed to the event:

var firstRun = true;
eleventyConfig.on('eleventy.before', async ({ dir, runMode, outputMode }) => {
  if (firstRun) {
    firstRun = false;
    generateCategoryPages({
      dataFileName: 'categories.json',
      dataFolder: dir.data,
      outputFolder: dir.output,
      postExtensions: ['.md'],
      postsFolder: 'src/posts',
      templateFileName: '11ty-cat-page.liquid',
      debugMode: false,
      quitOnError: false,
      imageProperties: false
    });
  }
});

or specify the specific settings for your project:

var firstRun = true;
eleventyConfig.on('eleventy.before', async ({ dir, runMode, outputMode }) => {
  if (firstRun) {
    firstRun = false;
    generateCategoryPages({
      dataFileName: 'categories.json',
      dataFolder: 'src/_data',
      outputFolder: 'src/categories',
      postExtensions: ['.md'],
      postsFolder: 'src/posts',
      templateFileName: '11ty-cat-page.liquid',
      debugMode: false,
      quitOnError: false,
      imageProperties: false
    });
  }
});

The supported configuration options are described below:

PropertyDescription
dataFileNameThe name of the global data file generated by the module. defaults to category-meta.json.
dataFolderThe project source folder for global data files. Defaults to src/_data. Update this value if you use a different structure for your Eleventy projects.
outputFolderThe project source folder where the module places the individual category pages. For navigation simplicity, the module defaults to categories.
postExtensionsArray specifying the file extensions used for site posts. Defaults to ['.md', '.njk']
postsFolderThe project source folder for post files. Defaults to src/posts. Update this value if you use a different structure for your Eleventy projects.
templateFileNameThe file name of the category template file used to generate category pages.
debugModeWhen true, the module generates additional output to the console when it runs.
quitOnErrorWhen true, the build process quits when the module encounters an error.
imagePropertiesWhen true, adds images properties (imageFilePath, imageAltText, and imageAttribution) to the metadata generated by this module. You would use this option if you wanted to display a different header images on posts/articles based on the assigned category. This creates a stable place your site can pull the image data from; here's a post that describes how to use it Eleventy Category Images.

By default, the output metadata file generated by the module looks something like this:

[
  {
    "category": "Cats",
    "count": 1,
    "description": ""
  },
  {
    "category": "Dogs",
    "count": 654,
    "description": ""
  },
  {
    "category": "Turtles",
    "count": 3,
    "description": ""
  }
]

When you enable imageProperties, the output looks like this:

[
  {
    "category": "Cats",
    "count": 1,
    "description": "",
    "imageFilePath": "",
    "imageAltText": "",
    "imageAttribution": ""
  },
  {
    "category": "Dogs",
    "count": 654,
    "description": "",
    "imageFilePath": "",
    "imageAltText": "",
    "imageAttribution": ""
  },
  {
    "category": "Turtles",
    "count": 3,
    "description": "",
    "imageFilePath": "",
    "imageAltText": "",
    "imageAttribution": ""
  }
]

Read below how to use this in your site.

Create the Template File

Every site will have different content for the category pages, so its up to you to create the template file used by the module. Create the file any way you want using whatever template language you're comfortable with. The general format of the file is:

  1. YAML Front matter specifying the layout, pagination, permalink, and so on required for the page.
  2. Content describing the page
  3. The template code required to render the paginated post list on the page (including previous and next buttons)

Here's an example from johnwargo.com:

File: 11ty-cat-pages.liquid

---
layout: generic
pagination:
  data: collections.post
  size: 20
  alias: catposts
category: 
description: 
eleventyComputed:
  title: "Category: {{ category }}"
permalink: "categories/{{ category | slugify }}/{% if pagination.pageNumber != 0 %}page-{{ pagination.pageNumber }}/{% endif %}"
---

{% include 'pagination-count.html' %}

{{ description }}

<p>This page lists all posts in the category, in reverse chronological order.</p>

<ul class="posts">
  {% for post in catposts %}
    <li>
      <h4>
        <a href="{{post.url}}" style="cursor: pointer">{{ post.data.title }}</a>
      </h4>
      Posted {{ post.date | readableDate }}
      {% if post.data.categories.length > 0 %}
        in
        {% for cat in post.data.categories %}
          <a href="/categories/{{ cat | slugify }}">{{ cat }}</a>
          {%- unless forloop.last %},
          {% endunless %}
        {% endfor %}
      {% endif %}
      <br/>
      {% if post.data.description %}
        {{ post.data.description }}
      {% else %}
        {% excerpt post %}
      {% endif %}
    </li>
  {% endfor %}
</ul>

{% include 'pagination-nav.html' %}

Note: The template file front matter must be in YAML format; the module does not understand any other format.

When you generate category data for your site, the module, for each category, converts the template into a category page that looks like this:

---js
{
  "layout": "generic",
  "pagination": {
    "data": "collections.post",
    "size": 20,
    "alias": "catposts",
    "before": function(paginationData, fullData){ return paginationData.filter((item) => item.data.categories.includes('Miscellaneous'));}
  },
  "category": "Miscellaneous",
  "eleventyComputed": {
    "title": "Category: {{ category }}"
  },
  "permalink": "categories/{{ category | slugify }}/{% if pagination.pageNumber != 0 %}page-{{ pagination.pageNumber }}/{% endif %}"
}
---

{% include 'pagination-count.html' %}

{{ description }}

<p>This page lists all posts in the category, in reverse chronological order.</p>

<ul class="posts">
  {% for post in catposts %}
    <li>
      <h4>
        <a href="{{post.url}}" style="cursor: pointer">{{ post.data.title }}</a>
      </h4>
      Posted {{ post.date | readableDate }}
      {% if post.data.categories.length > 0 %}
        in
        {% for cat in post.data.categories %}
          <a href="/categories/{{ cat | slugify }}">{{ cat }}</a>
          {%- unless forloop.last %},
          {% endunless %}
        {% endfor %}
      {% endif %}
      <br/>
      {% if post.data.description %}
        {{ post.data.description }}
      {% else %}
        {% excerpt post %}
      {% endif %}
    </li>
  {% endfor %}
</ul>

{% include 'pagination-nav.html' %}

The first thing you'll likely notice is that the module converted the front matter from YAML to JSON format. It did this because the ability to have separate paginated pages requires filtering on the fly to only generate pages for the selected category. The module does this using the Eleventy Pagination before callback function.

The before callback allows you to programmatically control the posts included in the pagination data set. The template's front matter must be in JSON format for the Eleventy processing tools to execute the before function.

In this example, the module generated the following function which is called before Eleventy starts generating the pagination pages:

function(paginationData, fullData){ 
  return paginationData.filter((item) => item.data.categories.includes('Miscellaneous'));
}

The function essentially returns all of the posts filtered by the category name (which in this case is 'Miscellaneous').

Note: I could have used a filter function in the project's eleventy.config.js file, but that would have added an additional dependency to make this work. Using the before callback eliminates the need to make any changes to the eleventy.config.js file.

Now, looking in the project folder, you should now see:

  1. The global data file (category-meta.json in this example) in the project's src/_data folder.
  2. A separate file for each category in the src/category folder

As shown in the following screenshot:

Project Folder

If you look in your project's src/_data/category-meta.json file, you should see the categories data file as shown below:

[
  {
    "category": "Cats",
    "count": 1,
    "description": ""
  },
  {
    "category": "Dogs",
    "count": 42,
    "description": ""
  },
  {
    "category": "Turtles",
    "count": 8,
    "description": ""
  }  
]

As you can see, the description property is blank for each category. You can edit the file, adding descriptions for each of the categories, your edits won't be overwritten by the module unless you remove all of the posts for the particular category and run the module again.

If you add a new category to the site and re-run the module, the new category appears in the file alongside the existing data:

[
  {
    "category": "Cats",
    "count": 1,
    "description": "Strip steak alcatra filet mignon drumstick, doner ham sausage."
  },
  {
    "category": "Dogs",
    "count": 42,
    "description": "Short loin andouille leberkas ball tip, pork belly pork jowl ham flank turducken meatball brisket beef prosciutto boudin."
  },
  {
    "category": "Ferrets",
    "count": 1,
    "description": ""
  },
  {
    "category": "Turtles",
    "count": 8,
    "description": "Short ribs jowl ground round spare ribs swine tenderloin."
  }  
]

Note: Descriptions provided by the Bacon Ipsum Generator.

When you open one of the files in the src/category folder, you should see a generated category page as described above, one for each category.

Example Categories Page

As an extra bonus, here's a sample Categories page you can use in your site:

---
title: Categories
layout: generic
---

{% assign categories = category-meta | sort %}

<p>View all posts for a particular category by clicking on one of the categories listed below. There are {{ categories.length }} categories on the site today.</p>

<ul class="posts">
  {% for catData in categories %}
    <li>
      <h4>
        <a href="{{ "/" | htmlBaseUrl }}categories/{{ catData.category | slugify }}/">{{ catData.category }}</a>
      </h4>
      Count: {{ catData.count }} |
      {% if catData.description %}
        {{ catData.description }}
      {% endif %}
    </li>
  {% endfor %}
</ul>

Getting Help Or Making Changes

Use GitHub Issues to get help with this module.

Pull Requests gladly accepted, but only with complete documentation of what the change is, why you made it, and why you think its important to have in the module.


If this code helps you, please consider buying me a coffee.

0.1.4

19 days ago

0.1.3

19 days ago

0.0.11

11 months ago

0.0.12

11 months ago

0.0.13

11 months ago

0.0.14

11 months ago

0.1.0

9 months ago

0.1.2

9 months ago

0.1.1

9 months ago

0.0.15

10 months ago

0.0.10

11 months ago

0.0.9

11 months ago

0.0.8

11 months ago

0.0.7

11 months ago

0.0.6

11 months ago

0.0.1

11 months ago