all repos — home @ 3c10b19cc48e692a65ddf060620e5bcbfc14896c

Split content in typed directories
Tim Izzo tim@octree.ch
Fri, 25 Nov 2022 15:37:37 +0100
commit

3c10b19cc48e692a65ddf060620e5bcbfc14896c

parent

0b416492deb4663cbd967a4421fe7bf33f79fbff

M README.mdREADME.md

@@ -11,11 +11,6 @@ 1. Write some content in `writing/published`

2. Go to `weblogg` and generate data and feeds with `deno run --unstable -A mod.ts` 3. Go to `home` and generate posts from JSON-LD with `yarn build` -## ToDo - -- [ ] Adapt self URL in atom feeds for html and gmi -- [ ] Add summary to atom feeds - ## Inspiration https://www.ecliptik.com/
M bin/generate-posts.jsbin/generate-posts.js

@@ -13,7 +13,7 @@ for (const item of jsonLd.items) {

const htmlContent = template .replace("$CONTENT", item.content) .replace("$TITLE", item.name); - const pagePath = `${BUILD_DIR}/posts/${item["@id"]}.html`; + const pagePath = `${BUILD_DIR}/posts/html/${item["@id"]}.html`; fs.writeFileSync(pagePath, htmlContent); console.log(`Post generated: ${pagePath}`); }

@@ -21,8 +21,8 @@

// Set posts list in index.html const postsList = jsonLd.items .sort((itemA, itemB) => new Date(itemB.published) - new Date(itemA.published)) - .map((item) => { - const date = new Date(item.published).toLocaleDateString('fr-CH'); + .map(item => { + const date = new Date(item.published).toLocaleDateString("fr-CH"); return `<li><a href="/posts/${item["@id"]}.html">${date} - ${item.name}</a></li>`; }); const indexPage = indexPageTemplate.replace(
M build.shbuild.sh

@@ -1,17 +1,14 @@

#!/bin/bash -INPUT=${HOME_INPUT:-"../../writing/published"} -OUTPUT=${HOME_OUTPUT:-"../build/posts"} +INPUT=${HOME_INPUT:-"../writing/published"} +OUTPUT=${HOME_OUTPUT:-"build"} # Reset build directory rm -rf build -mkdir -p build/posts +mkdir -p build # Generate pages and data -cd generator -deno run --unstable -A mod.ts --input $INPUT --output $OUTPUT -cd .. -node bin/generate-posts.js +deno run --unstable -A generator/mod.ts --input $INPUT --output $OUTPUT # Generate styles yarn tw
M generator/atom.tsgenerator/feeds/atom.ts

@@ -1,6 +1,6 @@

-// @deno-types="./types.d.ts" +// @deno-types="../types.d.ts" -import { stringify } from "./deps.ts"; +import { stringify } from "../deps.ts"; export const getAtomFeed = (collection: Collection) => { return stringify({

@@ -21,7 +21,7 @@ "@href": `${collection.url}/atom-html.xml`, // TODO: Set gemini or html

