@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.
- package/astro/BlogArchive.astro +16 -22
- package/astro/BlogAuthorPage.astro +19 -139
- package/astro/BlogAuthorsIndex.astro +15 -37
- package/astro/BlogEntry.astro +14 -84
- package/astro/BlogIndex.astro +31 -40
- package/astro/BlogIndexPaginated.astro +23 -56
- package/astro/BlogTagPage.astro +17 -66
- package/astro/BlogTagsIndex.astro +17 -22
- package/astro/Layout.astro +10 -3
- package/astro/pages/BlogArchive.astro +22 -0
- package/astro/pages/BlogAuthorPage.astro +30 -0
- package/astro/pages/BlogAuthorsIndex.astro +23 -0
- package/astro/pages/BlogEntry.astro +36 -0
- package/astro/pages/BlogIndex.astro +22 -0
- package/astro/pages/BlogIndexPaginated.astro +29 -0
- package/astro/pages/BlogTagPage.astro +24 -0
- package/astro/pages/BlogTagsIndex.astro +22 -0
- package/astro/pages/feeds/atom.xml.ts +28 -0
- package/astro/pages/feeds/feed.json.ts +28 -0
- package/astro/pages/feeds/rss.xml.ts +28 -0
- package/package.json +5 -3
- package/src/feeds.ts +286 -0
- package/src/index.ts +82 -72
- package/src/staticPaths.ts +299 -0
- package/src/virtual.d.ts +6 -0
|
@@ -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
|
+
}
|