인기 페이지 통계와 추천 사이트 메타데이터 추가 v1.5.9

This commit is contained in:
2026-05-27 10:34:07 +09:00
parent d7a3149ea1
commit fd9416c0e4
22 changed files with 596 additions and 94 deletions

View File

@@ -4,6 +4,8 @@ export const adminNavigationItemInputSchema = z.object({
id: z.string().uuid(),
label: z.string().trim().min(1),
url: z.string().trim().min(1).regex(/^(\/|https?:\/\/|#)/),
descriptionText: z.string().trim().max(200).optional().default(''),
thumbnailUrl: z.string().trim().max(500).optional().default(''),
location: z.enum(['primary', 'footer', 'recommended']),
sortOrder: z.coerce.number().int().min(0).default(0),
isVisible: z.boolean().default(true),

View File

@@ -2,9 +2,10 @@ import { z } from 'zod'
import {
isBotUserAgent,
isTrackableAnalyticsPath,
normalizePageSlugForAnalytics,
normalizePostSlugForAnalytics
} from '../../lib/analytics.js'
import { getPostBySlug } from '../repositories/content-repository.js'
import { getPageBySlug, getPostBySlug } from '../repositories/content-repository.js'
import {
createSessionHashFromEvent,
recordAnalyticsHeartbeat
@@ -13,6 +14,7 @@ import {
const heartbeatInputSchema = z.object({
path: z.string().trim().min(1).max(500),
postSlug: z.string().trim().max(200).optional().default(''),
pageSlug: z.string().trim().max(200).optional().default(''),
clientSessionId: z.string().trim().min(8).max(120),
durationSeconds: z.number().int().min(0).max(1800),
maxScrollRatio: z.number().min(0).max(1)
@@ -45,7 +47,9 @@ export const handleAnalyticsHeartbeat = async (event) => {
}
const postSlug = normalizePostSlugForAnalytics(body.postSlug)
const pageSlug = normalizePageSlugForAnalytics(body.pageSlug)
let postId = null
let pageId = null
if (postSlug) {
const post = await getPostBySlug(postSlug)
@@ -55,6 +59,14 @@ export const handleAnalyticsHeartbeat = async (event) => {
postId = post.id
}
if (!postId && pageSlug) {
const page = await getPageBySlug(pageSlug)
if (!page) {
return { ok: true }
}
pageId = page.id
}
const sessionHash = createSessionHashFromEvent(event, body.clientSessionId)
await recordAnalyticsHeartbeat({
@@ -63,6 +75,8 @@ export const handleAnalyticsHeartbeat = async (event) => {
path: body.path,
postId,
postSlug,
pageId,
pageSlug,
durationSeconds: body.durationSeconds,
maxScrollRatio: body.maxScrollRatio
})

View File

@@ -2,9 +2,10 @@ import { z } from 'zod'
import {
isBotUserAgent,
isTrackableAnalyticsPath,
normalizePageSlugForAnalytics,
normalizePostSlugForAnalytics
} from '../../lib/analytics.js'
import { getPostBySlug } from '../repositories/content-repository.js'
import { getPageBySlug, getPostBySlug } from '../repositories/content-repository.js'
import {
createVisitorHashFromEvent,
recordAnalyticsPageview
@@ -13,6 +14,7 @@ import {
const pageviewInputSchema = z.object({
path: z.string().trim().min(1).max(500),
postSlug: z.string().trim().max(200).optional().default(''),
pageSlug: z.string().trim().max(200).optional().default(''),
read: z.boolean().optional().default(false)
})
@@ -43,7 +45,9 @@ export const handleAnalyticsPageview = async (event) => {
}
const postSlug = normalizePostSlugForAnalytics(body.postSlug)
const pageSlug = normalizePageSlugForAnalytics(body.pageSlug)
let postId = null
let pageId = null
if (postSlug) {
const post = await getPostBySlug(postSlug)
@@ -53,14 +57,23 @@ export const handleAnalyticsPageview = async (event) => {
postId = post.id
}
if (!postId && pageSlug) {
const page = await getPageBySlug(pageSlug)
if (!page) {
return { ok: true }
}
pageId = page.id
}
const visitorHash = createVisitorHashFromEvent(event)
const isReadEvent = Boolean(body.read)
await recordAnalyticsPageview({
visitorHash,
postId,
pageId,
recordSite: !isReadEvent,
recordView: Boolean(postId) && !isReadEvent,
recordView: (Boolean(postId) || Boolean(pageId)) && !isReadEvent,
recordRead: Boolean(postId) && isReadEvent
})