Merge branch 'main' of ssh://5ika.ch:1917/home
jump to
@@ -1,4 +1,5 @@
# 5ika.ch + ## Generate blog **Local files must match following structure:**@@ -10,14 +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 - -- [ ] Remove RSS, only Atom ? -- [ ] Add link to Atom feed on site -- [ ] Ajouter OpenGraph pour les RS -- [ ] Adapt self URL in atom feeds for html and gmi -- [ ] Add summary to atom feeds ## Inspiration
@@ -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(); + .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(
@@ -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
@@ -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,@@ -29,6 +29,10 @@ author: {
name: item.attributedTo.name, uri: item.attributedTo.url, email: item.attributedTo.email, + }, + content: { + "@type": "html", + "#text": item.content, }, link: { "@href": item.url,
@@ -1,18 +1,19 @@
// @deno-types="./types.d.ts" -import { getJsonLdCollection, postToJsonLd } from "./json-ld.ts"; -import { getFileContent } from "./lib.ts"; +import { getFileContent } from "./utils.ts"; +import { postToJson } from "./json-ld/post.ts"; +import { formatJsonCollection } from "./json-ld/collection.ts"; export const getCollection = async ( postsPath: string, mediaType: MediaType -) => { +): Promise<Collection> => { let posts = []; for await (const post of Deno.readDir(postsPath)) { const mdContent = await getFileContent(`${postsPath}/${post.name}`); - const jsonLdPost = await postToJsonLd(mdContent, mediaType); + const jsonLdPost = await postToJson(mdContent, mediaType); posts.push(jsonLdPost); } - return getJsonLdCollection(posts); + return formatJsonCollection(posts); };
@@ -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;
@@ -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";
@@ -0,0 +1,19 @@
+// @deno-types="../types.d.ts" +import { getRssFeed } from "./rss.ts"; +import { getAtomFeed } from "./atom.ts"; + +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}`); +};
@@ -1,5 +1,5 @@
-// @deno-types="./types.d.ts" -import { slugify } from "./lib.ts"; +// @deno-types="../types.d.ts" +import { slugify, getExtension } from "../utils.ts"; import { datetime, remarkParse,@@ -8,29 +8,10 @@ rehypeStringify,
remarkRehype, rehypeRaw, rehypeSanitize, -} from "./deps.ts"; -import { getExtension } from "./lib.ts"; +} from "../deps.ts"; +import author from "../../templates/author.json" assert { type: "json" }; -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 ( +export const postToJson = async ( mdContent: string, mediaType: MediaType ): Promise<Post> => {
@@ -0,0 +1,13 @@
+// @deno-types="../types.d.ts" +import { datetime } from "../deps.ts"; +import author from "../../templates/author.json" assert { type: "json" }; +import collection from "../../templates/collection.json" assert { type: "json" }; + +export const formatJsonCollection = (items: Post[]): Collection => + ({ + ...collection, + totalItems: items.length, + items: items, + attributedTo: author, + published: datetime("").toISO(), + } as Collection);
@@ -0,0 +1,14 @@
+export const copyStatics = async () => { + const srcPath = "src"; + const dstPath = "build/json"; + + for await (const pageInfo of Deno.readDir(srcPath)) { + if (!pageInfo.name.endsWith(".json")) continue; + + await Deno.copyFile( + `${srcPath}/${pageInfo.name}`, + `${dstPath}/${pageInfo.name}` + ); + console.log(`${pageInfo.name} copied to ${dstPath}`); + } +};
@@ -1,8 +1,11 @@
+// @deno-types="./types.d.ts" + export const getExtension = (mediaType: MediaType) => ({ "text/html": "html", "text/markdown": "md", "text/gemini": "gmi", + "application/json": "json", }[mediaType] || "md"); export const getFileContent = async (filePath: string): Promise<string> => {
@@ -1,42 +1,43 @@
-// @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 } 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"; +import { copyStatics } from "./json-ld/lib.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 }); +/** + * Generate content for mime types + */ +const generateContent = async (mediaType: MediaType) => { + const collection = await getCollection(POSTS_PATH, mediaType); + 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); +}; -const collection = await getCollection(POSTS_PATH, "text/html"); -const jsonLdFilename = `${OUTPUT_PATH}/ld.json`; +await generateContent("text/html"); +await generateContent("text/markdown"); + +/** + * Generate JSON-LD content + */ +const outputPath = `${OUTPUT_PATH}/json`; +await Deno.mkdir(outputPath, { recursive: true }); +await copyStatics(); +const jsonCollection = await getCollection(POSTS_PATH, "text/html"); await Deno.writeTextFile( - jsonLdFilename, - JSON.stringify(collection, undefined, 2) + `${outputPath}/collection.json`, + JSON.stringify(jsonCollection, null, 4) ); -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/html"); -await generateFeeds("text/gemini"); +console.log(`JSON+LD collection written to ${outputPath}/collection.json`);
@@ -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)) || mdPage; + 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"; + } +};
@@ -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");
@@ -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,
@@ -18,7 +18,7 @@ mediaType: MediaType;
name: string; url: string; attributedTo: Person; - published?: string; + published: string; content: string; }
@@ -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": {
@@ -1,124 +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 & Ops 🧑💻 Open-Source 👐 Écologie 🌱 Sobriété numérique ✨</p> - </blockquote> - <p> - 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 - <a href="https://octree.ch" target="_blank" title="null">Octree</a>. En - parallèle, j'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 & Devops, Retour d'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://fosstodon.org/web/@5ika" target="_blank" title="null" - >Mastodon</a - > - </li> - </ul> - </main> - </body> -</html>
@@ -0,0 +1,15 @@
+{ + "@context": "http://schema.org/", + "@type": "Person", + "@id": "https://5ika.ch", + "name": "Tim Izzo", + "email": "tim@5ika.ch", + "givenName": "Timothy", + "familyName": "Izzo", + "alternateName": "5ika", + "jobTitle": "IT Engineer", + "url": "https://5ika.ch", + "worksFor": ["https://octree.ch"], + "image": "https://tooting.ch/system/accounts/avatars/107/355/320/386/938/203/original/7e6c00f72ed8b6db.jpeg", + "sameAs": "https://tooting.ch/users/5ika" +}
@@ -0,0 +1,28 @@
+> Dev & Ops 👨💻 Open-Source 👐 Écologie 🌱 Sobriété numérique ✨ + +Partisan d'un Internet ouvert, sobre et distribué, je cultive 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-->
@@ -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; }
@@ -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="page"> <h1 class="mb-8">Tim Izzo @<a href="/">5ika.ch</a></h1> $CONTENT </main>
@@ -0,0 +1,7 @@
+{ + "@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" +}
@@ -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="post"> + <h1 class="mb-8">Tim Izzo @<a href="/">5ika.ch</a></h1> + $CONTENT + </main> + </body> +</html>