|
| 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