On the 100-network site I help run, every blog post had a tag block at the bottom that rendered something like:
<footer class="post-tags">
Tagged: <a href="/blog/tag/blog/">blog</a>
</footer>
The tag was blog, because the frontmatter of every post had tags: post and the template had been written to emit each tag as a link. That anchor had been 404ing for months. Every single post on the site had at least one broken tag link, and on posts with multiple tags, the count was worse.
The Link Graph tool flagged it as "crawl-only, not in sitemap." Syntactically valid anchor, working click target, 404 response. The link worked as markup. The destination didn't exist.
How the Bug Gets Shipped
Eleventy's starter templates almost always include a tag-rendering block in the post layout. Something like:
{% if tags %}
<footer class="post-tags">
Tagged:
{% for tag in tags %}
<a href="/blog/tag/{{ tag }}/">{{ tag }}</a>
{% endfor %}
</footer>
{% endif %}
Looks harmless. It iterates the post's tags and emits a link for each. The problem is what the linked URL is supposed to be: a tag pagination page that lists every post with that tag. Eleventy does not generate that page automatically. You have to build it, with a specific template pattern, using eleventyComputed or the pagination key.
Most people who copy an Eleventy starter to a new project remember to fill in the post layout and the index page but never set up tag pagination. The tag anchors ship anyway. The site runs for months with every post linking to phantom tag pages.
The Detection Pattern
The Link Graph tool classifies every link on a site into one of four buckets:
- In sitemap, linked — healthy.
- In sitemap, not linked — orphan.
- Linked, not in sitemap, returns 2xx — may be fine (usually a page that's intentionally excluded from sitemap).
- Linked, not in sitemap, returns 4xx — broken.
The 4xx bucket is where tag links end up. They're linked from every post. They're not in the sitemap because no real page exists for them. They return 404. The tool surfaces each unique 4xx URL and the count of posts linking to it.
On the 100-network site the audit output looked like:
/blog/tag/blog/ linked from 47 posts, 404
/blog/tag/guide/ linked from 12 posts, 404
/blog/tag/review/ linked from 8 posts, 404
Sixty-seven posts had at least one broken tag link. The bug was one line in one template.
Option One: Remove the Tag Links
If you don't actually need tag browsing on your site — and for most small blogs you don't — the right fix is to delete the tag block from the post layout. Tags still exist in frontmatter, still filter correctly in your collection logic, still provide semantic structure for anyone reading the source. They just don't render as clickable links on the front end.
{# Before #}
<footer class="post-tags">
Tagged:
{% for tag in tags %}
<a href="/blog/tag/{{ tag }}/">{{ tag }}</a>
{% endfor %}
</footer>
{# After #}
{# Tag block removed. Frontmatter tags still drive collections. #}
Or, if you want to display the tags without linking them:
{% if tags %}
<footer class="post-tags">
Tagged:
{% for tag in tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</footer>
{% endif %}
Zero 404s. The tags are still readable to the user. The post layout just stops lying about the existence of tag pages.
Option Two: Build the Tag Pages
If you do want tag browsing — useful on larger sites where readers might actually want to find every post on a topic — set up Eleventy tag pagination. The canonical pattern uses a single template that iterates every tag in the site's collection and emits one page per tag.
Create src/blog-tag.njk:
---
pagination:
data: collections
size: 1
alias: tag
filter:
- all
- nav
- post
- postsByYear
permalink: /blog/tag/{{ tag | slugify }}/
layout: page.njk
eleventyComputed:
title: "Posts tagged {{ tag }}"
pageTitle: "Posts tagged {{ tag }} on jwatte.com"
pageDescription: "Every blog post tagged {{ tag }}."
---
<h1>Posts tagged {{ tag }}</h1>
<ul class="post-list">
{% for post in collections[tag] | reverse %}
<li>
<a href="{{ post.url }}">{{ post.data.title }}</a>
<small>{{ post.date | readableDate }}</small>
</li>
{% endfor %}
</ul>
<p><a href="/blog/">All posts</a></p>
Three things to understand about this template:
- The filter list. Eleventy's
collectionsobject includesall(every page),nav(pages tagged "nav"), plus every tag slug from your frontmatter. The filter array is the list of collection keys to skip. Always filter outall,post, andnavor you'll generate a page for each of those too — and a/blog/tag/all/page is not what you want. - The permalink.
undefinedmakes sure tags with spaces or capitals render as clean URLs. Netlify treats/blog/tag/Best%20Reviews/and/blog/tag/best-reviews/as different URLs. Slugify avoids the ambiguity. - The pagination size. Size 1 means "one tag per page." That's the whole pattern — each page in the pagination set is a different tag's archive. Set the size higher and you'll break the template.
After shipping this template, every /blog/tag/SLUG/ URL that was 404ing before now resolves to a real archive page. The Link Graph tool reruns and the crawl-only bucket is empty.
The Sitemap Step
Eleventy's default sitemap plugin iterates collections.all. Once you add tag pages, they get picked up automatically — they're real pages, they have permalinks, they're in the collection. One reload of your sitemap template and the tag pages are discoverable.
Check the sitemap after deploy:
curl https://example.com/sitemap.xml | grep "/blog/tag/"
If you see tag URLs listed, you're done. If you don't, something in your sitemap template is filtering them out; usually a tags != "post" condition that also excludes tag pages.
Why I Default to Option One
On most sites I run, I delete the tag links. Here's the reasoning.
Tag archives are a legacy pattern from WordPress, where every category and tag automatically generated a page and the author's choice was whether to noindex them. Eleventy flips the default: tag pages only exist if you build them. That inversion should be treated as a design decision, not an oversight. Most small blogs have 20 to 100 posts. A tag archive with 4 posts in it is thinner content than a well-written about page. Shipping it creates a weak page, weakens your average page quality, and dilutes your site's authority.
If I have 500 posts and readers genuinely benefit from topic browsing, tag pages are worth building. Below that threshold, I remove the tag links and let the main blog index do the work.
The Short Version
- Eleventy post templates often emit
<a href="/blog/tag/SLUG/">links without the matching tag pagination template. Every tag link is a 404. - The Link Graph tool classifies these as "crawl-only, not in sitemap" and surfaces them with per-post counts.
- If you don't need tag browsing: delete the tag links or convert them to non-linked spans.
- If you do need tag browsing: build
src/blog-tag.njkwith the pagination pattern. Filter outall,post, andnavfrom the collections. - Slugify tag names in the permalink. Capitals and spaces cause duplicate-URL bugs.
- After shipping tag pages, verify they appear in your sitemap with
curl sitemap.xml | grep tag/. - Below 500 posts, removing the tag links is usually the better call. Thin archive pages hurt average page quality.