Add new Starlight-GhostCMS plugin #66
|
@ -0,0 +1,60 @@
|
||||||
|
import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types'
|
||||||
|
import type { AstroIntegrationLogger } from 'astro'
|
||||||
|
import { type StarlightGhostConfig, validateConfig } from './src/schemas/config'
|
||||||
|
import { vitePluginStarlightGhostConfig } from './src/integrations/vite'
|
||||||
|
|
||||||
|
export type { StarlightGhostConfig }
|
||||||
|
|
||||||
|
export default function starlightBlogPlugin(userConfig?: StarlightGhostConfig): StarlightPlugin {
|
||||||
|
const config: StarlightGhostConfig = validateConfig(userConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'starlight-blog-plugin',
|
||||||
|
hooks: {
|
||||||
|
setup({ addIntegration, config: starlightConfig, logger, updateConfig: updateStarlightConfig }) {
|
||||||
|
updateStarlightConfig({
|
||||||
|
components: {
|
||||||
|
...starlightConfig.components,
|
||||||
|
...overrideStarlightComponent(starlightConfig.components, logger, 'MarkdownContent'),
|
||||||
|
...overrideStarlightComponent(starlightConfig.components, logger, 'Sidebar'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addIntegration({
|
||||||
|
name: 'starlight-ghostcms',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:setup': ({ injectRoute, updateConfig }) => {
|
||||||
|
injectRoute({
|
||||||
|
pattern: '',
|
||||||
|
entrypoint: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
updateConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [vitePluginStarlightGhostConfig(config)],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function overrideStarlightComponent(
|
||||||
|
components: StarlightUserConfig['components'],
|
||||||
|
logger: AstroIntegrationLogger,
|
||||||
|
component: keyof NonNullable<StarlightUserConfig['components']>,
|
||||||
|
) {
|
||||||
|
if (components?.[component]) {
|
||||||
|
logger.warn(`It looks like you already have a \`${component}\` component override in your Starlight configuration.`)
|
||||||
|
logger.warn(`To use \`starlight-blog\`, remove the override for the \`${component}\` component.\n`)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[component]: `starlight-blog/overrides/${component}.astro`,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "@matthiesenxyz/starlight-ghostcms",
|
||||||
|
"description": "Starlight GhostCMS plugin to allow easier importing of GhostCMS Content into your starlight website",
|
||||||
|
"version": "0.0.1-dev01",
|
||||||
|
"homepage": "https://astro-ghostcms.xyz/",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"author": {
|
||||||
|
"email": "adam@matthiesen.xyz",
|
||||||
|
"name": "Adam Matthiesen - MatthiesenXYZ",
|
||||||
|
"url": "https://matthiesen.xyz"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"starlight",
|
||||||
|
"starlight-plugin",
|
||||||
|
"blog",
|
||||||
|
"content",
|
||||||
|
"integration",
|
||||||
|
"ghost",
|
||||||
|
"ghostcms"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/MatthiesenXYZ/astro-ghostcms.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues",
|
||||||
|
"email": "issues@astro-ghostcms.xyz"
|
||||||
|
},
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "types.d.ts",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
".env.demo",
|
||||||
|
"index.ts",
|
||||||
|
"tsconfig.json",
|
||||||
|
"types.d.ts"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/starlight": "0.19.0",
|
||||||
|
"@ts-ghost/core-api": "5.1.2",
|
||||||
|
"astro": "4.3.7"
|
||||||
|
},
|
||||||
|
"peerdependencies": {
|
||||||
|
"@astrojs/starlight": ">=0.19.0",
|
||||||
|
"astro": ">=4.3.7"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
import type { Author } from '../schemas/authors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
author: Author
|
||||||
|
}
|
||||||
|
|
||||||
|
const { author } = Astro.props
|
||||||
|
|
||||||
|
const isLink = author.website !== undefined
|
||||||
|
const Element = isLink ? 'a' : 'div'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Element href={isLink ? author.website : undefined} class="author">
|
||||||
|
{author.profile_image && <img alt={author.name} src={author.profile_image} />}
|
||||||
|
<div class="text">
|
||||||
|
<div class="name">{author.name}</div>
|
||||||
|
{author.bio && <div class="title">{author.bio}</div>}
|
||||||
|
</div>
|
||||||
|
</Element>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.author {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
line-height: var(--sl-line-height-headings);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: var(--sl-text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author[href] .name {
|
||||||
|
color: var(--sl-color-text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--sl-text-xs);
|
||||||
|
color: var(--sl-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author[href]:hover .name {
|
||||||
|
color: var(--sl-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 1px solid var(--sl-color-gray-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
import { getBlogEntryMetadata, type StarlightBlogEntry } from '../utils/content'
|
||||||
|
|
||||||
|
import Author from './Author.astro'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: StarlightBlogEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
const { authors, date } = getBlogEntryMetadata(entry)
|
||||||
|
|
||||||
|
const hasAuthors = authors.length > 0
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="metadata not-content">
|
||||||
|
<time datetime={entry.data.date.toISOString()}>
|
||||||
|
{date}
|
||||||
|
</time>
|
||||||
|
{
|
||||||
|
hasAuthors ? (
|
||||||
|
<div class="authors">
|
||||||
|
{authors.map((author: any) => (
|
||||||
|
<Author {author} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
font-size: var(--sl-text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.authors {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
import StarlightPage, { type StarlightPageProps as Props } from '@astrojs/starlight/components/StarlightPage.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<StarlightPage {...Astro.props}>
|
||||||
|
<slot />
|
||||||
|
</StarlightPage>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(:is(.right-sidebar-panel, mobile-starlight-toc)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.main-frame) {
|
||||||
|
padding-top: var(--sl-nav-height);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
import type { StarlightBlogEntry } from '../utils/content'
|
||||||
|
|
||||||
|
import Preview from './Preview.astro'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entries: StarlightBlogEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entries } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="posts">
|
||||||
|
{entries.map((entry) => <Preview {entry} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.posts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
import type { StarlightBlogLink } from '../utils/content'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
next: StarlightBlogLink | undefined
|
||||||
|
prev: StarlightBlogLink | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const { next, prev } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
prev || next ? (
|
||||||
|
<div class="pagination not-content">
|
||||||
|
{prev && (
|
||||||
|
<a href={prev.href} rel="prev">
|
||||||
|
« {prev.label}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{next && (
|
||||||
|
<a href={next.href} rel="next">
|
||||||
|
{next.label} »
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
box-shadow: var(--sl-shadow-md);
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font-size: var(--sl-text-lg);
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
border-color: var(--sl-color-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[rel='next'] {
|
||||||
|
justify-content: end;
|
||||||
|
grid-column: 2;
|
||||||
|
text-align: end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
import { getBlogEntryExcerpt, type StarlightBlogEntry } from '../utils/content'
|
||||||
|
|
||||||
|
import Metadata from './Metadata.astro'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: StarlightBlogEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
|
||||||
|
const Excerpt = await getBlogEntryExcerpt(entry)
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="preview">
|
||||||
|
<header>
|
||||||
|
<h2>
|
||||||
|
<a href={`/${entry.slug}`}>{entry.data.title}</a>
|
||||||
|
</h2>
|
||||||
|
<Metadata entry={entry} />
|
||||||
|
</header>
|
||||||
|
<div class="sl-markdown-content">
|
||||||
|
{typeof Excerpt === 'string' ? Excerpt : <Excerpt />}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { ViteUserConfig } from 'astro'
|
||||||
|
|
||||||
|
import type { StarlightGhostConfig } from '../schemas/config.ts'
|
||||||
|
|
||||||
|
// Expose the starlight-blog plugin configuration.
|
||||||
|
export function vitePluginStarlightGhostConfig(config: StarlightGhostConfig): VitePlugin {
|
||||||
|
const moduleId = 'virtual:starlight-ghost-config'
|
||||||
|
const resolvedModuleId = `\0${moduleId}`
|
||||||
|
const moduleContent = `export default ${JSON.stringify(config)}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-starlight-ghost-config',
|
||||||
|
load(id) {
|
||||||
|
return id === resolvedModuleId ? moduleContent : undefined
|
||||||
|
},
|
||||||
|
resolveId(id) {
|
||||||
|
return id === moduleId ? resolvedModuleId : undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number]
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
import StarlightMarkdownContent from '@astrojs/starlight/components/MarkdownContent.astro'
|
||||||
|
import type { Props } from '@astrojs/starlight/props'
|
||||||
|
|
||||||
|
import PrevNextLinks from '../components/PrevNextLinks.astro'
|
||||||
|
import { getBlogEntry, type StarlightBlogEntryPaginated } from '../utils/content'
|
||||||
|
import { isAnyBlogPostPage } from '../utils/page'
|
||||||
|
import Metadata from '../components/Metadata.astro'
|
||||||
|
|
||||||
|
const isBlogPost = isAnyBlogPostPage(Astro.props.slug)
|
||||||
|
let blogEntry: StarlightBlogEntryPaginated | undefined = undefined
|
||||||
|
|
||||||
|
if (isBlogPost) {
|
||||||
|
blogEntry = await getBlogEntry(Astro.url.pathname)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<StarlightMarkdownContent {...Astro.props}>
|
||||||
|
{isBlogPost && blogEntry ? <Metadata entry={blogEntry.entry} /> : null}
|
||||||
|
<slot />
|
||||||
|
{
|
||||||
|
isBlogPost && blogEntry ? (
|
||||||
|
<div class="post-footer">
|
||||||
|
<PrevNextLinks next={blogEntry.nextLink} prev={blogEntry.prevLink} />
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</StarlightMarkdownContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-block-start: 2rem !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro'
|
||||||
|
import type { Props } from '@astrojs/starlight/props'
|
||||||
|
|
||||||
|
import { getRecentBlogEntries } from '../utils/content'
|
||||||
|
import { isAnyBlogPage, isBlogPostPage, isBlogRoot } from '../utils/page'
|
||||||
|
|
||||||
|
const isBlog = isAnyBlogPage(Astro.props.slug)
|
||||||
|
const recentEntries = isBlog ? await getRecentBlogEntries() : []
|
||||||
|
|
||||||
|
const blogSidebar: Props['sidebar'] = isBlog
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
attrs: {},
|
||||||
|
badge: undefined,
|
||||||
|
href: '/blog',
|
||||||
|
isCurrent: isBlogRoot(Astro.props.slug),
|
||||||
|
label: 'All posts',
|
||||||
|
type: 'link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
badge: undefined,
|
||||||
|
collapsed: false,
|
||||||
|
entries: recentEntries.map((blogEntry) => ({
|
||||||
|
attrs: {},
|
||||||
|
badge: undefined,
|
||||||
|
href: `/${blogEntry.slug}`,
|
||||||
|
isCurrent: isBlogPostPage(Astro.props.slug, blogEntry.slug),
|
||||||
|
label: blogEntry.data.title,
|
||||||
|
type: 'link',
|
||||||
|
})),
|
||||||
|
label: 'Recent posts',
|
||||||
|
type: 'group',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
!isBlog && (
|
||||||
|
<div class="md:sl-hidden">
|
||||||
|
<a href="/blog">Blog</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<StarlightSidebar {...Astro.props} sidebar={isBlog ? blogSidebar : Astro.props.sidebar} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
border-bottom: 1px solid var(--sl-color-gray-6);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div a {
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font-size: var(--sl-text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
ghostIdentitySchema,
|
||||||
|
ghostMetaSchema,
|
||||||
|
ghostMetadataSchema,
|
||||||
|
} from "@ts-ghost/core-api";
|
||||||
|
import { z } from "astro/zod";
|
||||||
|
|
||||||
|
export const authorsSchema = z.object({
|
||||||
|
...ghostIdentitySchema.shape,
|
||||||
|
...ghostMetadataSchema.shape,
|
||||||
|
name: z.string(),
|
||||||
|
profile_image: z.string().nullable(),
|
||||||
|
cover_image: z.string().nullable(),
|
||||||
|
bio: z.string().nullable(),
|
||||||
|
website: z.string().nullable(),
|
||||||
|
location: z.string().nullable(),
|
||||||
|
facebook: z.string().nullable(),
|
||||||
|
twitter: z.string().nullable(),
|
||||||
|
count: z
|
||||||
|
.object({
|
||||||
|
posts: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
url: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Author = z.infer<typeof authorsSchema>;
|
||||||
|
|
||||||
|
export const ghostFetchAuthorsSchema = z.object({
|
||||||
|
meta: ghostMetaSchema,
|
||||||
|
authors: z.array(authorsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authorsIncludeSchema = z.object({
|
||||||
|
"count.posts": z.literal(true).optional(),
|
||||||
|
});
|
||||||
|
export type AuthorsIncludeSchema = z.infer<typeof authorsIncludeSchema>;
|
|
@ -0,0 +1,35 @@
|
||||||
|
/** @ts-expect-error */
|
||||||
|
import { AstroError } from 'astro/errors'
|
||||||
|
import { z } from 'astro/zod'
|
||||||
|
|
||||||
|
const configSchema = z
|
||||||
|
.object({
|
||||||
|
/**
|
||||||
|
* The title of the blog.
|
||||||
|
*/
|
||||||
|
title: z.string().default('Blog'),
|
||||||
|
})
|
||||||
|
.default({})
|
||||||
|
|
||||||
|
export function validateConfig(userConfig: unknown): StarlightGhostConfig {
|
||||||
|
const config = configSchema.safeParse(userConfig)
|
||||||
|
|
||||||
|
if (!config.success) {
|
||||||
|
const errors = config.error.flatten()
|
||||||
|
|
||||||
|
throw new AstroError(
|
||||||
|
`Invalid starlight-GhostCMS configuration:
|
||||||
|
|
||||||
|
${errors.formErrors.map((formError) => ` - ${formError}`).join('\n')}
|
||||||
|
${Object.entries(errors.fieldErrors)
|
||||||
|
.map(([fieldName, fieldErrors]) => ` - ${fieldName}: ${fieldErrors.join(' - ')}`)
|
||||||
|
.join('\n')}
|
||||||
|
`,
|
||||||
|
"See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/matthiesenxyz/astro-ghostcms/issues/new/choose",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StarlightGhostConfig = z.infer<typeof configSchema>
|
|
@ -0,0 +1,135 @@
|
||||||
|
import type { z } from 'astro/zod'
|
||||||
|
import { getCollection, type AstroCollectionEntry } from 'astro:content'
|
||||||
|
import starlightConfig from 'virtual:starlight/user-config'
|
||||||
|
import config from 'virtual:starlight-ghost-config'
|
||||||
|
|
||||||
|
import type { Author } from '../schemas/authors'
|
||||||
|
|
||||||
|
export async function getBlogStaticPaths() {
|
||||||
|
const entries = await getBlogEntries()
|
||||||
|
|
||||||
|
const entryPages: StarlightBlogEntry[][] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const lastPage = entryPages.at(-1)
|
||||||
|
|
||||||
|
if (!lastPage || lastPage.length === config.postCount) {
|
||||||
|
entryPages.push([entry])
|
||||||
|
} else {
|
||||||
|
lastPage.push(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryPages.length === 0) {
|
||||||
|
entryPages.push([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return entryPages.map((entries, index) => {
|
||||||
|
const prevPage = index === 0 ? undefined : entryPages.at(index - 1)
|
||||||
|
const nextPage = entryPages.at(index + 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
page: index === 0 ? undefined : index + 1,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
entries,
|
||||||
|
nextLink: nextPage ? { href: `/blog/${index + 2}`, label: 'Older posts' } : undefined,
|
||||||
|
prevLink: prevPage ? { href: index === 1 ? '/blog' : `/blog/${index}`, label: 'Newer posts' } : undefined,
|
||||||
|
} satisfies StarlightBlogStaticProps,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecentBlogEntries() {
|
||||||
|
const entries = await getBlogEntries()
|
||||||
|
|
||||||
|
return entries.slice(0, config.recentPostCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlogEntry(slug: string): Promise<StarlightBlogEntryPaginated> {
|
||||||
|
const entries = await getBlogEntries()
|
||||||
|
|
||||||
|
const entryIndex = entries.findIndex((entry) => entry.slug === slug.replace(/^\//, '').replace(/\/$/, ''))
|
||||||
|
const entry = entries[entryIndex]
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Blog post with slug '${slug}' not found.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevEntry = entries[entryIndex - 1]
|
||||||
|
const nextEntry = entries[entryIndex + 1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
nextLink: nextEntry ? { href: `/${nextEntry.slug}`, label: nextEntry.data.title } : undefined,
|
||||||
|
prevLink: prevEntry ? { href: `/${prevEntry.slug}`, label: prevEntry.data.title } : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlogEntryMetadata(entry: StarlightBlogEntry): StarlightBlogEntryMetadata {
|
||||||
|
const authors: Author[] = []
|
||||||
|
|
||||||
|
if (!entry.data.authors) {
|
||||||
|
authors.push(...Object.values(authors))
|
||||||
|
} else {
|
||||||
|
authors.push(entry.data.authors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authors,
|
||||||
|
date: entry.data.date.toLocaleDateString(starlightConfig.defaultLocale.lang, { dateStyle: 'medium' }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlogEntries() {
|
||||||
|
const entries = await getCollection<StarlightEntryData>('docs', ({ id }) => {
|
||||||
|
return id.startsWith('blog/') && id !== 'blog/index.mdx'
|
||||||
|
})
|
||||||
|
|
||||||
|
return entries.sort((a, b) => {
|
||||||
|
return b.data.date.getTime() - a.data.date.getTime()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlogEntryExcerpt(entry: StarlightBlogEntry) {
|
||||||
|
if (entry.data.excerpt) {
|
||||||
|
return entry.data.excerpt
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Content } = await entry.render()
|
||||||
|
|
||||||
|
return Content
|
||||||
|
}
|
||||||
|
|
||||||
|
type StarlightEntryData = z.infer<ReturnType<typeof blogSchema>> & { title: string }
|
||||||
|
type StarlightEntry = AstroCollectionEntry<StarlightEntryData>
|
||||||
|
|
||||||
|
export type StarlightBlogEntry = StarlightEntry & {
|
||||||
|
data: {
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StarlightBlogLink {
|
||||||
|
href: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StarlightBlogEntryPaginated {
|
||||||
|
entry: StarlightBlogEntry
|
||||||
|
nextLink: StarlightBlogLink | undefined
|
||||||
|
prevLink: StarlightBlogLink | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StarlightBlogEntryMetadata {
|
||||||
|
authors: Author[]
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface StarlightBlogStaticProps {
|
||||||
|
entries: StarlightBlogEntry[]
|
||||||
|
nextLink: StarlightBlogLink | undefined
|
||||||
|
prevLink: StarlightBlogLink | undefined
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
export function isAnyBlogPage(slug: string) {
|
||||||
|
return slug.match(/^blog(\/?$|\/.+\/?$)/) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlogRoot(slug: string) {
|
||||||
|
return slug === 'blog'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAnyBlogPostPage(slug: string) {
|
||||||
|
return slug.match(/^blog\/(?!(\d+\/?|tags\/.+)$).+$/) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlogPostPage(slug: string, postSlug: string) {
|
||||||
|
return slug === postSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlogTagsPage(slug: string, tag: string) {
|
||||||
|
return slug === `blog/tags/${tag}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageProps(title: string): StarlightPageProps {
|
||||||
|
return {
|
||||||
|
frontmatter: {
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StarlightPageProps {
|
||||||
|
frontmatter: {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module 'virtual:starlight/user-config' {
|
||||||
|
const Config: import('@astrojs/starlight/types').StarlightConfig
|
||||||
|
|
||||||
|
export default Config
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module 'virtual:starlight-ghost-config' {
|
||||||
|
const StarlightGhostConfig: import('./src/schemas/config').StarlightGhostConfig
|
||||||
|
|
||||||
|
export default StarlightGhostConfig
|
||||||
|
}
|
817
pnpm-lock.yaml
817
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue