@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
package/astro/BlogArchive.astro
CHANGED
|
@@ -3,36 +3,30 @@
|
|
|
3
3
|
* Blog archive page - shows all posts organized by year.
|
|
4
4
|
*/
|
|
5
5
|
import { i18n } from 'astro:config/server'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
type CollectionEntry,
|
|
8
|
+
type CollectionKey,
|
|
9
|
+
getCollection,
|
|
10
|
+
} from 'astro:content'
|
|
11
|
+
import blogConfigDefault from 'virtual:shipyard-blog/config'
|
|
9
12
|
import { groupBy, map, pipe, prop, reverse, sortBy, toPairs } from 'ramda'
|
|
10
13
|
import Layout from './Layout.astro'
|
|
11
14
|
|
|
15
|
+
const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
|
|
16
|
+
const collectionName =
|
|
17
|
+
Astro.props.collectionName ?? blogConfigDefault.collectionName
|
|
18
|
+
|
|
12
19
|
const { includeDraftsInDev, routeBasePath } = blogConfig
|
|
13
20
|
|
|
14
21
|
// Filter out draft and unlisted posts
|
|
15
22
|
const isDev = import.meta.env.DEV
|
|
16
|
-
const shouldIncludePost = (post: CollectionEntry<
|
|
23
|
+
const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
|
|
17
24
|
if (post.data.unlisted) return false
|
|
18
25
|
if (post.data.draft && !(isDev && includeDraftsInDev)) return false
|
|
19
26
|
return true
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
if (i18n) {
|
|
24
|
-
return i18n.locales.map((locale) => {
|
|
25
|
-
if (typeof locale !== 'string') {
|
|
26
|
-
throw new Error('shipyard does only support strings as locales.')
|
|
27
|
-
}
|
|
28
|
-
return { params: { locale } }
|
|
29
|
-
})
|
|
30
|
-
} else {
|
|
31
|
-
return [{ params: {} }]
|
|
32
|
-
}
|
|
33
|
-
}) satisfies GetStaticPaths
|
|
34
|
-
|
|
35
|
-
const allPosts = await getCollection('blog')
|
|
29
|
+
const allPosts = await getCollection(collectionName as CollectionKey)
|
|
36
30
|
const filteredPosts = allPosts.filter(shouldIncludePost).filter(({ id }) => {
|
|
37
31
|
if (i18n) {
|
|
38
32
|
const [postLocale] = id.split('/')
|
|
@@ -49,10 +43,10 @@ const sortedPosts = filteredPosts.toSorted(
|
|
|
49
43
|
// Group posts by year
|
|
50
44
|
type YearPosts = {
|
|
51
45
|
year: string
|
|
52
|
-
posts: CollectionEntry<
|
|
46
|
+
posts: CollectionEntry<CollectionKey>[]
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
const getYearFromPost = (post: CollectionEntry<
|
|
49
|
+
const getYearFromPost = (post: CollectionEntry<CollectionKey>): string =>
|
|
56
50
|
post.data.date.getFullYear().toString()
|
|
57
51
|
|
|
58
52
|
const groupPostsByYear = pipe(
|
|
@@ -61,7 +55,7 @@ const groupPostsByYear = pipe(
|
|
|
61
55
|
map(([year, posts]): YearPosts => ({ year, posts: posts ?? [] })),
|
|
62
56
|
sortBy(prop('year')),
|
|
63
57
|
reverse,
|
|
64
|
-
) as (posts: CollectionEntry<
|
|
58
|
+
) as (posts: CollectionEntry<CollectionKey>[]) => YearPosts[]
|
|
65
59
|
|
|
66
60
|
const postsByYear = groupPostsByYear(sortedPosts)
|
|
67
61
|
|
|
@@ -79,7 +73,7 @@ const getBlogPostLink = (id: string, locale?: string) => {
|
|
|
79
73
|
}
|
|
80
74
|
---
|
|
81
75
|
|
|
82
|
-
<Layout title="Archive">
|
|
76
|
+
<Layout title="Archive" blogConfig={blogConfig} collectionName={collectionName}>
|
|
83
77
|
<div class="max-w-4xl mx-auto">
|
|
84
78
|
<h1 class="text-4xl font-bold mb-8">Archive</h1>
|
|
85
79
|
<p class="text-base-content/60 mb-8">
|
|
@@ -1,149 +1,29 @@
|
|
|
1
1
|
---
|
|
2
|
-
/**
|
|
3
|
-
* Individual author page showing author info and their posts.
|
|
4
|
-
*/
|
|
5
2
|
import { i18n } from 'astro:config/server'
|
|
6
|
-
import { type CollectionEntry, getCollection } from 'astro:content'
|
|
7
|
-
import blogConfig from 'virtual:shipyard-blog/config'
|
|
8
|
-
import Layout from '@levino/shipyard-base/layouts/Page.astro'
|
|
9
3
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
uniqBy,
|
|
18
|
-
} from 'ramda'
|
|
4
|
+
type CollectionEntry,
|
|
5
|
+
type CollectionKey,
|
|
6
|
+
getCollection,
|
|
7
|
+
} from 'astro:content'
|
|
8
|
+
import blogConfigDefault from 'virtual:shipyard-blog/config'
|
|
9
|
+
import Layout from '@levino/shipyard-base/layouts/Page.astro'
|
|
10
|
+
import { filter, pipe, reverse, sortBy } from 'ramda'
|
|
19
11
|
import type { Author } from '../src/index'
|
|
20
12
|
import { getReadingTime } from '../src/readingTime'
|
|
21
13
|
|
|
14
|
+
const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
|
|
15
|
+
const collectionName =
|
|
16
|
+
Astro.props.collectionName ?? blogConfigDefault.collectionName
|
|
17
|
+
|
|
22
18
|
const {
|
|
23
19
|
showReadingTime,
|
|
24
20
|
includeDraftsInDev: includeDraftsInDevConfig,
|
|
25
21
|
routeBasePath,
|
|
26
22
|
} = blogConfig
|
|
27
23
|
|
|
28
|
-
export async function getStaticPaths() {
|
|
29
|
-
// Must inline all helper functions and values because Astro hoists getStaticPaths to a separate module
|
|
30
|
-
const { authorsEnabled, includeDraftsInDev } = blogConfig
|
|
31
|
-
const isDev = import.meta.env.DEV
|
|
32
|
-
|
|
33
|
-
const normalizeAuthorInner = (
|
|
34
|
-
author:
|
|
35
|
-
| string
|
|
36
|
-
| {
|
|
37
|
-
name: string
|
|
38
|
-
title?: string
|
|
39
|
-
url?: string
|
|
40
|
-
image_url?: string
|
|
41
|
-
email?: string
|
|
42
|
-
},
|
|
43
|
-
): {
|
|
44
|
-
name: string
|
|
45
|
-
title?: string
|
|
46
|
-
url?: string
|
|
47
|
-
image_url?: string
|
|
48
|
-
email?: string
|
|
49
|
-
} => {
|
|
50
|
-
if (typeof author === 'string') {
|
|
51
|
-
return { name: author }
|
|
52
|
-
}
|
|
53
|
-
return author
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const getAuthorsFromPostInner = (
|
|
57
|
-
post: CollectionEntry<'blog'>,
|
|
58
|
-
): {
|
|
59
|
-
name: string
|
|
60
|
-
title?: string
|
|
61
|
-
url?: string
|
|
62
|
-
image_url?: string
|
|
63
|
-
email?: string
|
|
64
|
-
}[] => {
|
|
65
|
-
const authors = post.data.authors
|
|
66
|
-
if (!authors) return []
|
|
67
|
-
if (Array.isArray(authors)) {
|
|
68
|
-
return authors.map(normalizeAuthorInner)
|
|
69
|
-
}
|
|
70
|
-
return [
|
|
71
|
-
normalizeAuthorInner(
|
|
72
|
-
authors as
|
|
73
|
-
| string
|
|
74
|
-
| {
|
|
75
|
-
name: string
|
|
76
|
-
title?: string
|
|
77
|
-
url?: string
|
|
78
|
-
image_url?: string
|
|
79
|
-
email?: string
|
|
80
|
-
},
|
|
81
|
-
),
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const getAuthorSlugInner = (name: string): string =>
|
|
86
|
-
name.toLowerCase().replace(/\s+/g, '-')
|
|
87
|
-
|
|
88
|
-
const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
|
|
89
|
-
if (post.data.unlisted) return false
|
|
90
|
-
if (post.data.draft && !(isDev && includeDraftsInDev)) return false
|
|
91
|
-
return true
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!authorsEnabled) {
|
|
95
|
-
return []
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const allPosts = await getCollection('blog')
|
|
99
|
-
|
|
100
|
-
// Get all unique authors
|
|
101
|
-
const getAllAuthors = pipe(
|
|
102
|
-
filter(shouldIncludePost),
|
|
103
|
-
map(getAuthorsFromPostInner),
|
|
104
|
-
flatten,
|
|
105
|
-
uniqBy(prop('name')),
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if (i18n) {
|
|
109
|
-
const paths: {
|
|
110
|
-
params: { locale: string; author: string }
|
|
111
|
-
props: { author: Author }
|
|
112
|
-
}[] = []
|
|
113
|
-
|
|
114
|
-
for (const locale of i18n.locales) {
|
|
115
|
-
if (typeof locale !== 'string') {
|
|
116
|
-
throw new Error('shipyard does only support strings as locales.')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const localePosts = allPosts.filter((post) => {
|
|
120
|
-
const [postLocale] = post.id.split('/')
|
|
121
|
-
return postLocale === locale
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const authors = getAllAuthors(localePosts) as Author[]
|
|
125
|
-
|
|
126
|
-
for (const author of authors) {
|
|
127
|
-
paths.push({
|
|
128
|
-
params: { locale, author: getAuthorSlugInner(author.name) },
|
|
129
|
-
props: { author },
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return paths
|
|
135
|
-
} else {
|
|
136
|
-
const authors = getAllAuthors(allPosts) as Author[]
|
|
137
|
-
return authors.map((author) => ({
|
|
138
|
-
params: { author: getAuthorSlugInner(author.name) },
|
|
139
|
-
props: { author },
|
|
140
|
-
}))
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
24
|
const { author } = Astro.props
|
|
145
25
|
|
|
146
|
-
const allPosts = await getCollection(
|
|
26
|
+
const allPosts = await getCollection(collectionName as CollectionKey)
|
|
147
27
|
const isDev = import.meta.env.DEV
|
|
148
28
|
|
|
149
29
|
// Component-level helper functions
|
|
@@ -154,7 +34,7 @@ const normalizeAuthor = (authorArg: string | Author): Author => {
|
|
|
154
34
|
return authorArg
|
|
155
35
|
}
|
|
156
36
|
|
|
157
|
-
const getAuthorsFromPost = (post: CollectionEntry<
|
|
37
|
+
const getAuthorsFromPost = (post: CollectionEntry<CollectionKey>): Author[] => {
|
|
158
38
|
const authors = post.data.authors
|
|
159
39
|
if (!authors) return []
|
|
160
40
|
if (Array.isArray(authors)) {
|
|
@@ -164,7 +44,7 @@ const getAuthorsFromPost = (post: CollectionEntry<'blog'>): Author[] => {
|
|
|
164
44
|
}
|
|
165
45
|
|
|
166
46
|
// Filter posts (component-level)
|
|
167
|
-
const shouldIncludePostComponent = (post: CollectionEntry<
|
|
47
|
+
const shouldIncludePostComponent = (post: CollectionEntry<CollectionKey>) => {
|
|
168
48
|
if (post.data.unlisted) return false
|
|
169
49
|
if (post.data.draft && !(isDev && includeDraftsInDevConfig)) return false
|
|
170
50
|
return true
|
|
@@ -173,23 +53,23 @@ const shouldIncludePostComponent = (post: CollectionEntry<'blog'>) => {
|
|
|
173
53
|
// Get posts by this author
|
|
174
54
|
const authorPosts = pipe(
|
|
175
55
|
filter(shouldIncludePostComponent),
|
|
176
|
-
filter((post: CollectionEntry<
|
|
56
|
+
filter((post: CollectionEntry<CollectionKey>) => {
|
|
177
57
|
if (i18n) {
|
|
178
58
|
const [postLocale] = post.id.split('/')
|
|
179
59
|
return postLocale === Astro.currentLocale
|
|
180
60
|
}
|
|
181
61
|
return true
|
|
182
62
|
}),
|
|
183
|
-
filter((post: CollectionEntry<
|
|
63
|
+
filter((post: CollectionEntry<CollectionKey>) => {
|
|
184
64
|
const authors = getAuthorsFromPost(post)
|
|
185
65
|
return authors.some((a) => a.name === author.name)
|
|
186
66
|
}),
|
|
187
|
-
sortBy((post: CollectionEntry<
|
|
67
|
+
sortBy((post: CollectionEntry<CollectionKey>) => post.data.date.getTime()),
|
|
188
68
|
reverse,
|
|
189
|
-
)(allPosts) as CollectionEntry<
|
|
69
|
+
)(allPosts) as CollectionEntry<CollectionKey>[]
|
|
190
70
|
|
|
191
71
|
// Generate post URL
|
|
192
|
-
const getPostUrl = (post: CollectionEntry<
|
|
72
|
+
const getPostUrl = (post: CollectionEntry<CollectionKey>): string => {
|
|
193
73
|
if (i18n && Astro.currentLocale) {
|
|
194
74
|
const slug = post.id.replace(`${Astro.currentLocale}/`, '')
|
|
195
75
|
return `/${Astro.currentLocale}/${routeBasePath}/${slug}`
|
|
@@ -3,48 +3,26 @@
|
|
|
3
3
|
* Authors index page showing all blog authors.
|
|
4
4
|
*/
|
|
5
5
|
import { i18n } from 'astro:config/server'
|
|
6
|
-
import { type CollectionEntry, getCollection } from 'astro:content'
|
|
7
|
-
import blogConfig from 'virtual:shipyard-blog/config'
|
|
8
|
-
import Layout from '@levino/shipyard-base/layouts/Page.astro'
|
|
9
6
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
sortBy,
|
|
18
|
-
uniqBy,
|
|
19
|
-
values,
|
|
20
|
-
} from 'ramda'
|
|
7
|
+
type CollectionEntry,
|
|
8
|
+
type CollectionKey,
|
|
9
|
+
getCollection,
|
|
10
|
+
} from 'astro:content'
|
|
11
|
+
import blogConfigDefault from 'virtual:shipyard-blog/config'
|
|
12
|
+
import Layout from '@levino/shipyard-base/layouts/Page.astro'
|
|
13
|
+
import { filter, flatten, map, pipe, prop, sortBy, uniqBy } from 'ramda'
|
|
21
14
|
import type { Author } from '../src/index'
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!authorsEnabled) {
|
|
27
|
-
return []
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (i18n) {
|
|
31
|
-
return i18n.locales.map((locale) => {
|
|
32
|
-
if (typeof locale !== 'string') {
|
|
33
|
-
throw new Error('shipyard does only support strings as locales.')
|
|
34
|
-
}
|
|
35
|
-
return { params: { locale } }
|
|
36
|
-
})
|
|
37
|
-
} else {
|
|
38
|
-
return [{ params: {} }]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
16
|
+
const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
|
|
17
|
+
const collectionName =
|
|
18
|
+
Astro.props.collectionName ?? blogConfigDefault.collectionName
|
|
41
19
|
|
|
42
|
-
const allPosts = await getCollection(
|
|
20
|
+
const allPosts = await getCollection(collectionName as CollectionKey)
|
|
43
21
|
const { includeDraftsInDev, routeBasePath } = blogConfig
|
|
44
22
|
const isDev = import.meta.env.DEV
|
|
45
23
|
|
|
46
24
|
// Filter posts
|
|
47
|
-
const shouldIncludePost = (post: CollectionEntry<
|
|
25
|
+
const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
|
|
48
26
|
if (post.data.unlisted) return false
|
|
49
27
|
if (post.data.draft && !(isDev && includeDraftsInDev)) return false
|
|
50
28
|
return true
|
|
@@ -52,14 +30,14 @@ const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
|
|
|
52
30
|
|
|
53
31
|
const posts = pipe(
|
|
54
32
|
filter(shouldIncludePost),
|
|
55
|
-
filter((post: CollectionEntry<
|
|
33
|
+
filter((post: CollectionEntry<CollectionKey>) => {
|
|
56
34
|
if (i18n) {
|
|
57
35
|
const [postLocale] = post.id.split('/')
|
|
58
36
|
return postLocale === Astro.currentLocale
|
|
59
37
|
}
|
|
60
38
|
return true
|
|
61
39
|
}),
|
|
62
|
-
)(allPosts) as CollectionEntry<
|
|
40
|
+
)(allPosts) as CollectionEntry<CollectionKey>[]
|
|
63
41
|
|
|
64
42
|
// Extract authors from posts
|
|
65
43
|
const normalizeAuthor = (author: string | Author): Author => {
|
|
@@ -69,7 +47,7 @@ const normalizeAuthor = (author: string | Author): Author => {
|
|
|
69
47
|
return author
|
|
70
48
|
}
|
|
71
49
|
|
|
72
|
-
const getAuthorsFromPost = (post: CollectionEntry<
|
|
50
|
+
const getAuthorsFromPost = (post: CollectionEntry<CollectionKey>): Author[] => {
|
|
73
51
|
const authors = post.data.authors
|
|
74
52
|
if (!authors) return []
|
|
75
53
|
if (Array.isArray(authors)) {
|
package/astro/BlogEntry.astro
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { i18n } from 'astro:config/server'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type CollectionEntry,
|
|
5
|
+
type CollectionKey,
|
|
6
|
+
getCollection,
|
|
7
|
+
render,
|
|
8
|
+
} from 'astro:content'
|
|
9
|
+
import blogConfigDefault from 'virtual:shipyard-blog/config'
|
|
5
10
|
import { TableOfContents } from '@levino/shipyard-base/components'
|
|
6
11
|
import { getEditUrl, getGitMetadata } from '../src/gitMetadata'
|
|
7
12
|
import { type Author, getReadingTime } from '../src/index'
|
|
@@ -12,96 +17,21 @@ import BlogReadingTime from './BlogReadingTime.astro'
|
|
|
12
17
|
import BlogTags from './BlogTags.astro'
|
|
13
18
|
import Layout from './Layout.astro'
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Filter out draft posts for production - defined inside getStaticPaths
|
|
19
|
-
const isDev = import.meta.env.DEV
|
|
20
|
-
const { includeDraftsInDev } = blogConfig
|
|
21
|
-
const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
|
|
22
|
-
if (post.data.draft && !(isDev && includeDraftsInDev)) {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
return true
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const blogPosts = allPosts.filter(shouldIncludePost)
|
|
29
|
-
|
|
30
|
-
// Sort posts by date (newest first)
|
|
31
|
-
const sortedPosts = blogPosts.toSorted(
|
|
32
|
-
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
const getParams = (slug: string) => {
|
|
36
|
-
if (i18n) {
|
|
37
|
-
const [locale, ...rest] = slug.split('/')
|
|
38
|
-
return {
|
|
39
|
-
slug: rest.join('/'),
|
|
40
|
-
locale,
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
// For non-i18n, treat the entire slug as the path
|
|
44
|
-
return {
|
|
45
|
-
slug: slug,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const getPostUrl = (post: CollectionEntry<'blog'>) => {
|
|
51
|
-
const { routeBasePath } = blogConfig
|
|
52
|
-
if (i18n) {
|
|
53
|
-
const [locale, ...rest] = post.id.split('/')
|
|
54
|
-
return `/${locale}/${routeBasePath}/${rest.join('/')}`
|
|
55
|
-
}
|
|
56
|
-
return `/${routeBasePath}/${post.id}`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return sortedPosts.map((entry) => {
|
|
60
|
-
// Filter posts by locale for pagination
|
|
61
|
-
let localePosts = sortedPosts
|
|
62
|
-
if (i18n) {
|
|
63
|
-
const [locale] = entry.id.split('/')
|
|
64
|
-
localePosts = sortedPosts.filter((post) =>
|
|
65
|
-
post.id.startsWith(`${locale}/`),
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Find position within locale-filtered posts
|
|
70
|
-
const localeIndex = localePosts.findIndex((post) => post.id === entry.id)
|
|
71
|
-
|
|
72
|
-
// Newer is previous in the array (lower index), older is next (higher index)
|
|
73
|
-
const newerPost = localeIndex > 0 ? localePosts[localeIndex - 1] : undefined
|
|
74
|
-
const olderPost =
|
|
75
|
-
localeIndex < localePosts.length - 1
|
|
76
|
-
? localePosts[localeIndex + 1]
|
|
77
|
-
: undefined
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
params: getParams(entry.id),
|
|
81
|
-
props: {
|
|
82
|
-
entry,
|
|
83
|
-
older: olderPost
|
|
84
|
-
? { href: getPostUrl(olderPost), title: olderPost.data.title }
|
|
85
|
-
: undefined,
|
|
86
|
-
newer: newerPost
|
|
87
|
-
? { href: getPostUrl(newerPost), title: newerPost.data.title }
|
|
88
|
-
: undefined,
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
}
|
|
20
|
+
const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
|
|
21
|
+
const collectionName =
|
|
22
|
+
Astro.props.collectionName ?? blogConfigDefault.collectionName
|
|
93
23
|
|
|
94
24
|
// In SSR mode (prerender: false), getStaticPaths is not called so Astro.props will be empty.
|
|
95
25
|
// We need to fetch the entry from the collection based on URL params.
|
|
96
26
|
let { entry, older, newer } = Astro.props
|
|
97
27
|
|
|
98
28
|
if (!entry) {
|
|
99
|
-
const allPosts = await getCollection(
|
|
29
|
+
const allPosts = await getCollection(collectionName as CollectionKey)
|
|
100
30
|
|
|
101
31
|
// Filter out draft posts for production
|
|
102
32
|
const isDev = import.meta.env.DEV
|
|
103
33
|
const { includeDraftsInDev } = blogConfig
|
|
104
|
-
const shouldIncludePost = (post: CollectionEntry<
|
|
34
|
+
const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
|
|
105
35
|
if (post.data.draft && !(isDev && includeDraftsInDev)) {
|
|
106
36
|
return false
|
|
107
37
|
}
|
|
@@ -134,7 +64,7 @@ if (!entry) {
|
|
|
134
64
|
|
|
135
65
|
// Calculate pagination for SSR mode
|
|
136
66
|
const { routeBasePath } = blogConfig
|
|
137
|
-
const getPostUrl = (post: CollectionEntry<
|
|
67
|
+
const getPostUrl = (post: CollectionEntry<CollectionKey>) => {
|
|
138
68
|
if (i18n) {
|
|
139
69
|
const [postLocale, ...rest] = post.id.split('/')
|
|
140
70
|
return `/${postLocale}/${routeBasePath}/${rest.join('/')}`
|
|
@@ -279,7 +209,7 @@ const formatDate = new Intl.DateTimeFormat(Astro.currentLocale || 'en', {
|
|
|
279
209
|
const showToc = !hide_table_of_contents && filteredHeadings.length > 0
|
|
280
210
|
---
|
|
281
211
|
|
|
282
|
-
<Layout title={seoTitle} canonicalUrl={canonical_url}>
|
|
212
|
+
<Layout title={seoTitle} canonicalUrl={canonical_url} blogConfig={blogConfig} collectionName={collectionName}>
|
|
283
213
|
<div class="grid grid-cols-12 gap-6 max-w-7xl mx-auto">
|
|
284
214
|
<div class:list={['col-span-12', { 'xl:col-span-9': showToc }]}>
|
|
285
215
|
{/* Post header */}
|
package/astro/BlogIndex.astro
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Image } from 'astro:assets'
|
|
3
3
|
import { i18n } from 'astro:config/server'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
type CollectionEntry,
|
|
6
|
+
type CollectionKey,
|
|
7
|
+
getCollection,
|
|
8
|
+
} from 'astro:content'
|
|
9
|
+
import blogConfigDefault from 'virtual:shipyard-blog/config'
|
|
7
10
|
import { getReadingTime } from '../src/index'
|
|
8
11
|
import BlogReadingTime from './BlogReadingTime.astro'
|
|
9
12
|
import BlogTags from './BlogTags.astro'
|
|
10
13
|
import Layout from './Layout.astro'
|
|
11
14
|
|
|
15
|
+
const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
|
|
16
|
+
const collectionName =
|
|
17
|
+
Astro.props.collectionName ?? blogConfigDefault.collectionName
|
|
18
|
+
|
|
12
19
|
const {
|
|
13
20
|
postsPerPage,
|
|
14
21
|
showReadingTime,
|
|
@@ -19,7 +26,7 @@ const {
|
|
|
19
26
|
|
|
20
27
|
// Filter out draft and unlisted posts for production
|
|
21
28
|
const isDev = import.meta.env.DEV
|
|
22
|
-
const shouldIncludePost = (post: CollectionEntry<
|
|
29
|
+
const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
|
|
23
30
|
// Exclude unlisted posts from listings
|
|
24
31
|
if (post.data.unlisted) return false
|
|
25
32
|
// Include drafts only in dev mode if configured
|
|
@@ -27,45 +34,29 @@ const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
|
|
|
27
34
|
return true
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export const getStaticPaths = (() => {
|
|
31
|
-
if (i18n) {
|
|
32
|
-
return i18n.locales.map((locale) => {
|
|
33
|
-
if (typeof locale !== 'string') {
|
|
34
|
-
throw new Error('shipyard does only support strings as locales.')
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
params: {
|
|
38
|
-
locale,
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
} else {
|
|
43
|
-
// For non-i18n, return a single path without locale
|
|
44
|
-
return [{ params: {} }]
|
|
45
|
-
}
|
|
46
|
-
}) satisfies GetStaticPaths
|
|
47
|
-
|
|
48
37
|
// Page 1 is always the index page (no page param)
|
|
49
38
|
const currentPage = 1
|
|
50
39
|
|
|
51
|
-
const entries = await getCollection(
|
|
52
|
-
|
|
40
|
+
const entries = await getCollection(collectionName as CollectionKey).then(
|
|
41
|
+
(posts) => {
|
|
42
|
+
const filteredPosts = posts.filter(shouldIncludePost)
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
44
|
+
if (i18n) {
|
|
45
|
+
// With i18n: filter by locale
|
|
46
|
+
return filteredPosts
|
|
47
|
+
.filter(({ id }) => {
|
|
48
|
+
const [postLocale] = id.split('/')
|
|
49
|
+
return postLocale === Astro.currentLocale
|
|
50
|
+
})
|
|
51
|
+
.toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
|
52
|
+
} else {
|
|
53
|
+
// Without i18n: return all posts
|
|
54
|
+
return filteredPosts.toSorted(
|
|
55
|
+
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
)
|
|
69
60
|
|
|
70
61
|
// Calculate pagination
|
|
71
62
|
const totalPosts = entries.length
|
|
@@ -134,7 +125,7 @@ const tagBaseUrl = i18n
|
|
|
134
125
|
: `/${routeBasePath}/tags`
|
|
135
126
|
---
|
|
136
127
|
|
|
137
|
-
<Layout title={blogConfig.blogTitle}>
|
|
128
|
+
<Layout title={blogConfig.blogTitle} blogConfig={blogConfig} collectionName={collectionName}>
|
|
138
129
|
<div class="max-w-7xl mx-auto">
|
|
139
130
|
<h1 class="text-4xl font-bold mb-10">{blogConfig.blogTitle}</h1>
|
|
140
131
|
<div class="space-y-6">
|