In the previous tutorial, we added visitor tracking to our blog, allowing us to visually see how popular each article is.
The blog already looks quite complete, but it seems like something is still missing. Your blog already has a lot of articles, and users might get lost in them... So, how can users quickly find the topics they are interested in reading?
That's right, the blog now needs a tag feature.
Tags are used to display the theme and content of an article. You can assign multiple keywords to an article (e.g., "Technical Tutorial," "Nest.js," "Database").
In the next two tutorials, we will add a tag feature to our blog system. In this tutorial, we will first implement support for setting tags when creating and editing articles.
Step 1: Data Modeling and Relationship Building
Create the Tag Entity
First, let's create a corresponding module and entity for this new concept.
nest generate module tags
nest generate service tags
Create tag.entity.ts
in the src/tags
directory:
// src/tags/tag.entity.ts
import { Entity, Column, PrimaryColumn, ManyToMany } from 'typeorm';
import { Post } from '../posts/post.entity';
@Entity()
export class Tag {
@PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' })
id: string;
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}
Update the Post Entity to Establish the Relationship
Next, we need to update the src/posts/post.entity.ts
file to establish the association between posts and tags.
A post can have multiple tags, and a tag can be associated with multiple posts. This is a Many-to-Many relationship.
// src/posts/post.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
import { Tag } from '../tags/tag.entity';
// ... other imports
@Entity()
export class Post {
// ... other fields (id, title, content, etc.)
@ManyToMany(() => Tag, (tag) => tag.posts, {
cascade: true, // Allows creating new tags via a post
})
@JoinTable() // @JoinTable must be specified on one side of the relation
tags: Tag[];
// ... other relations (comments, views, etc.)
}
@JoinTable()
is a decorator required to define a many-to-many relationship. You need to create a join tablepost_tags_tag
to manage the association betweenPost
andTag
.
Update the Database Table Structure
Execute the following SQL statements to create the new tables and fields.
-- Create the tag table
CREATE TABLE "tag" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"name" VARCHAR UNIQUE NOT NULL
);
-- Create the post_tags_tag join table
CREATE TABLE "post_tags_tag" (
"postId" UUID REFERENCES "post" ON DELETE CASCADE,
"tagId" UUID REFERENCES "tag" ON DELETE CASCADE,
PRIMARY KEY ("postId", "tagId")
);
If your database was created on Leapcell,
you can easily execute SQL statements using the graphical interface. Just go to the Database management page on the website, paste the above statements into the SQL interface, and execute them.
Step 2: Implementing Backend Logic
We need to write a Service to handle the creation and finding of tags and update the PostsService
to handle these associations when creating a post.
Writing the TagsService
The logic for this service is relatively simple, mainly focusing on finding or creating tags.
Open src/tags/tags.module.ts
and register TypeOrmModule
.
// src/tags/tags.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tag } from './tag.entity';
import { TagsService } from './tags.service';
@Module({
imports: [TypeOrmModule.forFeature([Tag])],
providers: [TagsService],
exports: [TagsService], // Export the service for other modules to use
})
export class TagsModule {}
In src/tags/tags.service.ts
:
// src/tags/tags.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Tag } from './tag.entity';
@Injectable()
export class TagsService {
constructor(
@InjectRepository(Tag)
private tagsRepository: Repository<Tag>
) {}
// Find or create tags
async findOrCreate(tagNames: string[]): Promise<Tag[]> {
const existingTags = await this.tagsRepository.findBy({ name: In(tagNames) });
const existingTagNames = existingTags.map((tag) => tag.name);
const newTagNames = tagNames.filter((name) => !existingTagNames.includes(name));
const newTags = newTagNames.map((name) => this.tagsRepository.create({ name }));
await this.tagsRepository.save(newTags);
return [...existingTags, ...newTags];
}
}
Update PostsService to Handle the Association
We need to modify the create
and findOne
methods.
First, import TagsModule
into PostsModule
.
// src/posts/posts.module.ts
import { TagsModule } from '../tags/tags.module';
@Module({
imports: [TypeOrmModule.forFeature([Post]), CommentsModule, TrackingModule, TagsModule],
// ...
})
export class PostsModule {}
Then update src/posts/posts.service.ts
:
// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { TagsService } from '../tags/tags.service';
// ... other imports
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private postsRepository: Repository<Post>,
private readonly tagsService: TagsService // Inject TagsService
) {}
// Update the create method
async create(post: Omit<Partial<Post>, 'tags'> & { tags: string }): Promise<Post> {
const tagNames = post.tags
.split(',')
.map((tag) => tag.trim())
.filter(Boolean);
const tags = await this.tagsService.findOrCreate(tagNames);
const newPost = this.postsRepository.create({
...post,
tags,
});
return this.postsRepository.save(newPost);
}
// Update the findOne method to load related data
findOne(id: string): Promise<Post | null> {
return this.postsRepository.findOne({
where: { id },
relations: ['tags'], // Load tags
});
}
// ... other methods
}
Step 3: Frontend Page Integration
The final step is to modify the EJS templates to support setting tags when creating and editing posts, and to display tags on the post detail page.
Update the New/Edit Post Page
Open views/new-post.ejs
and add a form field for entering tags.
<form action="/posts" method="POST" class="post-form">
<div class="form-group">
<label for="tags">Tags (comma-separated)</label>
<input type="text" id="tags" name="tags" />
</div>
<button type="submit">Submit</button>
</form>
For simplicity, we are using comma-separated input for multiple tags here. In a real-world project, you could use more complex UI components and logic, such as a dedicated tag input component, auto-matching existing tags, etc., to improve the user experience.
Update the Post Detail Page
Open views/post.ejs
and display the tags in the post's metadata.
<article class="post-detail">
<h1><%= post.title %></h1>
<small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= viewCount %> </small>
<div class="post-content"><%- post.content %></div>
<% if (post.tags && post.tags.length > 0) { %>
<div class="tags-section">
<strong>Tags:</strong>
<% post.tags.forEach(tag => { %>
<a href="/tags/<%= tag.id %>" class="tag-item"><%= tag.name %></a>
<% }) %>
</div>
<% } %>
</article>
Running and Testing
Restart your application:
npm run start:dev
Open your browser and go to: http://localhost:3000/
Create a new post, and you will see the tag input box at the bottom.
Enter some tags, separated by commas, for example, Nest.js, Tutorial
, and then submit.
After submitting, go to the post detail page, and you will see that the post's tags are now displayed successfully.
Your blog now supports the creation and display of tags. However, users still can't filter articles by tags. We will implement this feature in the next tutorial.
Previous Tutorials:
- Build a Great Nest.js Blog: Visitor Analytics
- Build a Great Nest.js Blog: Full-Text Search for Posts
- Build a Great Nest.js Blog: Upload Image
- Build a Great Nest.js Blog: Reply Comment
- Build a Great Nest.js Blog: Comment System
- Build a Great Nest.js Blog: Add Authorization
- Build a Great Nest.js Blog: Add User System
- 10 Minutes from First Line of Code to Live Deployment: A Super Fast Nest.js Blog Course
Follow us on X: @LeapcellHQ
Related Posts:
Top comments (0)