
Implementing WordPress-like Tags in Nuxt Content
Learn how I implemented a complete WordPress-like tagging system in Nuxt Content, including tag pages, filtering, and content discovery features.
Peter Oliha
After migrating my site from WordPress to Nuxt, I realized there was one key feature missing: tags. In my previous article about migrating from WordPress to Nuxt, I briefly mentioned that tag management was something I hadn't yet fully implemented.
Well, I'm happy to report that WordPress-like tags are now fully functional on this site! You can see them in action below this article and throughout the site. Let me walk you through how I built this feature.
What Makes Good Tag Implementation?
Before diving into the technical details, it's worth understanding what makes a good tagging system. Coming from WordPress, I had certain expectations:
1. Content Discovery
Tags should help readers discover related content easily. If someone is reading about NestJS, they should be able to find all my other NestJS articles with a single click.
2. Visual Integration
Tags should feel like a natural part of the content, not an afterthought. They need to be visually appealing and consistent with the site's design.
3. Scalability
The system should handle growth gracefully. As I add more articles and tags, performance shouldn't degrade.
4. SEO Friendly
Each tag should have its own page with proper SEO metadata, helping with search engine discoverability.
The Technical Approach
Here's how I implemented the complete tagging system:
1. Content Structure
First, I created individual tag files in /content/tags/. While I initially tried using a single YAML file, individual files work much better with Nuxt Content's querying system:
# content/tags/nestjs.yml
name: 'NestJS'
slug: 'nestjs'
description: 'Articles about NestJS framework for building efficient, scalable Node.js server-side applications'
color: 'red'
This approach gives several benefits:
- Better performance (Nuxt Content can efficiently query individual tags)
- Easier management (each tag is self-contained)
- Better caching (only changed tag files are re-processed)
2. Content Configuration
Next, I updated the content configuration to include the tags collection:
// content.config.ts
tags: defineCollection({
type: 'data',
source: 'tags/*.yml',
schema: z.object({
name: z.string().nonempty(),
slug: z.string().nonempty(),
description: z.string().optional(),
color: z.string().optional()
})
});
3. Utility Functions
I created a comprehensive set of utility functions to handle tag operations:
// app/utils/tags.ts
export async function getAllTags(): Promise<Tag[]> {
const tagsCollection = await queryCollection('tags').all();
return tagsCollection || [];
}
export async function getArticlesByTag(tagSlug: string): Promise<any[]> {
const articles = await queryCollection('articles').all();
return articles.filter(
(article) =>
article.tags && article.tags.some((tag: string) => tag === tagSlug)
);
}
export async function getTagsWithCount(): Promise<TagWithCount[]> {
const [tags, articles] = await Promise.all([
getAllTags(),
queryCollection('articles').all()
]);
// Count articles per tag and return only used tags
// ... implementation details
}
These functions handle everything from basic tag retrieval to complex operations like finding related tags based on co-occurrence.
User Interface Implementation
Tag Display on Articles
Tags appear as clickable badges below the author information on each article:
<template>
<div
v-if="pageTags.length > 0"
class="flex items-center justify-center gap-2 mt-4 flex-wrap"
>
<NuxtLink to="/tags">
<UBadge color="primary" variant="outline" size="md"> All tags </UBadge>
</NuxtLink>
<NuxtLink v-for="tag in pageTags" :key="tag.slug" :to="`/tags/${tag.slug}`">
<UBadge color="neutral" variant="subtle" size="md">
{{ tag.name }}
</UBadge>
</NuxtLink>
</div>
</template>
I also added an "All tags" badge to help with discoverability, since tags aren't in the main navigation.
Tag Index Page
The /tags page shows all tags with article counts:
<template>
<UCard v-for="tag in tags" :key="tag.slug" :to="`/tags/${tag.slug}`">
<div class="text-center">
<h3 class="font-semibold text-lg mb-1">{{ tag.name }}</h3>
<p class="text-sm text-muted mb-2">
{{ tag.count }} article{{ tag.count === 1 ? '' : 's' }}
</p>
<p class="text-xs text-muted line-clamp-2">{{ tag.description }}</p>
</div>
</UCard>
</template>
Individual Tag Pages
Each tag gets its own page at /tags/[slug] showing all related articles plus related tags:
<template>
<UPage v-if="tag">
<UPageHero :title="tag.name" :description="tag.description" />
<!-- Related Tags Section -->
<div v-if="relatedTags.length > 0">
<h3>Related Tags</h3>
<div class="flex gap-2 flex-wrap">
<UBadge to="/tags" color="primary" variant="outline">All tags</UBadge>
<UBadge
v-for="relatedTag in relatedTags"
:key="relatedTag.slug"
:to="`/tags/${relatedTag.slug}`"
>
{{ relatedTag.name }}
</UBadge>
</div>
</div>
<!-- Articles with this tag -->
<UBlogPosts>
<!-- Article listing -->
</UBlogPosts>
</UPage>
</template>
Advanced Features
Related Tags
One feature I'm particularly proud of is the related tags functionality. It analyzes tag co-occurrence to suggest related topics:
export async function getRelatedTags(
tagSlug: string,
limit: number = 5
): Promise<Tag[]> {
const articles = await getArticlesByTag(tagSlug);
const relatedTagSlugs = new Map<string, number>();
// Count co-occurring tags
articles.forEach((article) => {
if (article.tags) {
article.tags.forEach((slug: string) => {
if (slug !== tagSlug) {
relatedTagSlugs.set(slug, (relatedTagSlugs.get(slug) || 0) + 1);
}
});
}
});
// Sort by frequency and return top tags
// ... rest of implementation
}
This creates a natural content discovery path where readers can explore related topics.
SEO Optimization
Each tag page includes proper SEO metadata:
useSeoMeta({
title: `${tag.value.name} - Tags - OLIHA.DEV`,
description:
tag.value.description || `Articles tagged with ${tag.value.name}`,
ogTitle: `${tag.value.name} - Tags - OLIHA.DEV`,
ogDescription:
tag.value.description || `Articles tagged with ${tag.value.name}`
});
Challenges and Solutions
Performance Considerations
With 20+ articles and 25+ tags, I had to be mindful of performance:
Solution: Individual tag files allow Nuxt Content to cache efficiently, and I only load tag metadata when needed.
TypeScript Integration
Getting the types right with Nuxt Content's dynamic collections took some iteration:
export interface Tag {
name: string;
slug: string;
description?: string;
color?: string;
}
export interface TagWithCount extends Tag {
count: number;
}
Solution: Making description optional and using proper type guards in filtering operations.
Content Discovery
Since tags aren't in the main navigation, I needed to ensure they're discoverable:
Solution: Added "All tags" badges throughout the interface and made sure tags are prominently displayed on articles.
Results and Benefits
The tagging system has been a great addition to the site:
- Better Content Organization: Readers can now find all articles on specific topics easily
- Improved SEO: Tag pages provide additional entry points for search engines
- Enhanced User Experience: The clean, consistent interface feels natural and WordPress-like
- Easy Maintenance: Adding new tags is as simple as creating a new YAML file
Going further
My current implementation works for me but if I were to expand it, I would consider:
- Tag Analytics: Track which tags are most popular
- Smart Suggestions: Automatically suggest tags for new articles
- Tag Hierarchies: Support for parent/child tag relationships
- Advanced Filtering: Multi-tag filtering on the articles page
Final Thoughts
Implementing WordPress-like tags in Nuxt Content was more straightforward than I initially thought. The key was understanding how Nuxt Content works best—individual files rather than arrays, proper utility functions, and clean separation of concerns.
The system now provides the content discovery features I was missing from WordPress while maintaining the performance and developer experience benefits of Nuxt. If you're building a content site with Nuxt, I highly recommend implementing a similar tagging system early in your development process.
Have questions about the implementation or want to see a specific aspect in more detail? Feel free to reach out!
Have any questions, want to share your thoughts or just say Hi? I'm always excited to connect! Follow me on Bluesky, LinkedIn or Twitter for more insights and discussions. If you've found this valuable, please consider sharing it on your social media. Your support through shares and follows means a lot to me!
How to resolve a blacklisted domain
Learn how to quickly resolve a blacklisted domain issue with SpamHaus. Step-by-step guide to get your domain removed from blacklists and prevent future issues.
Jobven
Jobven is a job posting API that collects listings directly from employer career pages. Fresh, structured job data for developers building job boards and recruiting tools.
