Building a Custom JavaScript Tagging System for Blog Posts with Directus

Tagging systems are a foundational feature of modern blogs and content sites. They allow users to filter, discover, and navigate content efficiently without relying on a full-page reload. When combined with a headless CMS like Directus, you can manage tags as structured data and then implement a dynamic, JavaScript-driven filter on the front end. This guide walks you through creating a performant, accessible tagging system using Directus as the backend and vanilla JavaScript for client-side filtering.

Why Use Directus for Your Tagging System?

Directus provides a robust, open-source backend that lets you define collections for posts and tags, link them with relational fields, and expose them via a REST or GraphQL API. Unlike hardcoding tag data in HTML, Directus allows editors to update tags, add new ones, and reorganize content without touching the frontend code. This separation of concerns makes your tagging system maintainable and scalable. As you build the JavaScript frontend, you will fetch the latest post and tag data from Directus, then render and filter it in the browser.

Planning the Data Model

Before writing any JavaScript, you need a data structure that reflects the relationship between posts and tags. In Directus, create two collections:

  • Posts – with fields for title, slug, content, publication date, and a many-to-many relationship to tags.
  • Tags – with fields for name and slug.

Directus automatically creates a junction table (e.g., posts_tags) that links the two. This approach allows a post to have multiple tags and a tag to be attached to many posts. After configuring the collections and adding some sample data, you can test the API endpoint, which will return posts with an array of nested tag objects.

Fetching Data from Directus

Use the Directus JavaScript SDK or plain fetch() to retrieve posts along with their tags. The SDK simplifies authentication and query building, but a direct API call is equally effective for a read‑only public blog. For example, a request to https://your-project.directus.app/items/posts?fields=*,tags.tags_id.* returns each post with its related tags. If your Directus instance requires public access, ensure the permissions are set to allow anonymous users to read the posts and tags collections.

Once the data arrives, you can store the posts array and the unique list of tags in JavaScript variables. This data will be the source of truth for your filtering logic.

Rendering Posts and Tags Dynamically

Instead of writing static HTML for each post, generate the markup from the fetched data. Create a function that iterates over the posts array and builds a <div> for each post, including a data-tags attribute containing a space-separated list of tag slugs. For example:

<div class="post" data-tags="javascript tutorial">
  <h3>Building a Tagging System</h3>
  <p>...</p>
</div>

Similarly, dynamically render filter buttons from the tags list. Each button should store the tag slug in a data attribute and display the tag name. Add a class or ID to the container so you can attach event listeners later.

Implementing Client-Side Filtering with JavaScript

The core of the tagging system is the JavaScript that responds to button clicks. Start by selecting all filter buttons and the post container. On each button click, determine the selected tag slug and then loop through the posts. For each post, check whether its data-tags attribute contains the slug. Use includes() for a simple match. Show the post if it matches, hide it if not. To keep the UI responsive, use display: none or a CSS class like .hidden.

Handling Multiple Tag Selection

A single‑tag filter is straightforward, but many users expect the ability to select multiple tags to broaden (or narrow) their results. There are two common approaches:

  • AND filtering – a post is shown only if it contains all selected tags. This narrows the result set.
  • OR filtering – a post is shown if it contains any of the selected tags. This expands the result set.

For a blog, OR filtering is more common because it helps users discover related content. To implement OR filtering, maintain an array of active tag slugs. When a button is clicked, toggle its slug in the array (add if absent, remove if present). Then, for each post, use Array.some() to check if at least one of the active slugs is present in the post’s tags. If the array is empty, show all posts. Update the button styles to indicate the active state – for example, add a .active class that changes the background color.

Enhancing the User Experience

Beyond basic filtering, consider these improvements:

  • Active state indicator – Use CSS and JavaScript to highlight the selected filter buttons. Visually communicating which filters are active helps users understand the current view.
  • Filter clearing – Provide a “Clear all” button or allow clicking an active button again to deselect it. This makes the interface more forgiving.
  • Animations – Smoothly transition post visibility with CSS transitions or the animate() API. However, avoid animations that delay the display of content; users value speed.
  • Accessibility – Ensure filter buttons are focusable and respond to keyboard events. Use aria-pressed or aria-selected to communicate state to assistive technologies. When posts are hidden, consider using aria-hidden="true" on hidden containers, but it’s often sufficient to rely on the hidden attribute or display: none.

