-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent-collections.ts
More file actions
113 lines (100 loc) · 3.55 KB
/
content-collections.ts
File metadata and controls
113 lines (100 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { defineCollection, defineConfig } from '@content-collections/core'
import { z } from 'zod'
const IMAGE_BASE_URL = 'https://images.hexdrinker.dev'
// reading time 계산 함수
function calculateReadingTime(content: string): string {
const wordsPerMinute = 200
const words = content.trim().split(/\s+/).length
const minutes = Math.ceil(words / wordsPerMinute)
return `${minutes} min read`
}
// excerpt 추출 함수
function extractExcerpt(content: string): string {
const plainText = content
.replace(/```[\s\S]*?```/g, '')
.replace(/`[^`]+`/g, '')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[#*_~`]/g, '')
.replace(/<[^>]*>/g, '')
.trim()
return plainText.substring(0, 200) + '...'
}
function resolveContentImage(src: string | undefined, basePath: string): string | undefined {
if (!src) return src
if (/^(https?:)?\/\//.test(src) || src.startsWith('data:')) {
return src
}
const normalizedBasePath = basePath.replace(/\/$/, '')
const normalizedSrc = src.replace(/^\//, '')
return `${IMAGE_BASE_URL}/${normalizedBasePath}/${normalizedSrc}`
}
const posts = defineCollection({
name: 'posts',
directory: 'content',
include: '**/*.mdx',
exclude: ['**/series/**/index.mdx'],
schema: z.object({
title: z.string(),
description: z.string().optional(),
category: z.string().optional(),
date: z.string(),
tags: z.array(z.coerce.string()).default([]),
draft: z.boolean().default(false),
thumbnail: z.string().optional(),
series: z.string().optional(),
seriesOrder: z.number().optional(),
}),
transform: (document) => {
// 파일 경로에서 category와 slug 추출
// _meta.path는 "tech/shadow-dom" 또는 "series/clean-architecture/01-intro" 형식
const pathParts = document._meta.path.split('/')
const pathCategory = pathParts[0]
const slug = pathParts[pathParts.length - 1]
// series 폴더 구조인 경우: series/[시리즈명]/[파일명]
// 시리즈명을 폴더에서 자동 추출
const isSeriesPost = pathCategory === 'series' && pathParts.length >= 3
const seriesFromPath = isSeriesPost ? pathParts[1] : undefined
const category = isSeriesPost ? 'series' : document.category || pathCategory
const imageBasePath = isSeriesPost
? `series/${pathParts[1]}/${slug}`
: `${pathCategory}/${slug}`
return {
...document,
category,
slug,
imageBasePath,
thumbnail: resolveContentImage(document.thumbnail, imageBasePath),
// series 폴더 구조면 폴더명에서 시리즈 추출, 아니면 frontmatter에서
series: seriesFromPath || document.series,
permalink: isSeriesPost
? `/series/${pathParts[1]}/${slug}`
: `/${category}/${slug}`,
readingTime: calculateReadingTime(document.content),
excerpt: extractExcerpt(document.content),
}
},
})
// 시리즈 메타 정보 컬렉션
const seriesMeta = defineCollection({
name: 'seriesMeta',
directory: 'content',
include: 'series/*/index.mdx',
schema: z.object({
title: z.string(),
description: z.string().optional(),
thumbnail: z.string().optional().nullable(),
}),
transform: (document) => {
// _meta.path는 "series/clean-architecture-deep-dive/index" 형식
const pathParts = document._meta.path.split('/')
const slug = pathParts[1] // series 다음의 폴더명
return {
...document,
slug,
thumbnail: resolveContentImage(document.thumbnail ?? undefined, `series/${slug}`),
}
},
})
export default defineConfig({
collections: [posts, seriesMeta],
})