diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2025-04-08 03:05:49 +0100 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2025-04-08 03:05:49 +0100 |
| commit | b2b46a361589521d5f8ea1fca52ad41722367998 (patch) | |
| tree | 1fa404a890d95a2745f9758dd52011316a1290c6 | |
| parent | 5dce939250be34f3fdb556de07bb4ed081dc7077 (diff) | |
Add basic support for atom feeds
| -rw-r--r-- | app/builder/buildProject.ts | 13 | ||||
| -rw-r--r-- | app/builder/discoverFeed.ts | 67 | ||||
| -rw-r--r-- | app/builder/pageDirectory.ts | 50 | ||||
| -rw-r--r-- | package-lock.json | 14 | ||||
| -rw-r--r-- | package.json | 5 |
5 files changed, 146 insertions, 3 deletions
diff --git a/app/builder/buildProject.ts b/app/builder/buildProject.ts index 700e0ec..2f7aec4 100644 --- a/app/builder/buildProject.ts +++ b/app/builder/buildProject.ts @@ -5,6 +5,7 @@ import path from 'path'; import { logger } from '../logger.js'; import glob from 'glob'; import { process as processCss } from './processCss.js'; +import { discoverFeed } from './discoverFeed.js'; export async function buildPages(verbose: boolean = true): Promise<{ success: boolean, errors: number, pageDirectory: PageDirectory}> { // Recreate output directory @@ -44,6 +45,18 @@ export async function buildPages(verbose: boolean = true): Promise<{ success: bo if (verbose) logger.info(`Rendered ${pagesRendered} of ${pagesCount} pages.`); + // Discover feeds + if (verbose) logger.info(`Discovering feeds...`); + const feeds = pageDirectory.getFeeds(); + for (const feed of feeds) { + try { + await discoverFeed(feed, pageDirectory); + } catch (e) { + logger.error(`Failed to discover feed ${feed.title}: ${e.message}`); + } + } + + //TODO move to util const ensureParentDirExists = (file: string) => { const joinedOutputPath = path.join(process.env.OUTPUT_DIR, 'static', file); diff --git a/app/builder/discoverFeed.ts b/app/builder/discoverFeed.ts new file mode 100644 index 0000000..5ea1a16 --- /dev/null +++ b/app/builder/discoverFeed.ts @@ -0,0 +1,67 @@ +import path from "path"; +import fs from "fs"; +import { Feed, PageDirectory } from "./pageDirectory.js"; +import { logger } from "../logger.js"; + +export async function discoverFeed(feed: Feed, pageDirectory: PageDirectory): Promise<boolean> { + const entries = []; + + const titleConfigPath = feed.paramStrategy?.title?.from || "title"; + const updatedConfigPath = feed.paramStrategy?.date?.from || "date"; + const descriptionConfigPath = feed.paramStrategy?.description?.from || "description"; + //todo support actual discovery strategies + + for (const page of pageDirectory.getPagesBeginningWith(feed.route)) { + if (page.name === 'index') { + continue; + } + const entry = { + title: page.config[titleConfigPath] || page.name, + updated: page.config[updatedConfigPath] || new Date(0), + id: page.name, + url: `${feed.url}/${page.name}`, + description: page.config[descriptionConfigPath] || "", + // description: page.html?.length > 100 ? `${page.html?.substring(0, 100)}...` || "" : page.html || "", + }; + entries.push(entry); + } + + feed.entries = entries; + feed.entries.sort((a, b) => b.updated.getTime() - a.updated.getTime()); + feed.updated = new Date(0); + for (const entry of feed.entries) { + if (entry.updated > feed.updated) { + feed.updated = entry.updated; + } + } + + const atomFeed = `<feed xmlns="http://www.w3.org/2005/Atom"> +<title>${feed.title}</title> +<updated>${feed.updated.toISOString()}</updated> +<link rel="self" href="${feed.url}/atom.xml" type="application/atom+xml"/> +<link rel="alternate" href="${feed.url}" type="text/html"/> +${feed.entries + .map((entry) => { + return `<entry> +<title>${entry.title}</title> +<updated>${entry.updated.toISOString()}</updated> +<id>${entry.id}</id> +<link rel="alternate" href="${entry.url}" type="text/html"/> +<summary>${entry.description}</summary> +</entry>`; + }) + .join("\n")} +</feed>`; + + try { + const file = feed.buildPath; + const dir = path.dirname(file); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, atomFeed); + } catch (e) { + logger.error(`Failed to write feed ${feed.buildPath}: ${e.message}`); + return false; + } +}
\ No newline at end of file diff --git a/app/builder/pageDirectory.ts b/app/builder/pageDirectory.ts index f95bc51..b7b3d04 100644 --- a/app/builder/pageDirectory.ts +++ b/app/builder/pageDirectory.ts @@ -8,6 +8,7 @@ import matter from "gray-matter"; import { markedHighlight } from "marked-highlight"; import hljs from "highlight.js"; import hljsDefineSolidity from 'highlightjs-solidity'; +import YAML from 'yaml' hljsDefineSolidity(hljs); hljs.initHighlightingOnLoad(); @@ -47,6 +48,7 @@ export class PageDirectory { private pagesPath: string; private pages: Record<string, Page> = {}; + private feeds: Record<string, Feed> = {}; private lastFullBuild: number; constructor(pagesPath: string) { @@ -62,6 +64,10 @@ export class PageDirectory { for (const page in localPages) { await this.loadPage(localPages[page]); } + const localFeeds = glob.sync(`**/feed.yml`, { cwd: this.pagesPath }); + for (const feed in localFeeds) { + await this.loadFeed(localFeeds[feed]); + } this.lastFullBuild = Date.now(); }; @@ -97,6 +103,28 @@ export class PageDirectory { return this.pages[route]; }; + public loadFeed = async (path: string): Promise<Feed> => { + let feed; + try { + feed = YAML.parse(loadRaw(`${this.pagesPath}/${path}`)); + } catch (e) { + logger.error(`Failed to read feed ${path}: ${e.message}`); + return undefined; + } + + this.feeds[feed.pages] = { + title: feed.title, + route: feed.pages, + url: feed.url, + buildPath: `${process.env.OUTPUT_DIR}/${feed.pages}/atom.xml`, + paramStrategy: feed.paramStrategy, + updated: new Date(0), + entries: [] + } + + return this.feeds[feed.pages]; + }; + public removePage = (page: string): void => { let route = page.replace(/\.[^.]*$/, ""); delete this.pages[route]; @@ -115,6 +143,10 @@ export class PageDirectory { return Object.values(this.pages); } + public getFeeds(): Feed[] { + return Object.values(this.feeds); + } + public getPagesBeginningWith(prefix: string): Page[] { return Object.values(this.pages).filter((page) => page.route.startsWith(prefix) @@ -134,3 +166,21 @@ export type Page = { buildTime: number; config: any; }; + +export type Feed = { + route: string; + title: string; + url: string; + buildPath: string; + paramStrategy: any; + updated: Date; + entries: FeedEntry[]; +} + +export type FeedEntry = { + title: string; + updated: Date; + id: string; + url: string; + description: string; +}
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f903e65..0576cb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,8 @@ "sass": "^1.66.1", "uglify-js": "^3.17.4", "winston": "^3.3.3", - "ws": "^8.13.0" + "ws": "^8.13.0", + "yaml": "^2.7.1" }, "devDependencies": { "@types/clean-css": "^4.2.6", @@ -3282,6 +3283,17 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 6c78a78..9d28679 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "panulat", - "version": "1.4", + "version": "1.5", "description": "", "main": "app/index.mjs", "scripts": { @@ -29,7 +29,8 @@ "sass": "^1.66.1", "uglify-js": "^3.17.4", "winston": "^3.3.3", - "ws": "^8.13.0" + "ws": "^8.13.0", + "yaml": "^2.7.1" }, "type": "module", "devDependencies": { |
