@levino/shipyard-blog 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ import type { BlogConfig } from './index'
2
+
3
+ interface BlogPost {
4
+ id: string
5
+ data: {
6
+ date: Date
7
+ title: string
8
+ draft?: boolean
9
+ unlisted?: boolean
10
+ tags?: string[]
11
+ authors?: string | { name: string } | (string | { name: string })[]
12
+ sidebar?: { label?: string }
13
+ }
14
+ body?: string
15
+ filePath?: string
16
+ }
17
+
18
+ interface I18nConfig {
19
+ locales: (string | { codes: string[] })[]
20
+ }
21
+
22
+ interface BlogConfigForPaths {
23
+ routeBasePath: string
24
+ includeDraftsInDev: boolean
25
+ postsPerPage: number
26
+ archiveEnabled?: boolean
27
+ authorsEnabled?: boolean
28
+ }
29
+
30
+ function shouldIncludePost(
31
+ post: BlogPost,
32
+ isDev: boolean,
33
+ includeDraftsInDev: boolean,
34
+ ): boolean {
35
+ if (post.data.draft && !(isDev && includeDraftsInDev)) return false
36
+ return true
37
+ }
38
+
39
+ function shouldIncludeInListing(
40
+ post: BlogPost,
41
+ isDev: boolean,
42
+ includeDraftsInDev: boolean,
43
+ ): boolean {
44
+ if (post.data.unlisted) return false
45
+ return shouldIncludePost(post, isDev, includeDraftsInDev)
46
+ }
47
+
48
+ /**
49
+ * Simple locale-based static paths (used by BlogIndex, BlogTagsIndex, BlogArchive, BlogAuthorsIndex).
50
+ */
51
+ export function getLocalePaths(i18n: I18nConfig | null | undefined | false) {
52
+ if (i18n) {
53
+ return i18n.locales.map((locale) => {
54
+ if (typeof locale !== 'string') {
55
+ throw new Error('shipyard does only support strings as locales.')
56
+ }
57
+ return { params: { locale } }
58
+ })
59
+ }
60
+ return [{ params: {} }]
61
+ }
62
+
63
+ /**
64
+ * Compute static paths for BlogEntry (one path per blog post with prev/next pagination).
65
+ */
66
+ export function computeBlogEntryPaths(
67
+ allPosts: BlogPost[],
68
+ blogConfig: BlogConfigForPaths,
69
+ i18n: I18nConfig | null | undefined | false,
70
+ ) {
71
+ const isDev = import.meta.env?.DEV ?? false
72
+ const blogPosts = allPosts.filter((post) =>
73
+ shouldIncludePost(post, isDev, blogConfig.includeDraftsInDev),
74
+ )
75
+ const sortedPosts = blogPosts.toSorted(
76
+ (a, b) => b.data.date.getTime() - a.data.date.getTime(),
77
+ )
78
+
79
+ const getParams = (slug: string) => {
80
+ if (i18n) {
81
+ const [locale, ...rest] = slug.split('/')
82
+ return { slug: rest.join('/'), locale }
83
+ }
84
+ return { slug }
85
+ }
86
+
87
+ const getPostUrl = (post: BlogPost) => {
88
+ const { routeBasePath } = blogConfig
89
+ if (i18n) {
90
+ const [locale, ...rest] = post.id.split('/')
91
+ return `/${locale}/${routeBasePath}/${rest.join('/')}`
92
+ }
93
+ return `/${routeBasePath}/${post.id}`
94
+ }
95
+
96
+ return sortedPosts.map((entry) => {
97
+ let localePosts = sortedPosts
98
+ if (i18n) {
99
+ const [locale] = entry.id.split('/')
100
+ localePosts = sortedPosts.filter((post) =>
101
+ post.id.startsWith(`${locale}/`),
102
+ )
103
+ }
104
+
105
+ const localeIndex = localePosts.findIndex((post) => post.id === entry.id)
106
+ const newerPost = localeIndex > 0 ? localePosts[localeIndex - 1] : undefined
107
+ const olderPost =
108
+ localeIndex < localePosts.length - 1
109
+ ? localePosts[localeIndex + 1]
110
+ : undefined
111
+
112
+ return {
113
+ params: getParams(entry.id),
114
+ props: {
115
+ entry,
116
+ older: olderPost
117
+ ? { href: getPostUrl(olderPost), title: olderPost.data.title }
118
+ : undefined,
119
+ newer: newerPost
120
+ ? { href: getPostUrl(newerPost), title: newerPost.data.title }
121
+ : undefined,
122
+ },
123
+ }
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Compute static paths for BlogIndexPaginated (one path per page, starting from page 2).
129
+ */
130
+ export function computeBlogPaginatedPaths(
131
+ allPosts: BlogPost[],
132
+ blogConfig: BlogConfigForPaths,
133
+ i18n: I18nConfig | null | undefined | false,
134
+ ) {
135
+ const isDev = import.meta.env?.DEV ?? false
136
+ const listedPosts = allPosts.filter((post) =>
137
+ shouldIncludeInListing(post, isDev, blogConfig.includeDraftsInDev),
138
+ )
139
+
140
+ if (i18n) {
141
+ const paths: { params: { locale: string; page: string } }[] = []
142
+ for (const locale of i18n.locales) {
143
+ if (typeof locale !== 'string') {
144
+ throw new Error('shipyard does only support strings as locales.')
145
+ }
146
+ const localePosts = listedPosts.filter(({ id }) => {
147
+ const [postLocale] = id.split('/')
148
+ return postLocale === locale
149
+ })
150
+ const totalPages = Math.ceil(localePosts.length / blogConfig.postsPerPage)
151
+ for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
152
+ paths.push({ params: { locale, page: String(pageNum) } })
153
+ }
154
+ }
155
+ return paths
156
+ }
157
+
158
+ const totalPages = Math.ceil(listedPosts.length / blogConfig.postsPerPage)
159
+ const paths: { params: { page: string } }[] = []
160
+ for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
161
+ paths.push({ params: { page: String(pageNum) } })
162
+ }
163
+ return paths
164
+ }
165
+
166
+ /**
167
+ * Compute static paths for BlogTagPage (one path per tag per locale).
168
+ */
169
+ export function computeBlogTagPaths(
170
+ allPosts: BlogPost[],
171
+ blogConfig: BlogConfigForPaths,
172
+ i18n: I18nConfig | null | undefined | false,
173
+ ) {
174
+ const isDev = import.meta.env?.DEV ?? false
175
+
176
+ const getAllTags = (posts: BlogPost[]) => {
177
+ const filtered = posts.filter((p) =>
178
+ shouldIncludeInListing(p, isDev, blogConfig.includeDraftsInDev),
179
+ )
180
+ const tags = filtered.flatMap((post) => post.data.tags ?? [])
181
+ return [...new Set(tags.map((t) => t.toLowerCase()))]
182
+ }
183
+
184
+ if (i18n) {
185
+ const paths: { params: { locale: string; tag: string } }[] = []
186
+ for (const locale of i18n.locales) {
187
+ if (typeof locale !== 'string') {
188
+ throw new Error('shipyard does only support strings as locales.')
189
+ }
190
+ const localePosts = allPosts.filter((post) => {
191
+ const [postLocale] = post.id.split('/')
192
+ return postLocale === locale
193
+ })
194
+ const localeTags = getAllTags(localePosts)
195
+ for (const tag of localeTags) {
196
+ paths.push({ params: { locale, tag } })
197
+ }
198
+ }
199
+ return paths
200
+ }
201
+
202
+ const allTags = getAllTags(allPosts)
203
+ return allTags.map((tag) => ({ params: { tag } }))
204
+ }
205
+
206
+ /**
207
+ * Compute static paths for BlogAuthorPage (one path per author per locale).
208
+ */
209
+ export function computeBlogAuthorPaths(
210
+ allPosts: BlogPost[],
211
+ blogConfig: BlogConfigForPaths,
212
+ i18n: I18nConfig | null | undefined | false,
213
+ ) {
214
+ if (!blogConfig.authorsEnabled) return []
215
+
216
+ const isDev = import.meta.env?.DEV ?? false
217
+
218
+ const normalizeAuthor = (
219
+ author: string | { name: string },
220
+ ): { name: string } => {
221
+ if (typeof author === 'string') return { name: author }
222
+ return author
223
+ }
224
+
225
+ const getAuthorsFromPost = (post: BlogPost) => {
226
+ const authors = post.data.authors
227
+ if (!authors) return []
228
+ if (Array.isArray(authors)) return authors.map(normalizeAuthor)
229
+ return [normalizeAuthor(authors)]
230
+ }
231
+
232
+ const getAuthorSlug = (name: string) =>
233
+ name.toLowerCase().replace(/\s+/g, '-')
234
+
235
+ const getUniqueAuthors = (posts: BlogPost[]) => {
236
+ const filtered = posts.filter((p) =>
237
+ shouldIncludeInListing(p, isDev, blogConfig.includeDraftsInDev),
238
+ )
239
+ const allAuthors = filtered.flatMap(getAuthorsFromPost)
240
+ const seen = new Set<string>()
241
+ return allAuthors.filter((a) => {
242
+ if (seen.has(a.name)) return false
243
+ seen.add(a.name)
244
+ return true
245
+ })
246
+ }
247
+
248
+ if (i18n) {
249
+ const paths: {
250
+ params: { locale: string; author: string }
251
+ props: { author: { name: string } }
252
+ }[] = []
253
+
254
+ for (const locale of i18n.locales) {
255
+ if (typeof locale !== 'string') {
256
+ throw new Error('shipyard does only support strings as locales.')
257
+ }
258
+ const localePosts = allPosts.filter((post) => {
259
+ const [postLocale] = post.id.split('/')
260
+ return postLocale === locale
261
+ })
262
+ const authors = getUniqueAuthors(localePosts)
263
+ for (const author of authors) {
264
+ paths.push({
265
+ params: { locale, author: getAuthorSlug(author.name) },
266
+ props: { author },
267
+ })
268
+ }
269
+ }
270
+ return paths
271
+ }
272
+
273
+ const authors = getUniqueAuthors(allPosts)
274
+ return authors.map((author) => ({
275
+ params: { author: getAuthorSlug(author.name) },
276
+ props: { author },
277
+ }))
278
+ }
279
+
280
+ export type InstanceConfig = {
281
+ blogConfig: BlogConfig
282
+ tagsMap: Record<string, unknown>
283
+ collectionName: string
284
+ }
285
+
286
+ export type Registry = Record<string, InstanceConfig>
287
+
288
+ export const getInstanceConfig = (
289
+ routePattern: string,
290
+ registry: Registry,
291
+ ): InstanceConfig => {
292
+ const stripped = routePattern.replace(/^\/?(\[locale\]\/)?/, '')
293
+ for (const [basePath, config] of Object.entries(registry)) {
294
+ if (stripped === basePath || stripped.startsWith(`${basePath}/`)) {
295
+ return config
296
+ }
297
+ }
298
+ throw new Error(`No blog instance found for route pattern: ${routePattern}`)
299
+ }
package/src/virtual.d.ts CHANGED
@@ -9,3 +9,9 @@ declare module 'virtual:shipyard-blog/tags' {
9
9
  const tagsMap: TagsMap
10
10
  export default tagsMap
11
11
  }
12
+
13
+ declare module 'virtual:shipyard-blog/registry' {
14
+ import type { Registry } from './staticPaths'
15
+ const registry: Registry
16
+ export default registry
17
+ }