Skip to content

Commit 43919f5

Browse files
refactor(blog): vendor shared libs and add tags sidebar group
Copy authors, content, and tags helpers from starlight-blog into a local libs directory so they can be patched. Add a localized tags sidebar group to the blog middleware and remove the BlogI18nControls component from the blog listing page. Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent aa49270 commit 43919f5

7 files changed

Lines changed: 452 additions & 8 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { GetStaticPathsResult } from 'astro'
2+
import { slug } from 'github-slugger'
3+
import starlightConfig from 'virtual:starlight/user-config'
4+
import config from 'virtual:starlight-blog/config'
5+
6+
import type { StarlightBlogAuthor } from '../../../../node_modules/starlight-blog/schema'
7+
import { DefaultLocale, type Locale } from '../../../../node_modules/starlight-blog/libs/i18n.ts'
8+
import { getPathWithLocale } from '../../../../node_modules/starlight-blog/libs/page.ts'
9+
import { getBlogEntries, type StarlightBlogEntry } from './content'
10+
11+
export async function getAllAuthors(locale: Locale): Promise<StarlightBlogEntryAuthors> {
12+
const entries = await getBlogEntries(locale)
13+
const entryAuthors: StarlightBlogEntryAuthors = new Map()
14+
15+
for (const entry of entries) {
16+
for (const author of getEntryAuthors(entry)) {
17+
const infos = entryAuthors.get(author.name) ?? { entries: [], author: { ...author, slug: slug(author.name) } }
18+
19+
infos.entries.push(entry)
20+
21+
entryAuthors.set(author.name, infos)
22+
}
23+
}
24+
25+
return entryAuthors
26+
}
27+
28+
export async function getAuthorsStaticPaths() {
29+
const paths = []
30+
31+
if (starlightConfig.isMultilingual) {
32+
for (const localeKey of Object.keys(starlightConfig.locales)) {
33+
const locale = localeKey === 'root' ? undefined : localeKey
34+
35+
const entryAuthors = await getAllAuthors(locale)
36+
37+
for (const [, { author, entries }] of entryAuthors.entries()) {
38+
paths.push(getAuthorsStaticPath(entries, author, locale))
39+
}
40+
}
41+
} else {
42+
const entryAuthors = await getAllAuthors(DefaultLocale)
43+
44+
for (const [, { author, entries }] of entryAuthors.entries()) {
45+
paths.push(getAuthorsStaticPath(entries, author, DefaultLocale))
46+
}
47+
}
48+
49+
return paths satisfies GetStaticPathsResult
50+
}
51+
52+
export function getEntryAuthors(entry: StarlightBlogEntry): StarlightBlogAuthor[] {
53+
const authors: StarlightBlogAuthor[] = []
54+
55+
if (!entry.data.authors) {
56+
authors.push(...Object.values(config.authors))
57+
} else if (typeof entry.data.authors === 'string') {
58+
authors.push(getAuthorFromConfig(entry.data.authors))
59+
} else if (Array.isArray(entry.data.authors)) {
60+
for (const author of entry.data.authors) {
61+
if (typeof author === 'string') {
62+
authors.push(getAuthorFromConfig(author))
63+
} else {
64+
authors.push(author)
65+
}
66+
}
67+
} else {
68+
authors.push(entry.data.authors)
69+
}
70+
71+
return authors
72+
}
73+
74+
function getAuthorFromConfig(id: string): StarlightBlogAuthor {
75+
const author = config.authors[id]
76+
77+
if (!author) {
78+
throw new Error(`Author '${id}' not found in the blog configuration.`)
79+
}
80+
81+
return author
82+
}
83+
84+
function getAuthorsStaticPath(entries: StarlightBlogEntry[], author: StarlightBlogEntryAuthor, locale: Locale) {
85+
return {
86+
params: {
87+
prefix: getPathWithLocale(config.prefix, locale),
88+
author: author.slug,
89+
},
90+
props: {
91+
author,
92+
entries,
93+
locale,
94+
},
95+
}
96+
}
97+
98+
type StarlightBlogEntryAuthorSlug = string
99+
100+
interface StarlightBlogEntryAuthor extends StarlightBlogAuthor {
101+
slug: StarlightBlogEntryAuthorSlug
102+
}
103+
104+
type StarlightBlogEntryAuthors = Map<
105+
StarlightBlogEntryAuthorSlug,
106+
{
107+
entries: StarlightBlogEntry[]
108+
author: StarlightBlogEntryAuthor
109+
}
110+
>
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import type { GetStaticPathsResult } from 'astro'
2+
import { type CollectionEntry, getCollection, getEntry, render } from 'astro:content'
3+
import starlightConfig from 'virtual:starlight/user-config'
4+
import config from 'virtual:starlight-blog/config'
5+
6+
import { DefaultLocale, type Locale } from '../../../../node_modules/starlight-blog/libs/i18n.ts'
7+
import { getRelativeUrl, getRelativeBlogUrl, getPathWithLocale } from '../../../../node_modules/starlight-blog/libs/page.ts'
8+
import { stripLeadingSlash, stripTrailingSlash } from '../../../../node_modules/starlight-blog/libs/path.ts'
9+
10+
const blogEntriesPerLocale = new Map<Locale, StarlightBlogEntry[]>()
11+
12+
export async function getBlogStaticPaths() {
13+
const paths = []
14+
15+
if (starlightConfig.isMultilingual) {
16+
for (const localeKey of Object.keys(starlightConfig.locales)) {
17+
const locale = localeKey === 'root' ? undefined : localeKey
18+
19+
const entries = await getBlogEntries(locale)
20+
const pages = getPaginatedBlogEntries(entries)
21+
22+
for (const [index, entries] of pages.entries()) {
23+
paths.push(getBlogStaticPath(pages, entries, index, locale))
24+
}
25+
}
26+
} else {
27+
const entries = await getBlogEntries(DefaultLocale)
28+
const pages = getPaginatedBlogEntries(entries)
29+
30+
for (const [index, entries] of pages.entries()) {
31+
paths.push(getBlogStaticPath(pages, entries, index, DefaultLocale))
32+
}
33+
}
34+
35+
return paths satisfies GetStaticPathsResult
36+
}
37+
38+
export async function getSidebarBlogEntries(locale: Locale) {
39+
const entries = await getBlogEntries(locale)
40+
41+
const featured: StarlightBlogEntry[] = []
42+
const recent: StarlightBlogEntry[] = []
43+
44+
for (const entry of entries) {
45+
if (entry.data.featured) {
46+
featured.push(entry)
47+
} else {
48+
recent.push(entry)
49+
}
50+
}
51+
52+
return { featured, recent: recent.slice(0, config.recentPostCount) }
53+
}
54+
55+
export async function getBlogEntry(slug: string, locale: Locale): Promise<StarlightBlogEntryPaginated> {
56+
const entries = await getBlogEntries(locale)
57+
58+
const entryIndex = entries.findIndex((entry) => {
59+
if (entry.id === stripLeadingSlash(stripTrailingSlash(slug))) return true
60+
if (locale) return entry.id === stripLeadingSlash(stripTrailingSlash(getPathWithLocale(slug, undefined)))
61+
return false
62+
})
63+
const entry = entries[entryIndex]
64+
65+
if (!entry) {
66+
throw new Error(`Blog post with slug '${slug}' not found.`)
67+
}
68+
69+
validateBlogEntry(entry)
70+
71+
const prevEntry = entries[entryIndex - 1]
72+
const prevLink = prevEntry
73+
? { href: getRelativeUrl(`/${getPathWithLocale(prevEntry.id, locale)}`), label: prevEntry.data.title }
74+
: undefined
75+
76+
const nextEntry = entries[entryIndex + 1]
77+
const nextLink = nextEntry
78+
? { href: getRelativeUrl(`/${getPathWithLocale(nextEntry.id, locale)}`), label: nextEntry.data.title }
79+
: undefined
80+
81+
return {
82+
entry,
83+
nextLink: config.prevNextLinksOrder === 'reverse-chronological' ? nextLink : prevLink,
84+
prevLink: config.prevNextLinksOrder === 'reverse-chronological' ? prevLink : nextLink,
85+
}
86+
}
87+
88+
function isDefaultLocaleBlogEntry(entry: CollectionEntry<'docs'>) {
89+
return entry.id.startsWith(`${config.prefix}/`) && entry.id !== `${config.prefix}/index`
90+
}
91+
92+
export async function getBlogEntries(locale: Locale): Promise<StarlightBlogEntry[]> {
93+
if (blogEntriesPerLocale.has(locale)) {
94+
return blogEntriesPerLocale.get(locale) as StarlightBlogEntry[]
95+
}
96+
97+
const docEntries = await getCollection('docs')
98+
const blogEntries: StarlightEntry[] = []
99+
100+
for (const entry of docEntries) {
101+
if (import.meta.env.MODE === 'production' && entry.data.draft === true) continue
102+
if (!isDefaultLocaleBlogEntry(entry)) continue
103+
104+
if (locale === DefaultLocale) {
105+
blogEntries.push(entry)
106+
continue
107+
}
108+
109+
const localizedEntryId = getPathWithLocale(entry.id, locale)
110+
111+
try {
112+
const localizedEntry = await getEntry('docs', localizedEntryId)
113+
if (!localizedEntry) throw new Error('Unavailable localized entry.')
114+
if (localizedEntry.data.draft === true) throw new Error('Draft localized entry.')
115+
blogEntries.push(localizedEntry)
116+
} catch {
117+
blogEntries.push(entry)
118+
}
119+
}
120+
121+
validateBlogEntries(blogEntries)
122+
123+
blogEntries.sort((a, b) => {
124+
return b.data.date.getTime() - a.data.date.getTime() || a.data.title.localeCompare(b.data.title)
125+
})
126+
127+
blogEntriesPerLocale.set(locale, blogEntries)
128+
129+
return blogEntries
130+
}
131+
132+
export async function getBlogEntryExcerpt(entry: StarlightBlogEntry) {
133+
if (entry.data.excerpt) {
134+
return entry.data.excerpt
135+
}
136+
137+
const { Content } = await render(entry)
138+
139+
return Content
140+
}
141+
142+
function getBlogStaticPath(
143+
pages: StarlightBlogEntry[][],
144+
entries: StarlightBlogEntry[],
145+
index: number,
146+
locale: Locale,
147+
) {
148+
const prevPage = index === 0 ? undefined : pages.at(index - 1)
149+
const prevLink = prevPage ? { href: getRelativeBlogUrl(index === 1 ? '/' : `/${index}`, locale) } : undefined
150+
151+
const nextPage = pages.at(index + 1)
152+
const nextLink = nextPage ? { href: getRelativeBlogUrl(`/${index + 2}`, locale) } : undefined
153+
154+
return {
155+
params: {
156+
page: index === 0 ? undefined : `${index + 1}`,
157+
prefix: getPathWithLocale(config.prefix, locale),
158+
},
159+
props: {
160+
entries,
161+
locale,
162+
nextLink: config.prevNextLinksOrder === 'reverse-chronological' ? nextLink : prevLink,
163+
prevLink: config.prevNextLinksOrder === 'reverse-chronological' ? prevLink : nextLink,
164+
} satisfies StarlightBlogStaticProps,
165+
}
166+
}
167+
168+
function getPaginatedBlogEntries(entries: StarlightBlogEntry[]): StarlightBlogEntry[][] {
169+
const pages: StarlightBlogEntry[][] = []
170+
171+
for (const entry of entries) {
172+
const lastPage = pages.at(-1)
173+
174+
if (!lastPage || lastPage.length === config.postCount) {
175+
pages.push([entry])
176+
} else {
177+
lastPage.push(entry)
178+
}
179+
}
180+
181+
if (pages.length === 0) {
182+
pages.push([])
183+
}
184+
185+
return pages
186+
}
187+
188+
function validateBlogEntries(entries: StarlightEntry[]): asserts entries is StarlightBlogEntry[] {
189+
for (const entry of entries) {
190+
validateBlogEntry(entry)
191+
}
192+
}
193+
194+
function validateBlogEntry(entry: StarlightEntry): asserts entry is StarlightBlogEntry {
195+
if (entry.data.date === undefined) {
196+
throw new Error(`Missing date for blog entry '${entry.id}'.`)
197+
}
198+
}
199+
200+
type StarlightEntry = CollectionEntry<'docs'>
201+
202+
export type StarlightBlogEntry = StarlightEntry & {
203+
data: {
204+
date: Date
205+
}
206+
}
207+
208+
export interface StarlightBlogLink {
209+
href: string
210+
label?: string
211+
}
212+
213+
export interface StarlightBlogEntryPaginated {
214+
entry: StarlightBlogEntry
215+
nextLink: StarlightBlogLink | undefined
216+
prevLink: StarlightBlogLink | undefined
217+
}
218+
219+
interface StarlightBlogStaticProps {
220+
entries: StarlightBlogEntry[]
221+
locale: Locale
222+
nextLink: StarlightBlogLink | undefined
223+
prevLink: StarlightBlogLink | undefined
224+
}

0 commit comments

Comments
 (0)