Eleventy: Add next and previous aliases to collection items

Created on 12 May 2019  ·  19Comments  ·  Source: 11ty/eleventy

It seems like pagination is the only way to get URLs to the "next" and "previous" items in a collection. Then I can create a template with this front-matter:

pagination:
  data: collections.posts
  size: 1

Here's the rub:

  • If I put my collection templates outside of my input directory (config.dir.input) then they don't create a collection.
  • If I put my collection templates in my input directory, then each one produces two .html documents: one for the template itself, and one for its page in the collection.

How can I paginate without the extra output documents?

enhancement favorite

Most helpful comment

@thejohnfreeman @brycewray maybe I don't fully understand what it is you're trying to achieve, but doesn't something like this work?

// .eleventy.js
eleventyConfig.addCollection("posts", function(collection) {
  const coll = collection.getFilteredByTag("posts");

  for(let i = 0; i < coll.length ; i++) {
    const prevPost = coll[i-1];
    const nextPost = coll[i + 1];

    coll[i].data["prevPost"] = prevPost;
    coll[i].data["nextPost"] = nextPost;
  }

  return coll;
});

This way you'll get access to the next and previous post anywhere you're using the collection:

{{ prevPost.url }} {{ prevPost.data.title }}

All 19 comments

Try this one:

pagination:
  data: collections.posts
  size: 1
  alias: posts
  reverse: true
permalink: /posts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber }}/{% endif %}index.html
---

<div class="pagination">
  {% if pagination.previousPageHref %}
  <a class="pagination-item prev" rel="prev" href="{{ pagination.previousPageHref }}">← Newer</a>
  {% endif %}

  {% if pagination.nextPageHref %}
  <a class="pagination-item next" rel="next" href="{{ pagination.nextPageHref }}">Older →</a>
  {% endif %}
</div>

You can read more about pagination on the pagination docs.

Did you read the full question? The title doesn't tell the whole story. Pretty sure your answer is going to produce the double output I talked about.

Oh no, I'm so sorry. I'm just reading the title and make comments, maybe sleepy. :v:

@thejohnfreeman

Have you tried using the filter pagination option? May be able to use it to exclude items to prevent doubling. (May not work in your case.)

Did you try defining your own collection if keeping the items outside of config.input.dir? This will create the collection, but they won't be built as final individual .html files. (You could then use pagination again to generate a template file for each item in the collection.)

eleventyConfig.addCollection('outsideOfInputDir', c => {
  return c.getFilteredByGlob('some/where/outside/of/input/dir/**/*.md');
});

@jevets Yes, I forgot to mention that my attempt to put the files outside my input directory involved trying to build the collection in the configuration, but it came up empty. I think it's because only files in the input directory ever get added to the collections parameter, and getFilteredByGlob just filters files added to the collections parameter; it does not trigger a new search for more files outside of the input directory. I've put the broken example in the pagination branch of my repository.

@thejohnfreeman

