Skip to main content
Martin Hähnel

Nice Permalinks In Eleventy AND Nice Filenames in Obsidian

Changelog

2025-07-06 - Added a couple of headings and added a little section on how to add back the title to post.njk

Wikilinks Broken For This Article

2025-07-07 - I am aware that wikilinks are broken at the moment... But it is very late here on a Sunday so I have to look into this more tomorrow. Sad trombone. I think it has to do with using raw/endraw to show nunjucks syntax below.

A while ago [[eleventy-change-permalinks|I made a post]] about how to change (part of) the permalink that Eleventy generates from the filename. Since I am using Obsidian as my blog editor I had a little problem:

  1. On the one hand: I wanted to have nice, readable filenames - with spaces and everything - so that I can link between entries more easily
  2. On the other: If I use readable filenames - with spaces and everything - they get not slugified correctly, meaning that spaces in the filename are just urlencoded instead of replaced by dashes, etc. (see [[The Closed List|this post]] for an example)
  3. On yet another, secret, third hand: Without a title in the frontmatter, posts do not get a title in my blog

The Solution For Hands One And Two

We replace the permalink of our blog.11ydata.js:

"permalink": "{{page.filePathStem.slice(5).replace(page.fileSlug, '')}}{{page.fileSlug|slugify}}/index.html"

This is a little convoluted as Eleventy doesn't seem to expose the path without the filename. So we have to cut the file name off and then re-add a slugified version.[1]

The Solution For The Secret Third Hand (that is actually many hands...)

My blog is based on the eleventy-base-blog so I had to adjust my postlist.njk so that it understood to look for a title in the fileSlug. Here I had the additional challenge that I already had changed the logic for older posts that were written for a different blog system. Especially my DailyDogo series relied on the fact that if no title was set, then the first 20 characters of the body of a post were used. Changing this to the filename (fileSlug) yielded a lot of posts named "dailydogo".

To solve this, I came up with the following solution: We only use the filename, if it includes a space. This means that all other files will use the old logic, i.e. use a title if given, otherwise use the first 20 chars.

Here's how that looks:

{%- css %}.postlist { counter-reset: start-from {{ (postslistCounter or postslist.length) + 1 }} }{% endcss %}
<ol reversed class="postlist">
{%- for post in postslist | reverse %}
	<li class="postlist-item{% if post.url == url %} postlist-item-active{% endif %}">
		<a href="{{ post.url }}" class="postlist-link">
      {% if post.data.title %}
        {{ post.data.title }}
      {% elseif " " in post.data.page.fileSlug %}
        {# if filename has spaces (probably written in Obsidian use that as title #}
        {{ post.data.page.fileSlug }}
      {% else %}
        {{ post.rawInput | striptags | truncate(20) }}
      {% endif %}
    </a>
		<time class="postlist-date" datetime="{{ post.date | htmlDateString }}">{{ post.date | readableDate }}</time>
	</li>
{%- endfor %}
</ol>

We need a similar solution for the individual post template. Here's the important part:

{% if post.data.title %}
  <h1>{{ title }}</h1>
{% elseif " " in page.fileSlug %}
  {# if filename has spaces (probably written in Obsidian use that as title #}
  <h1>{{ page.fileSlug }}</h1>
{% endif %}

We also need to update our feed[2]:

{%- for post in collections.posts | reverse %}
    {%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
    <entry>
      <title>
        {% if post.data.title %}
          {{ post.data.title }}
        {% elseif " " in post.data.page.fileSlug %}
          {# if filename has spaces (probably written in Obsidian use that as title #}
          {{ post.data.page.fileSlug }}
        {% else %}
          {{ post.rawInput | striptags | truncate(20) }}
        {% endif %}
      </title>
  [...]

And [[search-in-eleventy|the JSON-Feed we use for our search]]:

"items": [
    {%- for post in collections.posts | reverse %}
    {%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
    {
      "id": "{{ absolutePostUrl }}",
      "url": "{{ absolutePostUrl }}",
      "title": "{% if post.data.title %}{{ post.data.title }}{% elseif " " in post.data.page.fileSlug %}{{ post.data.page.fileSlug }}{% else %}{{ post.rawInput | striptags | truncate(20) }}{% endif %}",
  [...]

The pattern should be clear by now. Anywhere where we had used post.data.title or title depending on the context, we now want to use post.data.fileSlug or page.fileSlug with a similar nunjucks if construct.

I had to change a bunch of other places:

[...]

{% if title %}
  <h1>{{ title }}</h1>
{% elseif " " in page.fileSlug %}
  {# if filename has spaces (probably written in Obsidian use that as title #}
  <h1>{{ page.fileSlug }}</h1>
{% endif %}


<ul class="post-metadata">
[...]
---
layout: layouts/base.njk
---
{% if title %}
  <h1>{{ title }}</h1>
{% elseif " " in page.fileSlug %}
  {# if filename has spaces (probably written in Obsidian use that as title #}
  <h1>{{ page.fileSlug }}</h1>
{% endif %}

{{ content | safe }}

And there you have it! Now we can have nice permalinks that are based on the filename and those filenames do not need to be "permalink" friendly (i.e. all small caps, dashes instead of spaces, etc.) and we still can adjust this on an individual basis by specifying a permalink and/or a title in the yaml frontmatter of individual posts.


  1. It doesn't help that the unslugified filename is called fileSlug ↩︎

  2. This one won't exist if you're using the base blog like it is setup out of the box. We have it, because we wanted more than one feed for our [[search-in-eleventy|search solution]]. ↩︎