The related posts are quite easy to add to Ghost if you have some basic knowledge on how to edit your current theme (there's no option to turning this ON and OFF on the Admin, unfortunately, so you must do it on the theme itself).

If you open your theme's files you would see a post.hbs template which is the one for the articles (for "this page you are seeing right now", basically), and the block expression {{#post}}...{{/post}} would be the one containing everything related to the post itself.

Outside that expression, after it, we want to add the related posts taking advantage of the {{#get}} helper.

{{#get}} is a special block helper that makes a custom query to the Ghost API to fetch publicly available data.

get documentation for Functional Helpers by Ghost

Clearly, you can use this helper to get the related posts anywhere you want, like on the sidebar of the blog if yours have one, or in a page instead of a post (but I personally like them at the end of an article).

Let's say you would like to get a list of related posts based on the tags of the current one, which can be done by filtering the data on the filter attribute.

{{#get "posts" filter="tags:[{{post.tags}}]+id:-{{post.id}}" as |related|}}
	[...]
{{/get}}

In this case the filter attribute also contains id:-{{post.id}} to ignore, from the list of posts we are getting, the current article (we wouldn't want to suggest as related post the same post the user just read).

You can add as many filters as you want using the + sign as separator.

Inside this helper, the related "object" will be the one containing all the related post, so you just need to "loop it".

{{#get "posts" filter="tags:[{{post.tags}}]+id:-{{post.id}}" as |related|}}
    <div class="related-posts-wrapper">
        {{#foreach related}}
            <article class="{{post_class}}">
                <a href="{{url}}">{{title}}</a>
            </article>
        {{/foreach}}
    </div>
{{/get}}

Once inside the foreach you are in the context of the post, and then you can just get whatever you want from that post such as the {{url}}, {{title}, etcetera.

You can limit how many post to show by using the limit attribute on the {{#get}} helper (15 by default, which seems like too much)

{{#get "posts" limit="2" filter="tags:[{{post.tags}}]+id:-{{post.id}}" as |related|}}
    ...
{{/get}}

The related posts can be anything you like and you would like to consider as related posts, as you are not limited to that filter for the tags I used as an example.

I'm currently using tags:[{{post.primary_tag.slug}}] within the filters to only consider those posts that have the primary tag of the current post as a tag in any position, as I think those posts would be more relevant to the current one.

{{#get "posts" filter="tags:[{{post.primary_tag.slug}}]+id:-{{post.id}}" as |related|}}
    ...
{{/get}}

This is not the same as using primary_tag:{{post.primary_tag.slug}} which is another filter option, in this case to get only those posts that their primary tag is the same as the current post (there's a slightly difference).

Optionally, you can use the featured:true filter option to get those posts that are checked as featured within the Admin.

{{#get "posts" filter="tags:[{{post.tags}}]+featured:true+id:-{{post.id}}" as |related|}}
    ...
{{/get}}

If you have multiple writers in your blog and you want to show related posts where the current author also participates you can use the authors:{{post.primary_author.slug}} option within the filters.

{{#get "posts" filter="authors:{{post.primary_author.slug}}+id:-{{post.id}}" as |related|}}
    ...
{{/get}}

This is different than using primary_author:{{post.primary_author.slug}} where we are filtering by those posts where the primary author is the same.

Refer to the get documentation on Ghost for more ideas on how to filter.