all repos — home @ 73bac4ae5a258be358ceda132ec3169d9451a68c

Add generator
Tim Izzo tim@octree.ch
Fri, 28 Oct 2022 18:36:41 +0200
commit

73bac4ae5a258be358ceda132ec3169d9451a68c

parent

092274c2a6f444b04305a6745c0520c728032569

M README.mdREADME.md

@@ -13,8 +13,7 @@ 3. Go to `home` and generate posts from JSON-LD with `yarn build`

## ToDo -- [ ] Use Tailwind through Deno to avoid node_modules -- [ ] Merge `weblogg` and `home` +- [ ] Use Tailwind through Deno and remove package.json and all npm related - [ ] Adapt git hook on server to deploy on each push
M build.shbuild.sh

@@ -3,7 +3,9 @@

rm -r build mkdir -p build/posts -cp ../weblogg/build/* build/posts +cd generator +deno run --unstable -A mod.ts +cd .. node bin/generate-posts.js yarn tw
A generator/atom.ts

@@ -0,0 +1,35 @@

+// @deno-types="./types.d.ts" + +import { stringify } from "./deps.ts"; + +export const getAtomFeed = (collection: Collection) => { + return stringify({ + xml: { "@version": "1.0", "@encoding": "UTF-8" }, + feed: { + "@xmlns": "http://www.w3.org/2005/Atom", + title: collection.name, + subtitle: collection.summary, + updated: collection.published, + link: [ + { + "@href": collection.url, + "@rel": "alternate", + }, + { + "@href": `${collection.url}/atom.xml`, + "@rel": "self", + }, + ], + entry: collection.items?.map(item => ({ + id: item["@id"], + title: item.name, + updated: item.published, + author: item.attributedTo, + link: { + "@href": item.url, + "@rel": "alternate", + }, + })), + }, + }); +};
A generator/collection.ts

@@ -0,0 +1,18 @@

+// @deno-types="./types.d.ts" +import { getJsonLdCollection, postToJsonLd } from "./json-ld.ts"; +import { getFileContent } from "./lib.ts"; + +export const getCollection = async ( + postsPath: string, + mediaType: MediaType +) => { + let posts = []; + + for await (const post of Deno.readDir(postsPath)) { + const mdContent = await getFileContent(`${postsPath}/${post.name}`); + const jsonLdPost = await postToJsonLd(mdContent, mediaType); + posts.push(jsonLdPost); + } + + return getJsonLdCollection(posts); +};
A generator/deps.ts

@@ -0,0 +1,8 @@

+export { unified } from "https://esm.sh/unified@10.1.2"; +export { default as remarkParse } from "https://esm.sh/remark-parse@10"; +export { default as remarkRehype } from "https://esm.sh/remark-rehype@10"; +export { default as rehypeRaw } from "https://esm.sh/rehype-raw"; +export { default as rehypeSanitize } from "https://esm.sh/rehype-sanitize"; +export { default as rehypeStringify } from "https://esm.sh/rehype-stringify@9.0.3"; +export { datetime } from "https://deno.land/x/ptera/mod.ts"; +export { stringify } from "https://deno.land/x/xml/mod.ts";
A generator/json-ld.ts

@@ -0,0 +1,72 @@

+// @deno-types="./types.d.ts" +import { slugify } from "./lib.ts"; +import { + datetime, + remarkParse, + unified, + rehypeStringify, + remarkRehype, + rehypeRaw, + rehypeSanitize, +} from "./deps.ts"; +import { getExtension } from "./lib.ts"; + +const author = { + type: "Person", + name: "Tim Izzo", + email: "tim@5ika.ch", + url: "https://5ika.ch", +}; + +export const getJsonLdCollection = (items: Post[]): Collection => ({ + "@context": "https://www.w3.org/ns/activitystreams", + name: "Le blog de 5ika", + url: "https://5ika.ch/posts", + type: "Collection", + summary: "Retrouvez les postes de 5ika.ch dans votre feed", + totalItems: items.length, + items: items, + attributedTo: author, + published: datetime("").toISO(), +}); + +export const postToJsonLd = async ( + mdContent: string, + mediaType: MediaType +): Promise<Post> => { + const tokens: MdTokens = unified().use(remarkParse).parse(mdContent); + const title = tokens.children.find( + token => token.type === "heading" && token.depth === 1 + )?.children?.[0]?.value; + const rawDate = tokens.children?.find(token => token.type === "blockquote") + ?.children?.[0]?.children?.[0]?.value; + const dateTime = rawDate && datetime().parse(rawDate, "dd.MM.YYYY").toISO(); + const slug = title ? slugify(title) : "No title"; + const ext = getExtension(mediaType); + const url = `https://5ika.ch/posts/${slug}.${ext}`; + + let content = mdContent; + + if (mediaType === "text/html") { + const vfile = await unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeSanitize) + .use(rehypeStringify) + .process(mdContent); + content = vfile.value; + } + + return { + "@context": "https://www.w3.org/ns/activitystreams", + "@id": slug, + url, + mediaType, + content, + type: "Article", + name: title || "No title", + attributedTo: author, + published: dateTime, + }; +};
A generator/lib.ts

@@ -0,0 +1,28 @@

+export const getExtension = (mediaType: MediaType) => + ({ + "text/html": "html", + "text/markdown": "md", + "text/gemini": "gmi", + }[mediaType] || "md"); + +export const getFileContent = async (filePath: string): Promise<string> => { + const decoder = new TextDecoder("utf-8"); + const mdFile = await Deno.readFile(filePath); + return decoder.decode(mdFile); +}; + +export const slugify = (str: string) => { + str = str.replace(/^\s+|\s+$/g, ""); + str = str.toLowerCase(); + const from = + "ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđ߯a·/_,:;"; + const to = + "AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------"; + for (var i = 0, l = from.length; i < l; i++) { + str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i)); + } + return str + .replace(/[^a-z0-9 -]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); +};
A generator/mod.ts

@@ -0,0 +1,37 @@

+// @deno-types="./types.d.ts" +import { getCollection } from "./collection.ts"; +import { getRssFeed } from "./rss.ts"; +import { getAtomFeed } from "./atom.ts"; +import { getExtension } from "./lib.ts"; + +const POSTS_PATH = "../../writing/published"; +const OUTPUT_PATH = "../build/posts"; + +await Deno.mkdir(OUTPUT_PATH, { recursive: true }); + +const collection = await getCollection(POSTS_PATH, "text/html"); +const jsonLdFilename = `${OUTPUT_PATH}/ld.json`; +await Deno.writeTextFile( + jsonLdFilename, + JSON.stringify(collection, undefined, 2) +); +console.log(`JSON-LD written to ${jsonLdFilename}`); + +const generateFeeds = async (mediaType: MediaType) => { + const extension = getExtension(mediaType); + const collection = await getCollection(POSTS_PATH, mediaType); + + const atomFilename = `${OUTPUT_PATH}/atom-${extension}.xml`; + const atomFeed = getAtomFeed(collection); + await Deno.writeTextFile(atomFilename, atomFeed); + console.log(`Atom feed for '${mediaType} written to ${atomFilename}`); + + const rssFilename = `${OUTPUT_PATH}/rss-${extension}.xml`; + const rssFeed = getRssFeed(collection); + await Deno.writeTextFile(rssFilename, rssFeed); + console.log(`RSS feed for '${mediaType} written to ${rssFilename}`); +}; + +await generateFeeds("text/markdown"); +await generateFeeds("text/html"); +await generateFeeds("text/gemini");
A generator/rss.ts

@@ -0,0 +1,26 @@

+// @deno-types="./types.d.ts" + +import { stringify } from "./deps.ts"; + +export const getRssFeed = (collection: Collection) => { + return stringify({ + xml: { "@version": "1.0", "@encoding": "UTF-8" }, + rss: { + "@version": "2.0", + channel: { + title: collection.name, + description: collection.summary, + link: collection.url, + lastBuildDate: collection.published, + ttl: 1800, + + item: collection.items.map(item => ({ + title: item.name, + link: item.url, + guid: item["@id"], + pubDate: item.published, + })), + }, + }, + }); +};
A generator/types.d.ts

@@ -0,0 +1,42 @@

+interface Collection { + "@context": "https://www.w3.org/ns/activitystreams"; + type: "Collection"; + name: string; + url: string; + items: Post[]; + summary: string; + published: string; + totalItems: number; + attributedTo: Person; +} + +interface Post { + "@context": "https://www.w3.org/ns/activitystreams"; + "@id": string; + type: "Article"; + mediaType: MediaType; + name: string; + url: string; + attributedTo: Person; + published?: string; + content: string; +} + +interface Person { + name: string; + email?: string; + url?: string; +} + +type MediaType = "text/markdown" | "text/html" | "text/gemini"; + +interface MdTokens { + children: MdToken[]; +} + +interface MdToken { + type: string; + value: string; + children: MdToken[]; + depth?: number; +}