Articles
Aug 23, 2025 - 12 MIN READ
Implementing WordPress-like Tags in Nuxt Content

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

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

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:

  1. Better Content Organization: Readers can now find all articles on specific topics easily
  2. Improved SEO: Tag pages provide additional entry points for search engines
  3. Enhanced User Experience: The clean, consistent interface feels natural and WordPress-like
  4. 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!

Copyright © 2026