DEV Community

Cover image for Build a Perfect Blog with FastAPI: Filter by Tag
Leapcell
Leapcell

Posted on

Build a Perfect Blog with FastAPI: Filter by Tag

In the previous article, we added the functionality to create and display tags for our blog.

Next, we will complete the remaining part of the tag functionality: filtering posts by tags.

When a user clicks on a tag link, we will take them to a new page that displays only the posts under that specific tag. To do this, we need to create a new route and handling logic in the backend, as well as a corresponding view in the frontend.

Step 1: Extend the Service Logic

First, we need to add two new methods to tags_service.py: one to find a tag by its ID (to get its name), and another to find all associated posts by a tag ID.

Open the tags_service.py file and add the following content:

# tags_service.py
import uuid
from typing import List
from sqlmodel import Session, select
from models import Tag, Post, PostTagLink # Import Post and PostTagLink

def find_or_create_tags(tag_names: List[str], session: Session) -> List[Tag]:
    """
    Find or create tag entities based on a list of tag names.
    """
    # ... this function remains unchanged ...
    tags = []
    if not tag_names:
        return tags

    statement = select(Tag).where(Tag.name.in_(tag_names))
    existing_tags = session.exec(statement).all()
    tags.extend(existing_tags)

    existing_tag_names = {tag.name for tag in existing_tags}

    new_tag_names = [name for name in tag_names if name not in existing_tag_names]

    for name in new_tag_names:
        new_tag = Tag(name=name)
        session.add(new_tag)
        tags.append(new_tag)

    session.commit()

    for tag in tags:
        if tag.id is None:
            session.refresh(tag)

    return tags

def get_tag_by_id(tag_id: uuid.UUID, session: Session) -> Tag | None:
    """Find a single tag by its ID"""
    return session.get(Tag, tag_id)

def get_posts_by_tag_id(tag_id: uuid.UUID, session: Session) -> List[Post]:
    """Find all associated posts by a tag ID"""
    statement = (
        select(Post)
        .join(PostTagLink, Post.id == PostTagLink.post_id)
        .where(PostTagLink.tag_id == tag_id)
        .order_by(Post.createdAt.desc())
    )
    posts = session.exec(statement).all()
    return posts
Enter fullscreen mode Exit fullscreen mode

Code Explanation:

  • get_tag_by_id: A simple helper function to get a tag object by its primary key.
  • get_posts_by_tag_id: This is the core query logic. We use SQLModel's select and join methods to filter out all Post objects associated with the given tag_id through the PostTagLink association table, and we order them by creation time in descending order.

Step 2: Create the Tag Route

Now, let's implement the route to handle /tags/{tag_id} requests.

First, create a new file named tags.py inside the routers folder.

# routers/tags.py
import uuid
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlmodel import Session

from database import get_session
from auth_dependencies import get_user_from_session
import tags_service
import tracking_service

router = APIRouter()
templates = Jinja2Templates(directory="templates")

@router.get("/tags/{tag_id}", response_class=HTMLResponse)
def get_posts_by_tag(
    request: Request,
    tag_id: uuid.UUID,
    session: Session = Depends(get_session),
    user: dict | None = Depends(get_user_from_session),
):
    # 1. Get all posts under this tag
    posts = tags_service.get_posts_by_tag_id(tag_id, session)

    # 2. Get the tag information to display the tag name on the page
    tag = tags_service.get_tag_by_id(tag_id, session)

    # 3. Batch get view counts for the post list to maintain consistency with the homepage
    post_ids = [post.id for post in posts]
    view_counts = tracking_service.get_counts_by_post_ids(post_ids, session)
    for post in posts:
        post.view_count = view_counts.get(post.id, 0)

    tag_name = tag.name if tag else "Unknown"

    return templates.TemplateResponse(
        "posts-by-tag.html",
        {
            "request": request,
            "posts": posts,
            "user": user,
            "filter_name": tag_name,
            "title": f"Posts in {tag_name}",
        },
    )
Enter fullscreen mode Exit fullscreen mode

Finally, don't forget to include this new router module in main.py.

# main.py
# ... other imports
from routers import posts, users, auth, comments, uploads, tags # import the tags router

# ...

# Include routers
app.include_router(posts.router)
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(comments.router)
app.include_router(uploads.router)
app.include_router(tags.router) # Mount the tag router
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Frontend View

The final step is to create a posts-by-tag.html view file in the templates folder. This file will be used to display the list of posts filtered by a tag, and its content will be very similar to index.html.

Create the templates/posts-by-tag.html file:

{% include "_header.html" %}

<div class="filter-header">
  <h2>Posts in Tag: <strong>{{ filter_name }}</strong></h2>
</div>

{% if posts %}
<div class="post-list">
  {% for post in posts %}
  <article class="post-item">
    <h2><a href="/posts/{{ post.id }}">{{ post.title }}</a></h2>
    <p>{{ post.content[:150] }}...</p>
    <small>{{ post.createdAt.strftime('%Y-%m-%d') }} | Views: {{ post.view_count }}</small>
  </article>
  {% endfor %}
</div>
{% else %}
<p>No posts found in this tag.</p>
{% endif %}

<a href="/" class="back-link" style="margin-top: 2rem;">&larr; Back to Home</a>

{% include "_footer.html" %}
Enter fullscreen mode Exit fullscreen mode

This template will dynamically display a title (e.g., "Posts in Tag: Python") and the list of posts under that tag. If there are no posts under the tag, it will display a corresponding message.

Running and Testing

Restart your application:

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

Open your browser, navigate to a post that has tags, and then click on any of the tag links below the post.

You will be redirected to the corresponding tag's filter page and see a list of all posts under that tag.

ImageP1

With these two tutorials, we have added a complete tagging system to our blog.

At this point, our FastAPI blog project has covered everything from basic infrastructure to core features, content organization, and data analysis.

The potential for blog features is limitless. Based on the current framework, you can continue to add more functionalities. The rest is up to your imagination!

Don't forget to deploy on Leapcell—it provides FastAPI support, a PostgreSQL database, Redis, web analytics, and all the tools you need to build web applications.

Leapcell


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)