Filter Posts by Date Block

I want to add a new block to my theme that shows a list of years / months, and the number of posts for each, and allows you to click and see those posts, in the same way that you can with tags.

I do not see any obvious way to do this from the API, any pointers?

Hey @TeslaCuil

I have a page in my theme called archive.hbs that I use to do what you’re saying. It may not cover your exact criterion but it should help as a good starting place. Here’s an example of the code:

    <h1>{{t "Archive"}}</h1>
    {{#get "posts"}}
      <span>Browse the complete archive of</span>
      <span>{{t "{total}"}}</span>
  <div class="">
    {{#foreach posts visibility="all"}}
      <p>{{date format="YYYY"}}</p>
      <p>{{date published_at format="DD MMM YYYY"}}</p>
      <h2><a href="{{url}}">{{title}}</a></h2>

You can view it in action here: The Company Theme (Page 1)


the date formatting stuff should come in handy, thanks!

but how would you go about filtering it further to get a list of unique years, along with the post count for that year?

I would like something like this:

Ideally, it would actually be just a list of years, with a list of months as a subsection.

1 Like

Yeah, this is something I will have to figure out eventually, as I do believe it’s the most ideal setup. It’s definitely on my list of tasks to complete.

If you learn anything in the meantime, please share!

This is probably best done with some custom coding. One option could be to write a new template “helper” that would create new template tags to use. Examples of how the current template tags specific to Ghost are implemented is here:

A bit more of documentation about writing them is here: Expressions | Handlebars

Generating this list by looping through all the posts in the database in JavaScript could be somewhat inefficient, though. A more efficient approach could be to write some custom database/SQL code that directly returns a unique list of months or years that posts were created in, and the number associated with is.

I don’t see a shortcut to complete this task using just HTML/CSS/JavaScript in a theme template that performs efficiently once the number posts grows large.

1 Like

Here is another example in case it is helpful (you can see it in action here)…

…and note the Javascript at the bottom)…

{{!-- template: site-archive-custom-post-template | 2022.06.16 --}}

{{!< default}}

<style> {
    list-style: none;
    margin-left: 0 !important;
    margin-top: 0 !important;
    line-height: 2 !important;
    font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif;

.post-archive-item {
    display: flex;
    justify-content: start;
    align-items: normal;
    flex-wrap: nowrap;
    line-height: 2!important;
    font-family: 'GCentra',-apple-system,BlinkMacSystemFont,"PingFang SC","Heiti SC","STXihei","Microsoft YaHei",sans-serif!important;

.post-archive-item a {
    text-decoration: none;
    vertical-align: sub;
    font-size: 16px;
    line-height: 1.6rem;

.post-archive-item .date {
    margin-right: 0.75rem;

.post-archive .post-archive-item time {
    white-space: nowrap;
    width: 75px;

.date-tag {
    font-size: .9rem!important;
    background-color: #e0dddd;
    border-color: #dbdbdb;
    border-width: 1px;
    color: #363636;
    cursor: pointer;
    justify-content: center;
    padding-bottom: calc(0.375em - 1px);
    padding-left: 0.75em;
    padding-right: 0.75em;
    padding-top: calc(0.375em - 1px);
    text-align: center;
    white-space: nowrap;

h2 {
    margin: 1em 0 0.5em;

hr {
    background: linear-gradient(90deg,rgba(254,22,97,.6) 0,rgba(22,93,254,.5) 100%);
    border: none;
    display: block;
    height: 2px;
    margin: 3rem 0 0;

.article-byline {

.article-header {
    padding: 0

.article-title {
    text-align: center;

.inset-h2 {
    padding: 25px 0 5px;
    font-size: 2.8rem;
    font-weight: 700;
    background-color: #4e2b2bcc;
    color: transparent;
    text-shadow: 2px 2px 3px rgb(255 255 255 / 50%);
    -webkit-background-clip: text;
    -moz-background-clip: text;
    background-clip: text;


{{!-- Everything inside the #post block pulls data from the post --}}

<main id="site-main" class="site-main">
<article class="article {{post_class}} {{#match @custom.post_image_style "Full"}}image-full{{else match @custom.post_image_style "=" "Small"}}image-small{{/match}}">

    <header class="article-header gh-canvas">

        <div class="article-tag post-card-tags">
                <span class="post-card-primary-tag">
                    <a href="{{url}}">{{name}}</a>
            {{#if featured}}
                <span class="post-card-featured">{{> "icons/fire"}} Featured</span>

        <h1 class="article-title">{{title}}</h1>

        {{#if custom_excerpt}}
            <p class="article-excerpt">{{custom_excerpt}}</p>

        <div class="article-byline">
        <section class="article-byline-content">

            <ul class="author-list">
                {{#foreach authors}}
                <li class="author-list-item">
                    {{#if profile_image}}
                    <a href="{{url}}" class="author-avatar">
                        <img class="author-profile-image" src="{{img_url profile_image size="xs"}}" alt="{{name}}" />
                    <a href="{{url}}" class="author-avatar author-profile-image">{{> "icons/avatar"}}</a>

            <div class="article-byline-meta">
                <h4 class="author-name">{{authors}}</h4>
                <div class="byline-meta-content">
                    <time class="byline-meta-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
                    {{#if reading_time}}
                        <span class="byline-reading-time"><span class="bull">&bull;</span> {{reading_time}}</span>


        {{#match @custom.post_image_style "!=" "Hidden"}}
        {{#if feature_image}}
            <figure class="article-image">
                {{!-- This is a responsive image, it loads different sizes depending on device
                    srcset="{{img_url feature_image size="s"}} 300w,
                            {{img_url feature_image size="m"}} 600w,
                            {{img_url feature_image size="l"}} 1000w,
                            {{img_url feature_image size="xl"}} 2000w"
                    sizes="(min-width: 1400px) 1400px, 92vw"
                    src="{{img_url feature_image size="xl"}}"
                    alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
                {{#if feature_image_caption}}


    <section class="gh-content gh-canvas">
                            {{#get "posts"}}
                                <div class="inset-h2"><center>{{t "{total} posts in total"}}</center></div>
                            <ul class="post-archive">
                                {{#get "posts" limit="all" include="authors" order="published_at desc"}}
                                    {{#foreach posts}}
                                    <li class='post-archive-item' year="{{date format=(t 'YYYY')}}" month="{{date format=(t 'MMMM')}}">
                                            <div class="date">
                                                <time class="date-tag">{{date published_at format=(t "MMM DD")}}</time>
                                            <div class="subtitle">
                                                <a href='{{url}}'>
                                                {{#if featured}}
                                                    <span style="color:var(--main-color)" title="{{t "Featured"}}"><i class="iconfont icon-star"></i></span>
                                                <span class="authors"> - {{#authors}} {{name}} {{/authors}} </span>



{{!-- A signup call to action is displayed here, unless viewed as a logged-in member --}}
{{#if @site.members_enabled}}
{{#unless @member}}
{{#if access}}
    <section class="footer-cta outer">
        <div class="inner">
            {{#if @custom.email_signup_text}}<h2 class="footer-cta-title">{{@custom.email_signup_text}}</h2>{{/if}}
            <a class="footer-cta-button" href="#/portal" data-portal>
                <div class="footer-cta-input">Enter your email</div>
            {{!-- ^ This looks like a form element, but it's just a link to Portal,
            making the form validation and submission much simpler. --}}


{{#contentFor "scripts"}}
// =================================================
// post archive: add year and month break
// =================================================
// Year & Month Break
var yearArray = [];
var monthObj = {};
$(".post-archive-item").each(function() {
    var archivesYear = $(this).attr("year");
    var archivesMonth = $(this).attr("month");
    if (archivesYear in monthObj) {
    else {
        monthObj[archivesYear] = [];
var uniqueYear = $.unique(yearArray);
for (var i = 0; i < uniqueYear.length; i++) {
    var html = "<hr><h2 class='inset-h2'>" + uniqueYear[i] + "</h2>";
    $("[year='" + uniqueYear[i] + "']:first").before(html);
    var uniqueMonth = $.unique(monthObj[uniqueYear[i]]);
    for (var m = 0; m < uniqueMonth.length; m++) {
        var html = "<h4>" + uniqueMonth[m] + "</h4>";
        $("[year='" + uniqueYear[i] + "'][month='" + uniqueMonth[m] + "']:first").before(html);


I’ve starting to get the feeling that what I want isn’t possible right now / would require an update to the API, which is strange to me since it seems like a standard feature of every other blog platform I have looked at.

The solution by @denvergeeks would work, but involves selecting the details of every post from the database, sending a lot of data to the frontend that will be discarded, and then having the browser process every post to come up with the result. Here’s example of what it looks like to query this data directly using SQL. I’ve provided an example of querying the by-month counts and also by-year counts. You can see the report that both queries take “0.00” seconds for a small dataset, and this would be a tiny amount of data to transfer to the frontend, even if there were thousands of posts.

Ghost could extend the Content API to support queries like this.

But as someone who has posted content online for over 20 years, I’ll ask:

Is this this navigation scheme for you, or is it for your users? I used to have my blog organized like that, and I have never once received feedback from someone who said they really appreciated that they could select my blog posts by year to read historical content.

If anyone is staying on my site to read more than one post, they are probably looking for “related” content, either content that has a related tag or maybe the “previous” post.

While I’m sure there are exceptions, organizing navigation by topic seems more user-centered. If people want to read old content, I’m OK if they have to select a topic or search for it.

mysql> select DATE_FORMAT(published_at,'%Y-%m') as yyyy_mm, 
                     count(*) as count 
                     from posts 
                     group by yyyy_mm 
                     order by yyyy_mm;
| yyyy_mm | count |
| 2004-01 |     1 |
| 2006-02 |     1 |
| 2006-05 |     1 |
| 2006-08 |     1 |
| 2007-02 |     1 |
| 2007-08 |     1 |
| 2008-11 |     2 |
| 2009-01 |     1 |
| 2009-08 |     1 |
| 2009-11 |     2 |
| 2010-01 |     2 |
| 2010-12 |     2 |
| 2015-11 |     1 |
| 2016-04 |     1 |
| 2016-08 |     2 |
| 2020-11 |     1 |
| 2021-02 |     1 |
| 2021-10 |     1 |
| 2022-02 |     1 |
| 2022-06 |     1 |
20 rows in set (0.00 sec)

mysql> select DATE_FORMAT(published_at,'%Y') as yyyy, 
                       count(*) As count 
                       from posts 
                       group by yyyy 
                       order by yyyy;
| yyyy | count |
| 2004 |     1 |
| 2006 |     3 |
| 2007 |     2 |
| 2008 |     2 |
| 2009 |     4 |
| 2010 |     4 |
| 2015 |     1 |
| 2016 |     3 |
| 2020 |     1 |
| 2021 |     2 |
| 2022 |     2 |
11 rows in set (0.00 sec)

Another thread that leads to a few more threads that may be helpful enough to get you further along the way:

1 Like

This navigation scheme is entirely for me, the site is not going to be used in any kind of commercial context.

I am going to be using this site a personal journal that I can share with close friends and family. I am currently using google blogger for this purpose, but am looking for other options, since I don’t really like blogger’s interface, and would eventually like to be self hosted.

my ultimate plan with ghost is to try and create an addon that would allow me to integrate with google photos, which is currently the only reason I am still using blogger. I will likely be creating another post about that eventually, but so far I get the feeling that ghost isn’t quite ready for my use case.

For Google photos, you can likely drag and drop photos from there into Ghost.

If you’ve got some coding experience, you write a small script for your server which makes a SQL call like one of the ones above and formats the result as JSON or HTML and writes it out to a web-visible file. This could happen on a timer, like once an hour, or once per day.

Then in a theme file, you could read that JSON or HTML file to complete a navigation scheme.

You could also possibly just maintain the navigation as some static HTML in your theme. That could involve adding a line of HTML once per month.

Dragging and dropping from google photos does not work, but copy paste does.

with google blogger, it’s able to link to the images, rather than copy them into the post but copying seems to work so I guess I could call that a solution, if storage limits aren’t a concern.

regarding the dates, is there any possibility that something like this will be added to the API eventually?
so you could do something like
{{#get “dates” include=“count.posts” limit=“all” as |dates|}}

then the last major thing that seems to be missing is an official app. My understanding is that there was one that was discontinued. do you know if there is any plan to bring it back ever?

what does the yaml route look like for that page, for it to get the required info?

This is the contents …


    permalink: /{slug}/
    template: index

  tag: /tag/{slug}/
  author: /author/{slug}/