Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more RSS feeds #24

Merged
merged 12 commits into from
Nov 19, 2023
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
},
{
"name": "Feeds",
"path": ["public/feed.xml"]
"path": ["public/feed.xml", "public/feed-*.xml"]
}
]
118 changes: 84 additions & 34 deletions vitepress/.vitepress/rss.mjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,99 @@
import path from 'path'
import { writeFileSync } from 'fs'
import { Feed } from 'feed'
import { createContentLoader } from 'vitepress'
import { isArticle, isNote } from './utils/content-type.mjs'
import { comparePublicationDate, isPublished } from './utils/frontmatter.mjs'
import { baseFeedOptions, compareItemsDates, feedItem, writeFeed } from './utils/rss.mjs'

/** @todo: should come from .env */
const APP_URL = `https://blog.mehdi.cc`

/** @param {import('vitepress').SiteConfig} config */
export async function rss(config) {
const feedOptions = baseFeedOptions(config)

// Load content and turn it into feed items.

/** @type {import('vitepress').ContentData[]} */
const content = (await createContentLoader(['articles/*.md', 'notes/*.md'], { excerpt: true, render: true }).load())
.filter(isPublished)
.toSorted(comparePublicationDate)

const notesItems = content.filter(isNote).map(feedItem)
const articlesItems = content.filter(isArticle).map(feedItem)
const articlesItemsExcerptOnly = content
.filter(isArticle)
.map(content => feedItem(content, { content: content.excerpt }))

/**
* https://www.rssboard.org/rss-profile
* https://github.com/jpmonette/feed
* Generate all feeds and store them on disk.
*
* - spec: https://www.rssboard.org/rss-specification
* - spec best practices: https://www.rssboard.org/rss-profile
* - `feed` package: https://github.com/jpmonette/feed
*/
const feed = new Feed({
docs: 'https://www.rssboard.org/rss-specification',
link: APP_URL + 'link',
title: config.site.title,

// Feed 1: full content

const feedWithEverything = new Feed({
title: 'Mehdi’s notes and articles',
description: config.site.description,
language: config.site.lang,
// image: 'https://blog.mehdi.cc/file.png',
// favicon: `${APP_URL}/favicon.ico`,
copyright: 'Copyright © 2023-present, Mehdi Merah',
feed: `${APP_URL}/feed.xml`,
ttl: 2880, // 1 day,
});
...feedOptions,
})

(await createContentLoader(['articles/*.md', 'notes/*.md'], { excerpt: true, render: true }).load())
.filter(isPublished)
.toSorted(comparePublicationDate)
.forEach(({ url, excerpt, frontmatter, html }) =>
feed.addItem({
title: frontmatter.title,
id: `${APP_URL}${url}`,
link: `${APP_URL}${url}`,
description: frontmatter.description || excerpt,
content: html,
date: frontmatter.publishedAt,
author: [{
name: 'Mehdi Merah',
link: 'https://mehdi.cc',
email: ' ', // hack, otherwise <author> is missing in RSS
}],
})
)

writeFileSync(path.join(config.outDir, 'feed.xml'), feed.rss2())
feedWithEverything.items = [...notesItems, ...articlesItems].toSorted(compareItemsDates)

writeFeed('feed', feedWithEverything)

// Feed 2: notes

const feedWithNotes = new Feed({
title: 'Mehdi’s notes',
description: 'A chronological gathering of… notes.',
feed: `${APP_URL}/feed-notes-only.xml`,
...feedOptions,
})

feedWithNotes.items = notesItems

writeFeed('feed-notes-only', feedWithNotes)

// Feed 3: articles

const feedWithArticles = new Feed({
title: 'Mehdi’s articles',
description: 'A chronological gathering of… articles.',
feed: `${APP_URL}/feed-articles-only.xml`,
...feedOptions,
})

feedWithArticles.items = articlesItems

writeFeed('feed-articles-only', feedWithArticles)

// Feed 4: articles excerpts

const feedWithArticlesExcerpts = new Feed({
title: 'Mehdi’s articles (excerpts only)',
description: 'Excerpt of my articles.',
feed: `${APP_URL}/feed-articles-excerpts-only.xml`,
...feedOptions,
})

feedWithArticlesExcerpts.items = articlesItemsExcerptOnly

writeFeed('feed-articles-excerpts-only', feedWithArticlesExcerpts)

// Feed 5: articles excerpts and notes

const feedWithArticlesExcerptsAndNotes = new Feed({
title: 'Mehdi’s light feed',
description: 'Articles excerpts and notes.',
feed: `${APP_URL}/feed-articles-excerpts-and-notes.xml`,
...feedOptions,
})

feedWithArticlesExcerptsAndNotes.items = [...notesItems, ...articlesItemsExcerptOnly].toSorted(compareItemsDates)

writeFeed('feed-articles-excerpts-and-notes', feedWithArticlesExcerptsAndNotes)
}
23 changes: 23 additions & 0 deletions vitepress/.vitepress/utils/content-type.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @typedef {import('vitepress').ContentData} ContentData */

/**
* Check the type of content based on URL.
*
* @param {ContentData} content
* @param {string} type
*/
const isContentType = ({ url }, type) => url.startsWith(`/${type}s/`)

/**
* The content is an article.
*
* @param {ContentData} content
*/
export const isArticle = content => isContentType(content, 'article')

/**
* The content is a note.
*
* @param {ContentData} content
*/
export const isNote = content => isContentType(content, 'note')
74 changes: 74 additions & 0 deletions vitepress/.vitepress/utils/rss.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import path from 'path'
import { writeFileSync } from 'fs'
import { Feed } from 'feed'

/** @typedef {import('feed').FeedOptions} FeedOptions */
/** @typedef {import('feed').Item} Item */
/** @typedef {import('vitepress').ContentData} ContentData */
/** @typedef {import('vitepress').SiteConfig} SiteConfig */

/** @todo: should come from .env */
const APP_URL = `https://blog.mehdi.cc`

/** @type SiteConfig */
let config = null

/**
* Compare frontmatter publication date
* @param {Item} a
* @param {Item} b
*/
export const compareItemsDates = (a, b) => new Date(b.date) - new Date(a.date)

/**
* @param {SiteConfig} config
* @returns {FeedOptions}
*/
export const baseFeedOptions = siteConfig => {
if (!config) {
config = { ...siteConfig }
}

return {
docs: 'https://www.rssboard.org/rss-specification',
link: APP_URL,
language: config.site.lang,
// image: 'https://blog.mehdi.cc/file.png',
// favicon: `${APP_URL}/favicon.ico`,
copyright: 'Copyright © 2023-present, Mehdi Merah',
ttl: 2880, // 1 day,
}
}

/**
* @param {ContentData} content
* @param {Item?} overrides
* @returns {Item}
*/
export const feedItem = ({ url, excerpt, frontmatter, html }, { content } = null) => ({
title: frontmatter.title,
id: `${APP_URL}${url}`,
link: `${APP_URL}${url}`,
description: frontmatter.description || excerpt,
content: content ?? html,
date: frontmatter.publishedAt,
author,
})

/** @type {import('feed').Author[]} */
export const author = [{
name: 'Mehdi Merah',
link: 'https://mehdi.cc',
email: '[email protected]',
}]

/**
* Save a feed on the file system.
*
* @param {string} filename The name of the feed without file extension.
* @param {Feed} feed
*/
export const writeFeed = (filename, feed) => writeFileSync(
path.join(config.outDir, `${filename}.xml`),
feed.rss2(),
)
Loading