優れたNest.jsブログを構築する:訪問者分析
Min-jun Kim
Dev Intern · Leapcell

前のチュートリアルでは、ブログに全文検索機能を統合し、良質なコンテンツを見つけやすくしました。
現在、ブログの機能が豊富になり、コンテンツが増えるにつれて、「どの記事が読者に最も人気があるのか?」という新たな疑問が自然に生じます。
読者の興味を理解することは、より質の高いコンテンツを作成するのに役立ちます。
そのため、このチュートリアルでは、ブログに基本的でありながら非常に重要な機能、訪問者追跡を追加します。各記事が読まれた回数を記録し、ページにビュー数を表示します。
Google Analyticsのようなサードパーティサービスの使用を検討するかもしれません。しかし、バックエンド駆動の追跡システムを自分で構築することで、より多くのデータを自分たちの手元に置き、収集したいデータをカスタマイズできます。
始めましょう:
ステップ1:ビューレコードのデータモデルを作成する
データベーステーブルの作成
次のSQLステートメントをPostgreSQLデータベースで実行し、page_view
テーブルを作成します。このテーブルは、各ビューの時刻、対応する投稿、および将来の詳細分析のための訪問者情報(IPアドレスやUser-Agentなど)を記録します。
CREATE TABLE "page_view" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "postId" UUID REFERENCES "post"("id") ON DELETE CASCADE, "ipAddress" VARCHAR(45), "userAgent" TEXT );
ON DELETE CASCADE
は、投稿が削除されたときに、関連するすべてのビューレコードも自動的にクリアされることを保証します。
データベースがLeapcellで作成された場合、
グラフィカルインターフェースを使用してSQLステートメントを簡単に実行できます。ウェブサイトのデータベース管理ページにアクセスし、上記のステートメントをSQLインターフェースに貼り付けて実行するだけです。
PageViewエンティティの作成
次に、この追跡機能の新しいモジュールを作成しましょう。
nest generate module tracking nest generate service tracking
src/tracking
ディレクトリにpage-view.entity.ts
ファイルを作成します:
// src/tracking/page-view.entity.ts import { Entity, PrimaryColumn, CreateDateColumn, ManyToOne, Column } from 'typeorm'; import { Post } from '../posts/post.entity'; @Entity() export class PageView { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => Post, { onDelete: 'CASCADE' }) post: Post; @Column({ type: 'varchar', length: 45, nullable: true }) ipAddress: string; @Column({ type: 'text', nullable: true }) userAgent: string; }
ステップ2:追跡サービスの 実装
TrackingService
は、新しいビューの記録やビュー数のクエリを含む、ビューレコードに関連するすべてのロジックを処理する責任を負います。
PageViewエンティティの登録
src/tracking/tracking.module.ts
を開き、TypeOrmModule
を登録し、TrackingService
をエクスポートします。
// src/tracking/tracking.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TrackingService } from './tracking.service'; import { PageView } from './page-view.entity'; @Module({ imports: [TypeOrmModule.forFeature([PageView])], providers: [TrackingService], exports: [TrackingService], }) export class TrackingModule {}
サービスロジックの記述
src/tracking/tracking.service.ts
を変更して、記録およびクエリ用のメソッドを追加します。
// src/tracking/tracking.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PageView } from './page-view.entity'; import { Post } from '../posts/post.entity'; @Injectable() export class TrackingService { constructor( @InjectRepository(PageView) private pageViewRepository: Repository<PageView> ) {} async recordView(post: Post, ipAddress: string, userAgent: string): Promise<void> { const newView = this.pageViewRepository.create({ post, ipAddress, userAgent, }); await this.pageViewRepository.save(newView); } async getCountByPostId(postId: string): Promise<number> { return this.pageViewRepository.count({ where: { post: { id: postId } }, }); } // For efficiency, fetch view counts for multiple posts at once async getCountsByPostIds(postIds: string[]): Promise<Record<string, number>> { if (postIds.length === 0) { return {}; } const counts = await this.pageViewRepository .createQueryBuilder('page_view') .select('page_view.postId', 'postId') .addSelect('COUNT(*)', 'count') .where('page_view.postId IN (:...postIds)', { postIds }) .groupBy('page_view.postId') .getRawMany(); const result: Record<string, number> = {}; counts.forEach((item) => { result[item.postId] = parseInt(item.count, 10); }); return result; } }
getCountsByPostIds
メソッドは、TypeORMのQueryBuilder
を使用してSQLを直接記述し、より効率的なGROUP BY
クエリを可能にします。これは、ホームページで多くの記事のビュー数を表示する必要がある場合に、各投稿に対して個別のcount
クエリを実行するよりもはるかに高速です。
ステップ3:投稿ページでのビュー記録の統合
次に、訪問者が記事を表示するたびにTrackingService
のrecordView
メソッドを呼び出す必要があります。これに最も適した場所は、ブログ記事の内容を取得するPostsController
のpost
メソッドです。
まず、src/posts/posts.module.ts
にTrackingModule
をインポートします。
// src/posts/posts.module.ts // ... import { TrackingModule } from '../tracking/tracking.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule, TrackingModule], // Import TrackingModule controllers: [PostsController], providers: [PostsService], }) export class PostsModule {}
次に、PostsController
にTrackingService
を注入して呼び出します。
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Request } from '@nestjs/common'; import { PostsService } from './posts.service'; import { CommentsService } from '../comments/comments.service'; import { TrackingService } from '../tracking/tracking.service'; // Import import { marked } from 'marked'; @Controller('posts') export class PostsController { constructor( private readonly postsService: PostsService, private readonly commentsService: CommentsService, private readonly trackingService: TrackingService // Inject ) {} // ... other methods @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); const comments = await this.commentsService.findByPostId(id); const viewCount = await this.trackingService.getCountByPostId(id); if (post) { // Record a view this.trackingService.recordView(post, req.ip, req.headers['user-agent']); post.content = marked.parse(post.content) as string; } return { post, user: req.session.user, comments, viewCount }; } }
注: recordView
メソッドにはawait
を使用しませんでした。これは「fire-and-forget」呼び出しです。ビューの記録という二次的な操作のために、応答を遅延させたくありません。記録が成功したかどうかに関わらず、記事ページはできるだけ早く読者に返されるべきです。
ステップ4:フロントエンドでのビュー数の表示
投稿詳細ページ
前のステップで、already viewCount
を取得し、post.ejs
テンプレートに渡しました。今度はテンプレートに表示するだけです。
views/post.ejs
を開き、投稿のメタ情報エリアにビュー数を追加します:
<article class="post-detail"> <h1><%= post.title %></h1> <small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= viewCount %> </small> <div class="post-content"><%- post.content %></div> </article>
ブログホームページ
ホームページの投稿リストにもビュー数を表示するには、PostsService
とPostsController
にいくつかの調整を加える必要があります。
** src/posts/posts.service.ts
を更新:**
ビュー数とともに投稿リストを取得する新しいメソッドを作成します。
// src/posts/posts.service.ts @Injectable() export class PostsService { // ... constructor // Interface to combine Post with its view count public async findAllWithViewCount(trackingService: TrackingService): Promise<any[]> { const posts = await this.findAll(); const postIds = posts.map((post) => post.id); const viewCounts = await trackingService.getCountsByPostIds(postIds); return posts.map((post) => ({ ...post, viewCount: viewCounts[post.id] || 0, })); } // ... other methods }
** src/posts/posts.controller.ts
を更新:**
ルートメソッドを、新しく作成したサービスメソッドを使用するように変更します。
// src/posts/posts.controller.ts // ... export class PostsController { // ... constructor @Get() @Render('index') async root(@Request() req) { const posts = await this.postsService.findAllWithViewCount(this.trackingService); return { posts, user: req.session.user }; } // ... }
最後に、views/index.ejs
テンプレートを更新してビュー数を表示します。
<div class="post-list"> <% posts.forEach(post => { %> <article class="post-item"> <h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2> <p><%= post.content.substring(0, 150) %>...</p> <small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= post.viewCount %></small> </article> <% }) %> </div>
実行とテスト
アプリケーションを再起動します。
npm run start:dev
ブラウザを開き、次のページにアクセスします。http://localhost:3000/
ブログリストには、各記事の横に「Views: 0」が表示されます。
いずれかの記事の詳細ページをクリックしてアクセスし、ページを数回更新します。この記事のビュー数がそれに応じて増加していることに気づくでしょう。
結論
これで、Nest.jsブログにバックエンドビュー数追跡システムを正常に追加しました。ユーザーの訪問データは、あなたの管理下に置かれました。
この生データを使用して、より詳細なデータ操作と分析を実行できます。たとえば:
- 重複排除:一定期間(例:1日)内の同じIPアドレスからの複数回の訪問を1回のビューとしてカウントします。
- ボットフィルタリング:
User-Agent
を分析して、検索エンジンのクローラーからの訪問を特定し、除外します。 - データダッシュボード:チャートを使用して記事のビュー傾向を視覚化するプライベートページを作成します。
データはすべてあなたのものなので、これらの探求はあなたに委ねます。
ブログがLeapcellにデプロイされている場合、LeapcellはすでにWeb Analytics機能を自動的に有効にしています(これは完全に無料です)。
LeapcellのWeb Analyticsには、実用的で強力な訪問者分析機能が多数含まれています。自分で開発する手間なく、訪問者の行動の基本的な分析を簡単に実行できます。
過去のチュートリアル: