Skip to content

Commit

Permalink
feat: adds an RSS feed to the generated blog
Browse files Browse the repository at this point in the history
  • Loading branch information
HiDeoo committed May 11, 2024
1 parent e950bfa commit e4ddb23
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 23 deletions.
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export default defineConfig({
title: 'Starlight Blog',
}),
],
site: 'https://starlight-blog-docs.vercel.app',
})
7 changes: 3 additions & 4 deletions docs/src/content/docs/blog/vario-nunc-polo.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
title: Vario nunc polo
date: 2020-12-12
excerpt: Cras placerat sollicitudin purus, at gravida ligula tincidunt vel. Integer blandit placerat enim, eget suscipit erat venenatis vitae. Nullam et enim egestas, posuere ex id, tempus nibh. Nullam consequat, magna id consequat porttitor, sapien ex vestibulum nisi, vitae vestibulum velit.
tags:
- Placeholder
authors:
Expand All @@ -11,13 +10,13 @@ authors:
- hideoo
---

## Canes plura palmas eodem huc scelerate spectat
### Canes plura palmas eodem huc scelerate spectat

Lorem markdownum, numen adducere Parthaoniae versa mirabere mea. Magnum utque, et senserat ferendum disiectisque sub coniunx et stolidi telum; duabus.

> Crebri pantherarum talis sed etiamnum habet: modo transire. Cum Xanthos rura per iter erat tunc **insanos tibi** ignorant cum inde fulgebant, dicentem certamina **temptabat iam inpleratque**. Enim figuram aliter, semper curru mihi est.
## Notae esse et gurgite sibi dat dona
### Notae esse et gurgite sibi dat dona

Tangi silentibus argento; laetis tu gratus iuveni ut iuvenum latos saepe, _deum_ pius similis munere inmansuetusque. Fine defessa. In natum luminis notat terra pulcherrima nomen falsi magis! Anguipedum armenta minoribus _tolluntur_ fecerit instanti quid iactantem haesit exaudire sanguineam, delapsa **fores exegi serpente** liquida; de.

Expand All @@ -27,7 +26,7 @@ Tangi silentibus argento; laetis tu gratus iuveni ut iuvenum latos saepe, _deum_
4. Quod longique parce pennisque saepe
5. Conrepta pendentia tua quod Deianira

## Manibusque Mnemosynen nam aut accipit illa
### Manibusque Mnemosynen nam aut accipit illa

Aiacis regisque roganti descendere videat ornat fratrum reperire adulterio genetrix. Erit vota _tempore aram quoque_ ille sceleri mittit saxificos deferre, illa inque animi renovare Oetaeus vertice, prius regia. Hippomene curis, pudorque potestas bene, dis deus mota hominum praestate. Et tibi Ixione tantum haec venata: nihil calentibus, ferox, illo corpore Aeson. Contra ab mora ne ventrem admirantibus longam summas cunis inundet?

Expand Down
39 changes: 38 additions & 1 deletion packages/starlight-blog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/ty
import type { AstroIntegrationLogger } from 'astro'

import { type StarlightBlogConfig, validateConfig, type StarlightBlogUserConfig } from './libs/config'
import { getDefaultLocale } from './libs/i18n'
import { stripLeadingSlash, stripTrailingSlash } from './libs/path'
import { vitePluginStarlightBlogConfig } from './libs/vite'

export type { StarlightBlogConfig, StarlightBlogUserConfig }
Expand All @@ -20,6 +22,24 @@ export default function starlightBlogPlugin(userConfig?: StarlightBlogUserConfig
...overrideStarlightComponent(starlightConfig.components, logger, 'Sidebar'),
...overrideStarlightComponent(starlightConfig.components, logger, 'ThemeSelect'),
},
head: [
...(starlightConfig.head ?? []),
...(astroConfig.site
? [
{
tag: 'link' as const,
attrs: {
href: `${stripTrailingSlash(astroConfig.site)}${stripTrailingSlash(
astroConfig.base,
)}/${stripLeadingSlash(stripTrailingSlash(config.prefix))}/rss.xml`,
rel: 'alternate',
title: config.title,
type: 'application/rss+xml',
},
},
]
: []),
],
})

addIntegration({
Expand All @@ -38,9 +58,26 @@ export default function starlightBlogPlugin(userConfig?: StarlightBlogUserConfig
prerender: true,
})

if (astroConfig.site) {
injectRoute({
entrypoint: 'starlight-blog/routes/rss',
pattern: '/[prefix]/rss.xml',
prerender: true,
})
}

updateConfig({
vite: {
plugins: [vitePluginStarlightBlogConfig(config, astroConfig)],
plugins: [
vitePluginStarlightBlogConfig(config, {
defaultLocale: getDefaultLocale(starlightConfig as StarlightUserConfig),
description: starlightConfig.description,
site: astroConfig.site,
title: starlightConfig.title,
titleDelimiter: starlightConfig.titleDelimiter,
trailingSlash: astroConfig.trailingSlash,
}),
],
},
})
},
Expand Down
15 changes: 15 additions & 0 deletions packages/starlight-blog/libs/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StarlightUserConfig } from '@astrojs/starlight/types'

