Trying to understand routes using filter, data, and template together

In the example given in this doc, I’m having trouble understanding how filter and data work together.

collections:
  /portfolio/:
    permalink: /work/{slug}/
    template: work
    filter: primary_tag:work
    data: tag.work

Isn’t filter providing all posts with primary_tag:work to work.hbs?
But at the same time, data is also providing all posts with work tag.
Aren’t they both doing the same things?

It’s been a while since I’ve done routing, but if memory serves

Filter - tells Ghost what posts belong to this collection. Also, the post data will be available in the {{#post}} context (I think) in the work.hbs template

Data - additional data to be fetched when rendering - in the case of tag.work, the {{#tag}} context will contain the work tag data

If you look in your Ghost admin at the TAG pages, you will see one for work - You can edit this page - e.g. setting a feature_image, meta-data etc.

The in the routes.yaml you tell Ghost to use the data from this tag.work page for the collection index.

In your template, you can access the details from the tag.work page using the {{#tag}} helper, for example:

{{!-- custom-work.hbs --}}
{{#tag}}
<header class="site-archive-header">
  {{> header-background background=feature_image }}

The info from the page specified in data: is available - i.e. the feature_image comes from that page. As you used tag.work you have to use the {{#tag}} helper - if you chose a page or post you would use the {{#post}} helper.

Clear as mud? :slight_smile:

another way to think about it:

  1. the data: tag.work supplies the information for the collection index page itself
  2. the filter: primary_tag:work supplies the list of articles in this collection

I think this data model is poorly structured and causing confusions. Proposed solution at the bottom.

Here’s what I’ve figured out after a full day of research.

  1. A dynamic page needs a template rather than a post or a page which are typically static (though they too can be made dynamic)

  2. A template needs
    a. title
    b. header image
    c. excerpt
    d. posts to display

  3. So this may make sense

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        template: work
          title: My title
          image: header.png
          excerpt: This is the summary of the resulting page
          posts_filter: primary_tag:work
          posts_count: 10
    

    title, image, excerpt, posts_filter, and posts_count belong under template because they are consumed by template. If they’re on the same level as template, it signifies that those fields are consumed by their parent object /portfolio/ which is a not the case.

  4. An equivalent model using a variable would be

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        template: work
          data: portfolio_variables
    
    data:
      portfolio_variables:
        title: My title
        image: header.png
        excerpt: This is the summary of the resulting page
        posts_filter: primary_tag:work
        posts_count: 10
    
  5. A disadvantage of this model is that the template data is stored in routes.yaml which isn’t easy to modify. Where should the template data be stored that’s easy to modify?

    Ghost decided to repurpose post, page, and tag objects as a storage for holding the template data (!!!)

    Until now, a post or a page was an object designed to be displayed on a screen. This is no longer the case. Now, we have a variant of post or a page that’s designed to hold some values for a template, essentially a hashmap seen at the bottom of 4.

    Attempting to display these new variants of post and pages will result in an incomplete page, meaning now we have posts and pages that are not suitable to be displayed on a screen.

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        template: work
          data: post.slug (a new variant of post that holds the template data)
    

    In addition, Posts, Pages, and Tags tabs now show the corresponding objects (posts, pages, and tags) AND unrelated objects template data storage.

  6. What’s worse, the existing post, page, and tag objects do not have fields for storing posts_filter or posts_count information, which are needed in a template. As a result, now we specify the data which points to a new variant of post, page, or a tag AND posts filter AND posts count.

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        template: work
          data: post.slug (a new variant of post that holds the template data, except for the filter and limit)
          filter: primary_tag:work
          limit: 10
    

    At this point, the keyword data doesn’t make sense anymore. If data holds “the data used by the template”, why aren’t filter and limit included in the data? (Who even named this field data? That’s just a generic word. static_fields would be more accurate.)

  7. And to make it even more confusing, data, filter, and limit are on the same level as template, arriving at

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        template: work
        data: tag.work
        filter: primary_tag:work
        limit: 10
    

I’m not mad, I’m just disappointed.

Here is the proposed solution.

  1. Create a new type called list which holds

    • < all the static data >
      • title
      • image
      • excerpt
      • tags applied to this list
    • < all the dynamic data >
      • filter
      • limit
    • < handlebars file >
      • template
  2. routes.yaml should use it like this

    collections:
      /portfolio/:
        permalink: /work/{slug}/
        list: work (points to a "work" instance of the new type "list")
    

This way, routes.yaml focuses on routing and list object focuses on generating a dynamic page.

Thank you for attending my TEDx.

Firstly, some taxonomy :slight_smile:

  • Your analysis appears to use Template when, perhaps you mean Index (aka Archive in some places in Ghost).
  • For me:
    • Template - a Handlebar template .hbs - used to render a Route Index, a Collection Index, a single Post, a Tag Index etc.
    • Index - a page that fronts a set of [Post|Page|Tag|Author] - needs a Template to render
      • an Index needs it’s own attributes as well as a Paginated set of children
    • Route - a set of Articles, fronted by a paginated Index.
      • Articles can appear in many Routes
    • Collection - a set of Articles, fronted by a paginated Index.
      • Collections are a structural / semantic sub-division of Articles
      • Articles only appear in ONE Collection
      • The Filter on a Collection is a first-order attribute - it defines the Collection

So - I actually prefer the way is currently is because (IMHO):

  • KISS - current approach is simple to understand and use.
  • it fits the style/approach/philosophy/tools used to write Ghost - it’s important that Ghost has an internally consistent approach.
  • Collections are a Very-Special-Case - used to create structural semantic divisions in Articles. I think this is why the rules are simple (and hence restrictive).
  • You have moved the Collection-Filter into Data, but:
    • Filter defines the Collection - it is a material first-order attribute of the Collection - it’s not data to be tweaked, like Title, Accent Colour etc.
    • Permalink is also structural - remember that song? Don’t go breaking my Links
    • If you want to use Filter in a Template - you should probably be using a Route.
  • Safety - I want to be able to change the Title, Image etc. safely using the Admin editor
    • this limits the scope of the change
    • routes.yaml is sensitive and affects the entire Site.
    • Much safer for users of a custom Theme

My main bugbear with the setup is the inconsistency between the Admin editor and the model - for example:

  • create a Template called custom-authors.hbs
  • create a static Page called /authors/ - and select the custom-authors.hbs as the template
  • on the /authors/ Page set the Title -> you can now render this {{title}} in your Template.
  • on the /authors/ Page add some Content - you might think that you can use {{content}} - the Content is actually in {{excerpt}} - meh… this is just something you have to discover.

These pages are a great resource - you gotta love the Ghost docs! https://ghost.org/docs/api/v3/handlebars-themes/context/

Thank you for the detailed response. I really appreciate it.

Index does sound like what I’m looking for. I saw the doc for it but I don’t understand the “Contexts & Templates” section. It talks about how to detect it, but how do I have a custom URL to be an index? For example, how do I set /archive/ to use Index context?

I can see that logic for collections because as you noted collections are a Very-Special-Case.

But how about the same syntax used in routes? Deciding the filter or limit currently requires editing routes.yaml.

routes:
  /newsletter/:
    controller: channel
    data: page.newsletter
    filter: tag:newsletter
    limit: 20
    template: newsletter

It would be more flexible and safer to edit if it had pointed to an object which holds the data.

routes:
  /newsletter/:
    controller: channel
    list: newsletter

routes.yaml should be about routing. I think it should simply point to a resource which has more information, rather than have content information. Imagine our wifi routers caring about the content - doesn’t seem right.

I agree and this is why I think it’s bad to repurpose post, page, and tag as a template data storage.

  1. It breaks the paradigm that post and page should be displayed on the screen, as now they must also act as a template data storage. Ghost haphazardly patches this issue by redirecting the slug of the data source to the route, so that the undisplayable post and page can’t be displayed.

  2. Because post, page, tag aren’t designed to be a template data storage, it doesn’t have a place to store filter and limit which causes the fields to spill over into routes.yaml

  3. you might think that you can use {{content}} - the Content is actually in {{excerpt}} IMO this is again exactly because page is reused to be a template data storage. It’s not made to be one, and now it has to act as one, therefore the abstraction becomes inconsistent.

I kind of look at it the other way up :slight_smile:

I see Posts, Pages, Tags as first-order information schema, rather than as mere holding places for Template display.

I see the Templates as secondary helpers that transform the structured information into a suitable format for a particular rendering surface - HTML, Print-language, RSS feed etc.

I view Routes as a convenient, permanent search of Articles, for Consumers.

I view Collections as fundamental structural sub-divisions of Articles.

That’s why I quite like the information being stored in Pages, Posts, Tags - my niggles are all small trivial implementation details.

The Context stuff is all the juicy goodness that the Ghost developers have given us to make life much easier in Template land - think of them as straight-forward interfaces to the structured data in your Articles (Posts) and using a consistent approach, also to the extra structured data you need to implement a site - Pages and Tags.

The easiest way to grasp it is to play with it all e.g. a series of small features like:

  1. implement a Tags Index - https://aspirethemes.com/blog/ghost-tags-page
  2. implement your own Authors Index using the same approach
  3. implement a convenient Route: search e.g. /linux/ -> all posts with any of [linux, kernel, linus, debian] tags
  4. implement a /tweets/ Collection that is distinct and separate from your usual Article stream, using primary_tag:#tweet

What I don’t understand is how to apply Index context to /linux/. I understand how to use {{#get}} to query the posts and show it. However, on an Index context, pagination is baked into the URL /linux/:num/ based on the data from package.json.

Given a page with a list using index.hbs template, how do I use Index context on it?

Pagination seems to be work automatically… you just have to include the {{pagination}} control somewhere in your custom- template.

The only thing that’s a pain is that I haven’t found a way to say

{{#gt ../pagination.pages 1 }} {{pagination}} {{/gt}}

because the {{#gt...}} {{#lt..}} {{#eq..}} helpers haven’t been set.

It should be noted that I just hack Casper templates - I haven’t tried to add custom Handlebar helpers.

The multi-page URLs look like this: /linux/page/2/ etc.

Oh my god that never occurred to me. Thank you! Based on this

The index context is present on both the root URL of the site, e.g. / and also on subsequent pages of the post list, which live at /page/:num/ .

I would have never guessed that /linux/page/2/ was a possibility.

Hopefully a final question… As a test in package.json I’ve set

"config": {
    "posts_per_page": 3,

and iterate through them using

{{!< default}}

<div id="main-content" class="extreme-container u-marginTop20">
    <div class="row">
        <div class="col col-left story-feed">
            <div class="story-feed-content">
                {{#foreach posts visibility="all"}}
                    {{!-- Story - partials/story/story-list.hbs --}}
                    {{> "story/story-list"}}
                {{/foreach}}
            </div>
        </div>
        {{!-- Sidebar - partials/sidebar.hbs --}}
        {{> "sidebar"}}
    </div>
</div>

{{pagination}}

I’m finding that each page starts with post 1, 4, 7, etc. but each page is also showing all posts, just starting at 1, 4, 7, etc… For example,

  • page 1 shows posts 1, 2, 3, 4, 5, …, n
  • page 2 shows posts 4, 5, 6, 7, 8, …, n
  • page 3 shows posts 7, 8, 9, 10, 11, …, n

I’ve tried setting limit=3 to {{#foreach}} but it still shows all posts. Any idea what I should be doing differently?

Thank you for all your help

It turned out that the theme had an infinite scroll enabled which was causing to load all articles. Turning it off stopped showing all the articles.

1 Like