Skip to content

Commit

Permalink
Add more RSS feeds
Browse files Browse the repository at this point in the history
Summary:
* Add separate feeds for notes and articles
* Add feed with articles excerpt only
* Add feed with articles excerpts and complete notes
* Add new feeds to `size-limit`
  • Loading branch information
meduzen authored Nov 19, 2023
1 parent 43f7b9a commit ba6ec8d
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 35 deletions.
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(),
)

0 comments on commit ba6ec8d

Please sign in to comment.