aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-04-08 03:05:49 +0100
committerLeonardo Bishop <me@leonardobishop.com>2025-04-08 03:05:49 +0100
commitb2b46a361589521d5f8ea1fca52ad41722367998 (patch)
tree1fa404a890d95a2745f9758dd52011316a1290c6
parent5dce939250be34f3fdb556de07bb4ed081dc7077 (diff)
Add basic support for atom feeds
-rw-r--r--app/builder/buildProject.ts13
-rw-r--r--app/builder/discoverFeed.ts67
-rw-r--r--app/builder/pageDirectory.ts50
-rw-r--r--package-lock.json14
-rw-r--r--package.json5
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": {