Integrating with the Directus SDK for Reactivity

If your blog grows, you may want to re‑fetch data when the user adds a new tags. Directus’s SDK supports GraphQL subscriptions or WebSocket connections for real-time updates. Alternatively, use a simple polling mechanism: fetch fresh data every 60 seconds and re‑render the posts if the data changed. This approach keeps the tagging system current without a full page reload.

For a static site generated with Directus data, you would typically fetch the data during the build process and output static HTML with tags embedded. However, the JavaScript filtering technique described here works on top of static HTML because the tag data is already in the DOM. For dynamic sites (e.g., SPAs), you can store the data in a global variable and re‑filter as needed.

Performance Optimizations

Filtering a few hundred posts in the browser is nearly instantaneous, but if you have thousands of posts, optimizations matter:

  • Use a virtual list – Render only the visible posts using an intersection observer. Libraries like @tanstack/react-virtual help, but for vanilla JS you can implement a simple lazy render.
  • Debounce rapid clicks – If users quickly toggle multiple filters, debounce the filtering function by 50‑100ms to avoid unnecessary reflows.
  • Cache the query results – Store the fetched post data and only re‑query when you know the data has changed. For a blog that updates occasionally, this saves bandwidth.
  • Use requestAnimationFrame – Batch DOM changes inside a requestAnimationFrame callback to avoid layout thrashing.

Testing and Debugging

Test your tagging system across different browsers and screen sizes. Verify that filtering works correctly when JavaScript is disabled (provide a fallback, like a server‑side rendered tag list). Use the browser’s developer tools to inspect the data attributes and ensure they match the expected values. If some posts appear incorrectly, check that the tags are properly joined in your Directus API response.

For Directus‑specific issues, review the collection permissions and ensure the API fields are exposed. The Directus API Explorer is a handy tool to test requests before writing frontend code.

Going Further: Advanced Features

Once the basic tagging system is in place, consider adding:

  • Combined search and tags – Allow users to search by keyword and filter by tags simultaneously. Combine the conditions in JavaScript: show posts that match the search query AND belong to at least one active tag.
  • URL synchronization – Update the URL query string (e.g., ?tags=javascript,tutorial) when filters change. On page load, read the query string to restore the filter state. This enables bookmarking and sharing filtered views.
  • Tag clouds or weight visualization – Display tags with varying font sizes based on their popularity. Directus can expose a count field in the tags endpoint using aggregation functions.
  • Analytics – Track which tags are most often used for filtering. Send events to Google Analytics or another service to understand user interests.

Example: Filtering with AND Logic

While OR filtering is more common, some use cases require AND logic – for example, displaying only posts that cover both “React” and “State Management”. To implement AND, replace Array.some() with Array.every(). For each post, check that every active tag slug is present in the post’s tags. This reduces the number of visible posts as more tags are selected. Provide clear UI feedback so users understand that selecting more tags narrows the results.

Conclusion

A custom JavaScript tagging system powered by Directus gives you full control over the user experience while keeping content management simple. By fetching structured data from your headless CMS, dynamically rendering posts and tags, and implementing client-side filtering, you create a smooth, fast browsing experience that encourages readers to explore more content. With the enhancements discussed – multiple tag selection, accessibility, performance tweaks, and URL synchronization – your tagging system can rival that of any major blogging platform. Start by setting up your Directus collections, fetch the data, and gradually add layers of interactivity. The result is a maintainable, scalable solution that puts content discovery at the forefront.

For further reading, consult the Directus documentation for API details, and review the MDN guide to data attributes for best practices in HTML. For accessibility considerations, the WAI-ARIA Authoring Practices provide patterns for filter controls.