"@rel": "self", }, ], - entry: collection.items?.map((item) => ({ + entry: collection.items?.map(item => ({ id: item.url, title: item.name, updated: item.published,

@@ -30,10 +30,10 @@ name: item.attributedTo.name,

uri: item.attributedTo.url, email: item.attributedTo.email, }, - content: { - '@type': 'html', - '#text': item.content - }, + content: { + "@type": "html", + "#text": item.content, + }, link: { "@href": item.url, "@rel": "alternate",
M generator/collection.tsgenerator/collection.ts

@@ -1,11 +1,11 @@

// @deno-types="./types.d.ts" import { getJsonLdCollection, postToJsonLd } from "./json-ld.ts"; -import { getFileContent } from "./lib.ts"; +import { getFileContent } from "./utils.ts"; export const getCollection = async ( postsPath: string, mediaType: MediaType -) => { +): Promise<Collection> => { let posts = []; for await (const post of Deno.readDir(postsPath)) {
A generator/converter.ts

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

+// @deno-types="./types.d.ts" + +import { + remarkParse, + unified, + rehypeStringify, + remarkRehype, + rehypeRaw, + rehypeSanitize, + rehypeAttr, +} from "./deps.ts"; + +type Converter = { + [key in MediaType]: null | ((mdContent: string) => Promise<string>); +}; + +const mdToHtml = async (mdContent: string) => { + const vfile = await unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeAttr, { properties: "attr" }) + .use(rehypeSanitize) + .use(rehypeStringify) + .process(mdContent); + return vfile.value; +}; + +const mdToMd = (mdContent: string) => Promise.resolve(mdContent); + +const converter: Converter = { + "text/html": mdToHtml, + "text/markdown": mdToMd, + "text/gemini": null, +}; + +export default converter;
M generator/deps.tsgenerator/deps.ts

@@ -4,5 +4,6 @@ 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 { default as rehypeAttr } from "https://esm.sh/rehype-attr@2.1.2"; export { datetime } from "https://deno.land/x/ptera/mod.ts"; export { stringify } from "https://deno.land/x/xml/mod.ts";
A generator/feeds/lib.ts

@@ -0,0 +1,31 @@

+// @deno-types="../types.d.ts" +import { getRssFeed } from "./rss.ts"; +import { getAtomFeed } from "./atom.ts"; + +export const generateJsonLd = async ( + collection: Collection, + outputPath: string +) => { + const jsonLdFilename = `${outputPath}/ld.json`; + await Deno.writeTextFile( + jsonLdFilename, + JSON.stringify(collection, undefined, 2) + ); + console.log(`JSON-LD written to ${jsonLdFilename}`); +}; + +export const generateFeeds = async ( + mediaType: MediaType, + collection: Collection, + outputPath: string +) => { + const atomFilename = `${outputPath}/atom.xml`; + const atomFeed = getAtomFeed(collection); + await Deno.writeTextFile(atomFilename, atomFeed); + console.log(`Atom feed for '${mediaType} written to ${atomFilename}`); + + const rssFilename = `${outputPath}/rss.xml`; + const rssFeed = getRssFeed(collection); + await Deno.writeTextFile(rssFilename, rssFeed); + console.log(`RSS feed for '${mediaType} written to ${rssFilename}`); +};
M generator/json-ld.tsgenerator/json-ld.ts

@@ -1,5 +1,5 @@

// @deno-types="./types.d.ts" -import { slugify } from "./lib.ts"; +import { slugify, getExtension } from "./utils.ts"; import { datetime, remarkParse,

@@ -9,7 +9,6 @@ remarkRehype,

rehypeRaw, rehypeSanitize, } from "./deps.ts"; -import { getExtension } from "./lib.ts"; const author = { type: "Person",
M generator/lib.tsgenerator/utils.ts

@@ -1,3 +1,5 @@

+// @deno-types="./types.d.ts" + export const getExtension = (mediaType: MediaType) => ({ "text/html": "html",
M generator/mod.tsgenerator/mod.ts

@@ -1,42 +1,29 @@

-// @deno-types="./types.d.ts" -import { parse } from "https://deno.land/std/flags/mod.ts" +import { parse } from "https://deno.land/std/flags/mod.ts"; +import { generateFeeds, generateJsonLd } from "./feeds/lib.ts"; +import { generatePages, generatePosts } from "./pages/lib.ts"; import { getCollection } from "./collection.ts"; -import { getRssFeed } from "./rss.ts"; -import { getAtomFeed } from "./atom.ts"; -import { getExtension } from "./lib.ts"; +import { getExtension } from "./utils.ts"; const args = parse(Deno.args); -if(!args.input) throw new Error("No --input provided"); +if (!args.input) throw new Error("No --input provided"); const POSTS_PATH = args.input; -if(!args.output) throw new Error("No --output provided"); +if (!args.output) throw new Error("No --output provided"); const OUTPUT_PATH = args.output; -await Deno.mkdir(OUTPUT_PATH, { recursive: true }); +const collectionHtml = await getCollection(POSTS_PATH, "text/html"); +await generateJsonLd(collectionHtml, OUTPUT_PATH); -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 generateContent = async (mediaType: 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}`); + const ext = getExtension(mediaType); + const outputPath = `${OUTPUT_PATH}/${ext}`; + await Deno.mkdir(`${outputPath}/posts`, { recursive: true }); + await generateFeeds(mediaType, collection, `${outputPath}/posts`); + await generatePosts(mediaType, collection, `${outputPath}/posts`); + await generatePages(mediaType, collection, outputPath); }; -await generateFeeds("text/html"); -await generateFeeds("text/gemini"); +await generateContent("text/html"); +await generateContent("text/markdown");
A generator/pages/lib.ts

@@ -0,0 +1,58 @@

+// @deno-types="../types.d.ts" +import { getExtension } from "../utils.ts"; +import { collectionToLinks, formatFilename } from "./utils.ts"; +import converter from "../converter.ts"; + +export const generatePosts = async ( + mediaType: MediaType, + collection: Collection, + outputPath: string +) => { + const ext = getExtension(mediaType); + const template = await getTemplate(`./templates/post.${ext}`); + + for (const item of collection.items) { + const postContent = template + .replace("$CONTENT", item.content) + .replace("$TITLE", item.name); + const postPath = `${outputPath}/${item["@id"]}.${ext}`; + + await Deno.writeTextFile(postPath, postContent); + + console.log(`Page ${postPath} added`); + } +}; + +export const generatePages = async ( + mediaType: MediaType, + collection: Collection, + outputPath: string +) => { + const srcPath = "src"; + const ext = getExtension(mediaType); + const template = await getTemplate(`./templates/page.${ext}`); + + for await (const pageInfo of Deno.readDir(srcPath)) { + if (!pageInfo.name.endsWith(".md")) continue; + + const pagePath = `${outputPath}/${formatFilename(pageInfo.name, ext)}`; + const postLinks = collectionToLinks(collection); + const rawMdPage = Deno.readTextFileSync(`${srcPath}/${pageInfo.name}`); + const mdPage = rawMdPage.replace(/\\?\$POSTS_LIST/, postLinks); + const page = (await converter[mediaType]?.(mdPage)) || "NOT SUPPORTED YET"; + const pageContent = template + .replace("$CONTENT", page) + .replace("$TITLE", "Tim Izzo @5ika.ch"); + + await Deno.writeTextFile(pagePath, pageContent); + } +}; + +const getTemplate = async (path: string) => { + try { + const template = await Deno.readTextFile(path); + return template; + } catch (_err) { + return "$CONTENT"; + } +};
A generator/pages/utils.ts

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

+// @deno-types="../types.d.ts" + +export const formatFilename = (filename: string, ext: string) => { + const rawName = filename.replace(/\.\w*$/gm, ""); + return `${rawName}.${ext}`; +}; + +export const collectionToLinks = (collection: Collection) => + collection.items + .filter(item => Boolean(item.published)) + .sort( + (itemA, itemB) => new Date(itemB.published) - new Date(itemA.published) + ) + ?.map(item => { + const date = new Date(item.published).toLocaleDateString("fr-CH"); + return `- [${date} - ${item.name}](/posts/${item["@id"]}.html)`; + }) + .join("\n");
M generator/rss.tsgenerator/feeds/rss.ts

@@ -1,6 +1,6 @@

-// @deno-types="./types.d.ts" +// @deno-types="../types.d.ts" -import { stringify } from "./deps.ts"; +import { stringify } from "../deps.ts"; export const getRssFeed = (collection: Collection) => { return stringify({

@@ -14,7 +14,7 @@ link: collection.url,

lastBuildDate: collection.published, ttl: 1800, - item: collection.items.map((item) => ({ + item: collection.items.map(item => ({ title: item.name, link: item.url, guid: item.url,
A generator/templates/page.html

@@ -0,0 +1,31 @@

+<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link + rel="icon" + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦌</text></svg>" + /> + <link href="/style.css" rel="stylesheet" /> + <link + href="/posts/atom.xml" + type="application/atom+xml" + rel="alternate" + title="5ika's posts" + /> + <link + href="/posts/rss.xml" + type="application/rss+xml" + rel="alternate" + title="5ika's posts" + /> + <title>$TITLE</title> + </head> + <body> + <main class="page"> + <h1 class="mb-8">Tim Izzo @<a href="/">5ika.ch</a></h1> + $CONTENT + </main> + </body> +</html>
M generator/types.d.tsgenerator/types.d.ts

@@ -18,7 +18,7 @@ mediaType: MediaType;

name: string; url: string; attributedTo: Person; - published?: string; + published: string; content: string; }
M package.jsonpackage.json

@@ -6,7 +6,7 @@ "author": "Tim Izzo - 5ika",

"license": "MIT", "private": true, "scripts": { - "tw": "tailwindcss -i ./src/style.css -o ./build/style.css --minify", + "tw": "tailwindcss -i ./src/style.css -o ./build/html/style.css --minify", "build": "./build.sh" }, "dependencies": {
D src/index.html

@@ -1,123 +0,0 @@

-<!DOCTYPE html> -<html> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <link - rel="icon" - href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦌</text></svg>" - /> - <link href="/style.css" rel="stylesheet" /> - <link - href="/posts/atom-html.xml" - type="application/atom+xml" - rel="alternate" - title="5ika's posts" - /> - <link - href="/posts/rss-html.xml" - type="application/rss+xml" - rel="alternate" - title="5ika's posts" - /> - <title>Tim Izzo @5ika.ch</title> - </head> - <body> - <main class='home'> - <h1 class="mb-8">Tim Izzo @<a href="/">5ika.ch</a></h1> - <blockquote> - <p>Dev &amp; Ops 🧑‍💻 Open-Source 👐 Écologie 🌱 Sobriété numérique ✨</p> - </blockquote> - <p> - Partisan d&#39;un Internet ouvert, sobre et distribué, je développe et - met en place des outils digitaux par mon activité d&#39;indépendant - ainsi qu&#39;au travers de l&#39;entreprise auto-gouvernée - <a href="https://octree.ch" target="_blank" title="null">Octree</a>. En - parallèle, j&#39;enseigne le métier de développeur/euse à - <a - href="https://www.creageneve.com/bachelor/developpement-web-et-applications/" - target="_blank" - title="null" - >CREA Genève</a - >. - </p> - <h2 id="blog">Blog</h2> - <ul> - $POSTS_LIST - </ul> - <h2 id="projets">Projets</h2> - <ul> - <li> - 🚗 - <a href="https://caroster.io/" target="_blank" title="null" - >Caroster</a - > - <small>[Octree]</small> - </li> - <li> - ♻️ <a href="https://r-21.ch" target="_blank" title="null">R-21</a> - <small>[Octree]</small> - </li> - <li> - 🏢 - <a href="https://evospe.ch" target="_blank" title="null">EVOSPE</a> - <small>[Octree]</small> - </li> - <li> - 🗳️ - <a - href="https://www.demaincestaujourdhui.online/" - target="_blank" - title="null" - >Civic Echo</a - > - <small>[Octree]</small> - </li> - <li> - 👐 - <a href="https://open-ge.ch/" target="_blank" title="null" - >Genève Open-Source</a - > - </li> - <li> - 🔓 - <a - href="https://github.com/5ika/denotion" - target="_blank" - title="null" - >Denotion</a - > - </li> - <li> - 🚋 - <a href="https://github.com/5ika/tipigee" target="_blank" title="null" - >tipigee</a - > - <small><em>inactif</em></small> - </li> - </ul> - <h2 id="talk">Talk</h2> - <ul> - <li> - <a - href="https://www.youtube.com/watch?v=2_Cx-HvxVX0" - target="_blank" - title="null" - >21.02.2019 - Holacracy &amp; Devops, Retour d&#39;expérience</a - > - </li> - </ul> - <h2 id="contact">Contact</h2> - <ul> - <li> - ✉️ <a href="mailto:tim@5ika.ch" target="" title="null">tim@5ika.ch</a> - </li> - <li> - 🐘 - <a href="https://tooting.ch/@5ika" target="_blank" rel="me">Mastodon</a - > - </li> - </ul> - </main> - </body> -</html>
A src/index.md

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

+> Dev & Ops 🧑‍💻 Open-Source 👐 Écologie 🌱 Sobriété numérique ✨ + +Partisan d'un Internet ouvert, sobre et distribué, je développe et met en place des outils digitaux +par mon activité d'indépendant ainsi qu'au travers de l'entreprise auto-gouvernée [Octree](https://octree.ch). +En parallèle, j'enseigne le métier de développeur/euse à [CREA Genève](https://www.creageneve.com/bachelor/developpement-web-et-applications/). + +## Blog + +\$POSTS_LIST + +## Projets + +* 🚗 [Caroster](https://caroster.io/) \[Octree\] +* ♻️ [R-21](https://r-21.ch) \[Octree\] +* 🏢 [EVOSPE](https://evospe.ch) \[Octree\] +* 🗳️ [Civic Echo](https://www.demaincestaujourdhui.online/) \[Octree\] +* 👐 [Genève Open-Source](https://open-ge.ch/) +* 🔓 [Denotion](https://github.com/5ika/denotion) +* 🚋 [tipigee](https://github.com/5ika/tipigee) _inactif_ + +## Talk + +* [21.02.2019 - Holacracy & Devops, Retour d'expérience](https://www.youtube.com/watch?v=2_Cx-HvxVX0) + +## Contact + +* ✉️ [tim@5ika.ch](mailto:tim@5ika.ch) +* 🐘 [Mastodon](https://tooting.ch/@5ika)<!--rehype:rel=me&target=_blank-->
M src/style.csssrc/style.css

@@ -52,8 +52,12 @@ @apply dark:text-indigo-400;

@apply dark:hover:text-indigo-200; } + li { + list-style: none; + } + .post li { - list-style: '-'; + list-style: "-"; padding-left: 0.5rem; margin-left: 1rem; }
M src/template.htmlgenerator/templates/post.html

@@ -9,13 +9,13 @@ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦌</text></svg>"

/> <link href="/style.css" rel="stylesheet" /> <link - href="/posts/atom-html.xml" + href="/posts/atom.xml" type="application/atom+xml" rel="alternate" title="5ika's posts" /> <link - href="/posts/rss-html.xml" + href="/posts/rss.xml" type="application/rss+xml" rel="alternate" title="5ika's posts"

@@ -23,7 +23,7 @@ />

<title>$TITLE</title> </head> <body> - <main class='post'> + <main class="post"> <h1 class="mb-8">Tim Izzo @<a href="/">5ika.ch</a></h1> $CONTENT </main>