(Assuming you checked out #211.)

Yeah, you're right that files only get added to collections if they're in config.dir.input. I forgot about that, and I think I've attempted that a few times in the past to no avail.

  • You do want each post to get its own .html file in the final build output, right? (i.e. _site/blog/2012-04-25-disqus-with-markdown/index.html)
  • And in each one of those .html files, you want to show next/prev post links (based on publish date or whatever your sorting is)
  • And you want a blog index template that shows, say 10 posts per (pagination) page and generates _site/blog/page/x/, right?

Gonna have to keep the post markdown files inside of config.dir.input, and if you do end up using pagination size=1 to generate the single post pages instead of using the markdown files directly as templates (as @zachleat mentions in #211), you could bypass permalinks in a posts/posts.json file to keep them from being output twice as html files (you'd rely on pagination w/ size=1 and dynamic permalinks instead).

I'm gonna clone your repo later on or setup a separate example to dig more into this. But for now, let me know if any above assumptions are incorrect or if any other info to consider.

Those assumptions are correct. Right now, my index page just has all the posts since there are not many (page size 100, let's say).

I updated the pagination branch of my blog with your suggestions.

  • I have a _posts subdirectory of my input directory (config.dir.input) with all my posts as Markdown files.
  • In that _posts directory is a directory data file, _posts.json, with permalink: false to disable normal output for all the posts.
  • I moved the post layout from _includes/post.liquid to be the page template at blog/index.liquid.
  • I added pagination settings to the page template with page size 1.

What does this give me?

  • Single output for every post.
  • Each post is put at the right permalink.
  • Within the page template, I get URLs for the previous and next posts.

There are a couple remaining problems (in my opinion):

  • On my blog index page, I have to manually construct the URL to the "page" for each post.
  • Pagination does not give me the neighboring post data so that I can get their titles. I want to use the post title for my link text instead of "Next" or "Previous".

Here's some code to solve the second remaining problem (post titles in next/prev post links). And a screenshot of it running locally.

Screen Shot 2019-05-14 at 11 41 32 AM

// blog/index.liquid

{% assign previousIndex = pagination.pageNumber | minus: 1 %}
{% assign previousItem = collections.posts[previousIndex] %}
{% assign nextIndex = pagination.pageNumber | plus: 1 %}
{% assign nextItem = collections.posts[nextIndex] %}

<pre>
  {% if previousItem.data.title %}
    Previous title: {{ previousItem.data.title }}
    Previous URL: {{ pagination.previousPageHref }}
  {% endif %}
  {% if nextItem.data.title %}
    Next title: {{ nextItem.data.title }}
    Next URL: {{ pagination.nextPageHref }}
  {% endif %}
</pre>

Regarding the first problem:

On my blog index page, I have to manually construct the URL to the "page" for each post.

Are you referring to the the disqus config?

this.page.url = 'https://thejohnfreeman.com{{ page.url }}';

@thejohnfreeman

A couple notes on the previous/next post titles and links.

  • Be careful about the sort order. If you ever change how you're sorting posts (asc/desc), you'll probably need to update in two places to ensure indexes match. (i.e. next item on blog index shows what the single blog post considers to be the previous item)
  • previousItem.url and nextItem.url won't work, since your _posts.json returns false for permalinks. You could probably change it up to make each post markdown files its own template, then use collections.posts to lookup the current item (maybe use fileSlug) and get back the previous and next items. Then you could use previousItem.data.title and previousItem.url (and nextItem). Not sure it's worth it to you.

The first problem is regarding the URL of each post on the home page:

<table class="table">
  <tbody>
    {%- for post in collections.post reversed -%}
    <tr>
      <td>{{ post.date | utcDate: 'YYYY[&nbsp;]MMMM[&nbsp;]D' }}</td>
      <td><a href="{{ post.url }}">{{ post.data.title }}</a></td>
       <!-- this ^ url has to be constructed manually to match what I use in the page template -->
    </tr>
    {%- endfor -%}
  </tbody>
</table>

I've thought about trying to add some function or data to link within a collection without using pagination, but I haven't dug into it yet. I think I would prefer that approach considering I'd have to do essentially the same thing in pagination to get the neighboring titles.

Yeah, if each post markdown file was its own template, its url would be built appropriately for you. That'd be enough for me to consider switching over.
Then you'd only need pagination for the home page, eventually (if ever).

@thejohnfreeman I do not know if this would be of any help for you, but here is what I came up with to bring the previous and next links to a portfolio where each photo is a post from a collection:

I created 2 shortcodes in my .eleventy.js file:

eleventyConfig.addShortcode('previous', (collections, [tag], {inputPath}) => {
  // Assumes the first tag to be the filter for the post to be "paginated".
  const collec = collections[tag];

  for (let i = 0; i <= collec.length; i++) {
    if (collec[i+1] && collec[i+1].data.page.inputPath === inputPath) {
      return `<a href="${ collec[i].data.page.url }">Previous</a>`;
    }
  }
});

eleventyConfig.addShortcode('next', (collections, [tag], {inputPath}) => {
  // Assumes the first tag to be the filter for the post to be "paginated".
  const collec = collections[tag];

  for (let i = 1; i <= collec.length-1; i++) {
    if (collec[i-1] && collec[i-1].data.page.inputPath === inputPath) {
      return `<a href="${ collec[i].data.page.url }">Next</a>`;
    }
  }
});

These 2 shortcodes are to be used this way in the template or layout:

<!-- POST goes here -->
{% previous collections, tags, page %}&nbsp;|&nbsp;{% next collections, tags, page %}

@morgaan Have tried a variation on this but can’t seem to handle what happens if there is no previous or next — always comes up as undefined which is, of course, to be expected but I can’t seem to if/else my way out of showing an undefined result. When I do the following...

  eleventyConfig.addShortcode('next', (collections, [tag], {inputPath}) => {
    // Assumes the first tag to be the filter for the post to be "paginated."
    const collec = collections[tag]

    for (let i = 1; i <= collec.length-1; i++) {
      if (collec[i-1] && collec[i-1].data.page.inputPath === inputPath) {
        return `<p class="ctr"><strong>Next</strong>: <a class="next" href="${ collec[i].data.page.url }">${ collec[i].data.title }</a></p>`
      } else {
        return `<p style="display: none;">No next</p>`
      }
    }
  })

  eleventyConfig.addShortcode('previous', (collections, [tag], {inputPath}, ) => {
    // Assumes the first tag to be the filter for the post to be "paginated."
    const collec = collections[tag]

    for (let i = 0; i <= collec.length; i++) {
      if (collec[i+1] && collec[i+1].data.page.inputPath === inputPath) {
        return `<p class="ctr"><strong>Previous</strong>: <a class="previous" href="${ collec[i].data.page.url }">${ collec[i].data.title }</a></p>`
      } else {
        return `<p style="display: none;">No previous</p>`
      }
    }
  })

...everything comes up blank (except on the very first post, which does indeed show a “Next” but no “Previous” as is desired in this case). In other words, they all fail the test. If I remove the else part from each, it works but, again, null values produce the expected undefined. I’m sure there’s something obvious that I’m missing. Any thoughts?

Update: Ah, I see now. The return is breaking the loop and so i never increments past 1 at most. (D’oh.) Will see if I can compensate for that.

@thejohnfreeman @brycewray maybe I don't fully understand what it is you're trying to achieve, but doesn't something like this work?

// .eleventy.js
eleventyConfig.addCollection("posts", function(collection) {
  const coll = collection.getFilteredByTag("posts");

  for(let i = 0; i < coll.length ; i++) {
    const prevPost = coll[i-1];
    const nextPost = coll[i + 1];

    coll[i].data["prevPost"] = prevPost;
    coll[i].data["nextPost"] = nextPost;
  }

  return coll;
});

This way you'll get access to the next and previous post anywhere you're using the collection:

{{ prevPost.url }} {{ prevPost.data.title }}

@pascalw Yes, was just about to sit down and try basically that exact approach (i.e., return outside the loop) before I have to head on an out-of-town trip. 👍 But I would not have had your incredibly elegant logic in there — just want to be clear on that. You win big-time on that.

Update: Works perfectly. You da man! 💯 Thanks very much!!

Can also confirm the solution by @pascalw works very nicely for me as well. Hats off to you!

Gonna swap this issue to the enhancement queue to adopt something like @pascalw’s solution into core https://github.com/11ty/eleventy/issues/529#issuecomment-568257426

This repository is now using lodash style issue management for enhancements. This means enhancement issues will now be closed instead of leaving them open.

View the enhancement backlog here. Don’t forget to upvote the top comment with 👍!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

michrome picture michrome  ·  3Comments

zac-heisey picture zac-heisey  ·  3Comments

ndaidong picture ndaidong  ·  4Comments

matt-auckland picture matt-auckland  ·  3Comments

nilsmielke picture nilsmielke  ·  4Comments