export function getDefaultLocale(starlightConfig: StarlightUserConfig) {
let defaultLocale = 'en'
const defaultLocaleKey = starlightConfig.defaultLocale

if (starlightConfig.locales) {
defaultLocale =
defaultLocaleKey && defaultLocaleKey !== 'root'
? starlightConfig.locales[defaultLocaleKey]?.lang ?? defaultLocaleKey
: starlightConfig.locales['root']?.lang ?? defaultLocale
}

return defaultLocale
}
17 changes: 17 additions & 0 deletions packages/starlight-blog/libs/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Marked } from 'marked'
import markedPlaintify from 'marked-plaintify'
import { transform } from 'ultrahtml'
import sanitize from 'ultrahtml/transformers/sanitize'

const markedMd = new Marked({ gfm: true })
const markedText = new Marked({ gfm: true }, markedPlaintify())

export async function stripMarkdown(markdown: string) {
return await markedText.parse(markdown)
}

export async function renderMarkdownToHTML(markdown: string) {
const content = await markedMd.parse(markdown)

return transform(content, [sanitize()])
}
4 changes: 2 additions & 2 deletions packages/starlight-blog/libs/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ const trailingSlashTransformers: Record<AstroConfig['trailingSlash'], (path: str

const base = stripTrailingSlash(import.meta.env.BASE_URL)

export function getBlogPathWithBase(path: string) {
export function getBlogPathWithBase(path: string, ignoreTrailingSlash = false) {
path = stripLeadingSlash(path)

return getPathWithBase(path ? `/${config.prefix}/${path}` : `/${config.prefix}`)
return getPathWithBase(path ? `/${config.prefix}/${path}` : `/${config.prefix}`, ignoreTrailingSlash)
}

export function getPathWithBase(path: string, ignoreTrailingSlash = false) {
Expand Down
64 changes: 64 additions & 0 deletions packages/starlight-blog/libs/rss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { RSSOptions } from '@astrojs/rss'
import config from 'virtual:starlight-blog-config'
import context from 'virtual:starlight-blog-context'

import { getBlogEntries, type StarlightBlogEntry } from './content'
import { renderMarkdownToHTML, stripMarkdown } from './markdown'
import { getPathWithBase } from './page'

export function getRSSStaticPaths() {
return [{ params: { prefix: config.prefix } }]
}

export async function getRSSOptions(site: URL | undefined) {
const entries = await getBlogEntries()
entries.splice(20)

const options: RSSOptions = {
title: getRSSTitle(),
description: context.description ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The route is only injected if `site` is defined in the user Astro config.
site: site!,
items: await Promise.all(
entries.map(async (entry) => {
return {
title: entry.data.title,
link: getPathWithBase(`/${entry.slug}`),
pubDate: entry.data.date,
categories: entry.data.tags,
description: await getRSSDescription(entry),
content: await getRSSContent(entry),
}
}),
),
customData: `<language>${context.defaultLocale}</language>`,
}

if (context.trailingSlash !== 'ignore') {
options.trailingSlash = context.trailingSlash === 'always'
}

return options
}

function getRSSTitle() {
let title = (typeof context.title === 'string' ? context.title : context.title[context.defaultLocale]) ?? ''

if (title.length > 0) {
title += ` ${context.titleDelimiter ?? '|'} `
}

title += config.title

return title
}

function getRSSDescription(entry: StarlightBlogEntry): Promise<string> | undefined {
if (!entry.data.excerpt) return

return stripMarkdown(entry.data.excerpt)
}

function getRSSContent(entry: StarlightBlogEntry): Promise<string> {
return renderMarkdownToHTML(entry.body)
}
10 changes: 7 additions & 3 deletions packages/starlight-blog/libs/vite.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { StarlightUserConfig } from '@astrojs/starlight/types'
import type { AstroConfig, ViteUserConfig } from 'astro'

import type { StarlightBlogConfig } from './config'
Expand All @@ -9,9 +10,7 @@ export function vitePluginStarlightBlogConfig(
): VitePlugin {
const modules = {
'virtual:starlight-blog-config': `export default ${JSON.stringify(starlightBlogConfig)}`,
'virtual:starlight-blog-context': `export default ${JSON.stringify({
trailingSlash: context.trailingSlash,
})}`,
'virtual:starlight-blog-context': `export default ${JSON.stringify(context)}`,
}

const moduleResolutionMap = Object.fromEntries(
Expand All @@ -35,6 +34,11 @@ function resolveVirtualModuleId<TModuleId extends string>(id: TModuleId): `\0${T
}

export interface StarlightBlogContext {
defaultLocale: string
description: StarlightUserConfig['description']
site: AstroConfig['site']
title: StarlightUserConfig['title']
titleDelimiter: StarlightUserConfig['titleDelimiter']
trailingSlash: AstroConfig['trailingSlash']
}

Expand Down
12 changes: 12 additions & 0 deletions packages/starlight-blog/overrides/Sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro'
import type { Props } from '@astrojs/starlight/props'
import config from 'virtual:starlight-blog-config'
import context from 'virtual:starlight-blog-context'
import { getRecentBlogEntries } from '../libs/content'
import {
Expand Down Expand Up @@ -71,6 +72,17 @@ if (isBlog) {
type: 'group',
})
}
if (context.site) {
blogSidebar.push({
attrs: {},
badge: undefined,
href: getBlogPathWithBase('/rss.xml'),
isCurrent: false,
label: 'RSS',
type: 'link',
})
}
}
---

Expand Down
7 changes: 6 additions & 1 deletion packages/starlight-blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"./overrides/ThemeSelect.astro": "./overrides/ThemeSelect.astro",
"./routes/Blog.astro": "./routes/Blog.astro",
"./routes/Tags.astro": "./routes/Tags.astro",
"./routes/rss": "./routes/rss.xml.ts",
"./schema": "./schema.ts",
"./package.json": "./package.json"
},
Expand All @@ -22,8 +23,12 @@
"lint": "prettier -c --cache . && eslint . --cache --max-warnings=0"
},
"dependencies": {
"@astrojs/rss": "4.0.5",
"astro-remote": "0.3.2",
"github-slugger": "2.0.0"
"github-slugger": "2.0.0",
"marked": "12.0.2",
"marked-plaintify": "1.0.1",
"ultrahtml": "1.5.3"
},
"devDependencies": {
"@astrojs/starlight": "0.22.1",
Expand Down
12 changes: 12 additions & 0 deletions packages/starlight-blog/routes/rss.xml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import rss from '@astrojs/rss'
import type { APIRoute } from 'astro'

import { getRSSOptions, getRSSStaticPaths } from '../libs/rss'

export function getStaticPaths() {
return getRSSStaticPaths()
}

export const GET: APIRoute = async ({ site }) => {
return rss(await getRSSOptions(site))
}
9 changes: 9 additions & 0 deletions packages/starlight-blog/tests/e2e/blog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,12 @@ test('should not list draft blog posts in production', async ({ blogPage }) => {
blogPage.page.getByRole('article').getByRole('link', { exact: true, name: 'Pertimuit munere' }),
).not.toBeVisible()
})

test('should add a link to the RSS feed in the sidebar', async ({ blogPage }) => {
await blogPage.goto()

const link = blogPage.page.getByRole('link', { name: 'RSS' })

await expect(link).toBeVisible()
expect(await link.getAttribute('href')).toBe('/blog/rss.xml')
})
59 changes: 59 additions & 0 deletions packages/starlight-blog/tests/unit/basics/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { StarlightUserConfig } from '@astrojs/starlight/types'
import { describe, expect, test } from 'vitest'

import { getDefaultLocale } from '../../../libs/i18n'

describe('getDefaultLocale', () => {
test('returns the default locale for the built-in default locale', () => {
expect(getTestDefaultLocale({})).toBe('en')
})

test('returns the default locale with a root locale', () => {
expect(
getTestDefaultLocale({
locales: {
root: { label: 'Français', lang: 'fr' },
},
}),
).toBe('fr')
})

test('returns the default locale with a root locale and a default locale', () => {
expect(
getTestDefaultLocale({
defaultLocale: 'root',
locales: {
root: { label: 'Français', lang: 'fr' },
'zh-cn': { label: '简体中文', lang: 'zh-CN' },
},
}),
).toBe('fr')
})

test('returns the default locale with no root locale', () => {
expect(
getTestDefaultLocale({
defaultLocale: 'fr',
locales: {
fr: { label: 'Français', lang: 'fr' },
'zh-cn': { label: '简体中文', lang: 'zh-CN' },
},
}),
).toBe('fr')
})

test('returns the default locale with non root single locale', () => {
expect(
getTestDefaultLocale({
defaultLocale: 'fr',
locales: {
fr: { label: 'Français', lang: 'fr' },
},
}),
).toBe('fr')
})
})

function getTestDefaultLocale(config: Partial<StarlightUserConfig>) {
return getDefaultLocale(config as StarlightUserConfig)
}
Loading

0 comments on commit e4ddb23

Please sign in to comment.