summaryrefslogtreecommitdiff
path: root/src/build
diff options
context:
space:
mode:
Diffstat (limited to 'src/build')
-rw-r--r--src/build/article.mjs31
-rw-r--r--src/build/i18n.mjs85
-rw-r--r--src/build/index.mjs208
-rw-r--r--src/build/list-articles.mjs7
-rw-r--r--src/build/loadMD.mjs79
-rw-r--r--src/build/rss.mjs26
-rw-r--r--src/build/special_box.mjs15
-rw-r--r--src/build/to-html.mjs12
-rw-r--r--src/build/utils.mjs10
9 files changed, 373 insertions, 100 deletions
diff --git a/src/build/article.mjs b/src/build/article.mjs
new file mode 100644
index 0000000..1a7b09b
--- /dev/null
+++ b/src/build/article.mjs
@@ -0,0 +1,31 @@
+// Set of functions to handle articles
+
+function getArticleYear(article) {
+ if (article.metaData.pubDate.getFullYear) {
+ return article.metaData.pubDate.getFullYear();
+ }
+
+ if (article.metaData.pubDate.getUTCFullYear) {
+ return article.metaData.pubDate.getUTCFullYear();
+ }
+
+ return 0;
+}
+
+function getArticleDate(article) {
+ if (article.metaData.pubDate.getDate) {
+ return article.metaData.pubDate.getFullYear() * 100 + article.metaData.pubDate.getDate();
+ }
+
+ if (article.metaData.pubDate.getUTCDate) {
+ return article.metaData.pubDate.getUTCFullYear() * 100 + article.metaData.pubDate.getDate();
+ }
+
+ return 0;
+}
+
+function cmpArticles(a, b) {
+ return getArticleDate(b) - getArticleDate(a);
+}
+
+export {getArticleDate, getArticleYear, cmpArticles};
diff --git a/src/build/i18n.mjs b/src/build/i18n.mjs
new file mode 100644
index 0000000..d1cebe6
--- /dev/null
+++ b/src/build/i18n.mjs
@@ -0,0 +1,85 @@
+
+const i18n = {
+ fr: {
+ intro: {
+ 'description': 'Éternel étudiant en Math-Info.',
+ 'about': 'Autodidacte passionné,<br><span class="type_wrap"><span class="type">désormais ingénieur.</span></span>',
+ 'about_tags': 'GNU\\Linux, C, C++, Python, Math, autohébergement, décentralisation, P2P, commun, ... <br> ',
+ },
+ title: {
+ 'main': 'ache: Blog personnel',
+ 'home': 'L\'accueil',
+ 'git': 'Dépôt git personnel',
+ 'mastodon': 'Mon mastodon',
+ },
+ articles: [
+ 'framasoft-et-les-mascottes-du-libre.md',
+ 'les-trains-et-la-publicité.md',
+ 'formats-images-web.md',
+ 'bizarreries-du-langage-c.md',
+ 'retour-sur-laoc-2021-semaine-1.md',
+ '2FA-discord-sur-pc.md',
+ 'duckduckgo-google-en-mieux.md',
+ ],
+ rss: {
+ 'title': 'ache: Blog personnel',
+ 'quick_description': 'Programmation, Algorithmique, Système, *pick you poison*',
+ 'description': `<b>Ceci est un flux RSS</b> à destination des agrégateurs de contenu.<br>\nEn tant qu'être humain vous cherchez certainement <a href="https://ache.one/fr/">mon site web</a>.`
+ },
+ toc_keyword: "Sommaire",
+ tag_desc: "Liste des articles ayant le tag",
+ index_desc: "Blog personnel à propos de programmation, logiciel libre et autohébergement. Essayons de rendre le monde meilleur.",
+ like_title: "Si vous avez aimez cet article cliquez sur le cœur !",
+ like_text: "Vous pouvez même envoyez plusieurs cœurs ! <br><span class=\"likesNotes\">Le délais d'attente entre deux cœurs double à chaque fois.</span>",
+ },
+ en: {
+ intro: {
+ 'description': 'Eternal student in computer science.',
+ 'quick_description': 'Programmation, Algorithmique, Système, *pick you poison*',
+ 'about': 'Self-taught developper,<br><span class="type_wrap"><span class="type">now engineer.</span></span>',
+ 'about_tags': 'GNU\\Linux, C, C++, Python, Math, self-hosted, dececntralisation, P2P, ... <br>',
+ },
+ title: {
+ 'main': 'ache: Personal blog',
+ 'home': 'Home',
+ 'git': 'Personnel git repository',
+ 'mastodon': 'Mastodon account',
+ },
+ articles: [
+ 'rail-and-advertising.md',
+ 'web-image-formats.md',
+ 'c-language-quirks.md',
+ ],
+ rss: {
+ 'title': 'ache: Personal blog',
+ 'description': `<b>This is a RSS feed</b> for content agreagator.<br>\nAs a human being, you are certainly looking for <a href="https://ache.one/en/">my website</a>.`
+ },
+ toc_keyword: "Table of contents",
+ tag_desc: "List of articles with the tag",
+ index_desc: "Personal blog about programming, free software and self-hosting. Let's try to make the world a better place.",
+ like_title: "If you liked this article click on the heart!",
+ like_text: "You can even send multiple hearts! <br><span class=\"likesNotes\">The waiting time between two hearts doubles each time.</span>",
+ }
+}
+
+const lang_desc = {
+ fr: "Version Française",
+ en: "English version",
+}
+
+export function addDescription(alt_lang) {
+ if (alt_lang) {
+ alt_lang.forEach(e => {
+ e.description = lang_desc[e.lang]
+ });
+ }
+
+ return alt_lang;
+}
+
+export function getTocHeading() {
+ return Object.entries(i18n).map(([_, v]) => v.toc_keyword).join('|');
+}
+
+
+export default i18n;
diff --git a/src/build/index.mjs b/src/build/index.mjs
index c7e1cf7..a14cdc7 100644
--- a/src/build/index.mjs
+++ b/src/build/index.mjs
@@ -1,104 +1,150 @@
+import process from 'node:process';
import fs from 'node:fs';
import mustache from 'mustache';
-import {u} from 'unist-builder';
-import {h} from 'hastscript';
-import {select} from 'hast-util-select';
-import {toString as hastToString} from 'mdast-util-to-string';
-import cssesc from 'cssesc';
-
-import {toHtmlRaw, toString, toMdRaw, mdToHtmlRaw} from './to-html.mjs';
import loadSVG from './load-svg.mjs';
-import listArticles from './list-articles.mjs';
import getRSS from './rss.mjs';
-import toml from '@ltd/j-toml';
-
-const loadMD = (listFile, suffix) => {
- const listContent = [];
- for (const file of listFile) {
- console.log(`Working on ${file}`);
- const content = fs.readFileSync(`${suffix}/${file}`, 'utf8');
- const mdRaw = toMdRaw(content);
- const tomlStringValue = mdRaw.children[0].value;
- const metaData = toml.parse(tomlStringValue);
- const newHTML = mdToHtmlRaw(mdRaw);
-
- const htmlContent = newHTML;
- const htmlRender = toString(htmlContent);
-
- const titleHtml = select('h1', htmlContent);
- const intro = select('p', htmlContent);
- intro.children = intro.children.filter(child => child.tagName !== 'br');
-
- const logo = select('img', intro);
- logo.properties.src = `${suffix}/${logo.properties.src}`;
- logo.properties.height = '150';
- logo.properties.width = '150';
- titleHtml.children[0].properties.href = `${suffix}/${file.slice(0, -3)}`;
-
- const title = hastToString(titleHtml);
- const domTitle = cssesc(title.replace(/\s+/g, '-').replace(/['"#@]/, '').toLowerCase()); // Maybe encodeURI
-
- const readMore = h('a', 'Lire plus...');
-
- readMore.properties.href = `${suffix}/${file.slice(0, -3)}`;
-
- listContent.push({
- name: file.slice(0, -3),
- content: htmlRender,
- intro: toString(u('root', [titleHtml, intro, readMore])),
- introDesc: hastToString(intro),
- imageUrl: logo.properties.src,
- metaData,
- title,
- domTitle,
- });
- }
+import loadMD from './loadMD.mjs';
+import {cmpArticles} from './article.mjs';
+import i18n from './i18n.mjs';
+import {addDescription} from './i18n.mjs';
+import {minifyHTML} from './utils.mjs';
+
+import { SitemapStream, streamToPromise } from 'sitemap'
+import { Readable } from 'stream'
- return listContent;
-};
const leftPanelTmpl = fs.readFileSync('src/templates/left.tmpl', 'utf8');
+const likesTmpl = fs.readFileSync('src/templates/likes.tmpl', 'utf8');
const headerTmpl = fs.readFileSync('src/templates/header.tmpl', 'utf8');
const articleTmpl = fs.readFileSync('src/templates/article.tmpl', 'utf8');
const indexTmpl = fs.readFileSync('src/templates/index.tmpl', 'utf8');
+const tagTmpl = fs.readFileSync('src/templates/tag.tmpl', 'utf8');
+const hidTmpl = fs.readFileSync('src/templates/hid.tmpl', 'utf8');
const baseUrl = 'https://ache.one/';
const partials = {
header: headerTmpl,
leftPanel: leftPanelTmpl,
+ likesButton: likesTmpl,
+ hid: hidTmpl,
};
+// Load global variables
const svg = loadSVG();
-const articles = loadMD(listArticles, 'articles');
+let links = [];
+
+for (const lang in i18n) {
+ const tagsArticle = new Map();
+ const filter = process.argv.slice(2);
+ const listArticle = (filter.length > 0) ? i18n[lang].articles.filter((article) => filter.includes(article.name)) : i18n[lang].articles;
+ const articles = loadMD(listArticle, 'articles', lang);
+
+ for (const article of articles) {
+ const context = {
+ svg,
+ page_title: `${article.title} - ache`,
+ title: i18n[lang].title,
+ intro: i18n[lang].intro,
+ lang,
+ canonical: `${baseUrl}${article.url.slice(1)}`,
+ content: article.content,
+ title: i18n[lang].title,
+ metaData: article.metaData,
+ alt_lang: addDescription(article.metaData.alt_lang),
+ description: article.intro,
+
+ like_title: i18n[lang].like_title,
+ like_text: i18n[lang].like_text,
+ };
+ const output = mustache.render(articleTmpl, context, partials);
+
+ console.log(`Create : ${article.title}`);
+ fs.writeFileSync(`articles/${article.name}.html`, minifyHTML(output));
+ links.push({url: context.canonical, changefreq: 'yearly', priority: 0.6, img: [{url: article.imageUrl}]})
+
+ for (const tag of article.metaData.tags) {
+ if (tagsArticle.has(tag)) {
+ tagsArticle.get(tag).push(article);
+ } else {
+ tagsArticle.set(tag, [article]);
+ }
+ }
+ }
-for (const article of articles) {
- const context = {
- svg,
- title: `${article.title} - ache`,
- canonical: baseUrl + article.domTitle,
- content: article.content,
- domTitle: article.domTitle,
- };
- const output = mustache.render(articleTmpl, context, partials);
+// Set of pages language dependant
+ try {
+ fs.mkdirSync(`${lang}/tag`, {recursive: true});
+ } catch {
+ fs.rmSync(`${lang}/tag`, {force: true, recursive: true});
+ fs.mkdirSync(`${lang}/tag`, {recursive: true});
+ }
- console.log(`Create : ${article.title}`);
- fs.writeFileSync(`articles/${article.name}.html`, output);
-}
+ for (const [tag, articles] of tagsArticle.entries()) {
+ console.log(`Create tag page : ${lang}/${tag}.html`);
+ articles.sort(cmpArticles);
+
+ const context = {
+ svg,
+ page_title: `ache - Tag: ${tag}`,
+ title: i18n[lang].title,
+ intro: i18n[lang].intro,
+ lang,
+ tag,
+ articles,
+ description: `${i18n[lang]['tag_desc']} ${tag}.`,
+ };
+
+ const output = mustache.render(tagTmpl, context, partials);
+ fs.writeFileSync(`${lang}/tag/${tag}.html`, minifyHTML(output));
+ links.push({url: `${baseUrl}${lang}/tag/${tag}`, changefreq: 'monthly', priority: 0.3})
+ }
-console.log('Create : rss.xml');
-const xmlFeed = getRSS(articles, baseUrl);
-fs.writeFileSync('rss.xml', xmlFeed);
-
-{
- const context = {
- title: 'ache: Blog personnel',
- canonical: baseUrl,
- svg,
- articles,
- };
-
- console.log('Create : Home page');
- const output = mustache.render(indexTmpl, context, partials);
- fs.writeFileSync('index.html', output);
+ console.log(`Create RSS Flux: ${lang}/rss.xml`);
+ const xmlFeed = getRSS(articles, baseUrl, lang);
+ fs.writeFileSync(`${lang}/rss.xml`, xmlFeed);
+ links.push({url: `${baseUrl}${lang}/rss.xml`, changefreq: 'monthly', priority: 0.3})
+
+ {
+ const alt_lang = {
+ fr: {
+ lang: 'en',
+ url: '/en/',
+ },
+ en: {
+ lang: 'fr',
+ url: '/fr/',
+ },
+ };
+ const context = {
+ page_title: i18n[lang].title.main,
+ title: i18n[lang].title,
+ intro: i18n[lang].intro,
+ lang,
+ canonical: `${baseUrl}${lang}`,
+ svg,
+ articles,
+ alt_lang: [alt_lang[lang]],
+ description: i18n[lang].index_desc,
+ };
+
+ console.log(`Create : Home page ${lang}`);
+ const output = mustache.render(indexTmpl, context, partials);
+ fs.writeFileSync(`${lang}/index.html`, minifyHTML(output));
+ links.push({url: `${baseUrl}${lang}`, changefreq: 'monthly', priority: 0.7, img: [{url: "https://ache.one/res/ache.svg"}]})
+ }
}
+
+console.log(`Create: sitemap.xml`);
+
+// Create a stream to write to
+const stream = new SitemapStream({hostname: 'https://ache.one'});
+
+// Return a promise that resolves with your XML string
+streamToPromise(
+ Readable.from(links)
+ .pipe(stream))
+ .then(data => data.toString())
+ .then(sitemapXML => fs.writeFileSync("sitemap.xml", sitemapXML)
+);
+
diff --git a/src/build/list-articles.mjs b/src/build/list-articles.mjs
deleted file mode 100644
index d327573..0000000
--- a/src/build/list-articles.mjs
+++ /dev/null
@@ -1,7 +0,0 @@
-const listArticles = [
- 'bizarreries-du-langage-c.md',
- 'retour-sur-laoc-2021-semaine-1.md',
- '2FA-discord-sur-pc.md',
- 'duckduckgo-google-en-mieux.md',
-];
-export default listArticles;
diff --git a/src/build/loadMD.mjs b/src/build/loadMD.mjs
new file mode 100644
index 0000000..6ba4f76
--- /dev/null
+++ b/src/build/loadMD.mjs
@@ -0,0 +1,79 @@
+import fs from 'node:fs';
+import {u} from 'unist-builder';
+import {h} from 'hastscript';
+import {select} from 'hast-util-select';
+import {toString as hastToString} from 'mdast-util-to-string';
+import cssesc from 'cssesc';
+import toml from '@ltd/j-toml';
+
+import {toString, toMdRaw, mdToHtmlRaw} from './to-html.mjs';
+import {getArticleYear} from './article.mjs';
+import i18n from './i18n.mjs';
+
+const loadMD = (listFile, suffix, lang) => {
+ const listContent = [];
+ for (const file of listFile) {
+ console.log(`Working on ${file}`);
+ const content = fs.readFileSync(`${suffix}/${file}`, 'utf8');
+ const mdRaw = toMdRaw(content);
+ const tomlStringValue = mdRaw.children[0].value;
+ const metaData = toml.parse(tomlStringValue);
+ const newHTML = mdToHtmlRaw(mdRaw);
+
+ const htmlContent = newHTML;
+ const htmlRender = toString(htmlContent);
+
+ const titleHtml = select('h1', htmlContent);
+ const intro = select('p', htmlContent);
+ intro.children = intro.children.filter(child => child.tagName !== 'br');
+
+ const logo = select('img', intro);
+ if (logo && logo?.properties) {
+ if (logo.properties.src[0] != '/') {
+ logo.properties.src = `/${suffix}/${logo.properties.src}`;
+ }
+ logo.properties.height = '150';
+ logo.properties.width = '150';
+ }
+
+ const logoP = select('source', intro);
+ if (logoP !== null) {
+ logoP.properties.srcSet = `/${suffix}/${logoP.properties.srcSet}`;
+ }
+
+ titleHtml.children[0].properties.href = `/${suffix}/${file.slice(0, -3)}`;
+
+ const title = hastToString(titleHtml);
+ const domTitle = cssesc(title.replace(/\s+/g, '-').replace(/['"#@]/, '').toLowerCase()); // Maybe encodeURI
+
+ const readMore = h('a', i18n[lang]['read_more']);
+
+ readMore.properties.href = `/${suffix}/${file.slice(0, -3)}`;
+ const pubYear = getArticleYear({metaData});
+
+ if (metaData.pubDate) {
+ try {
+ metaData.pubDateISO = metaData.pubDate.toISOString();
+ } catch (error) {
+ console.error(`Error on file ${file} with pubDate (${metaData.pubDate}): ${error}`);
+ }
+ }
+
+ listContent.push({
+ name: file.slice(0, -3),
+ content: htmlRender,
+ intro: toString(u('root', [titleHtml, intro, readMore])),
+ introDesc: hastToString(intro),
+ imageUrl: logo?.properties?.src || '',
+ metaData,
+ pubYear,
+ title,
+ domTitle,
+ url: `/${suffix}/${file.slice(0, -3)}`,
+ });
+ }
+
+ return listContent;
+};
+
+export default loadMD;
diff --git a/src/build/rss.mjs b/src/build/rss.mjs
index e890152..6394af5 100644
--- a/src/build/rss.mjs
+++ b/src/build/rss.mjs
@@ -1,37 +1,43 @@
import RSS from 'rss';
+import i18n from './i18n.mjs';
-const getRSS = (articles, baseUrl) => {
+const getRSS = (articles, baseUrl, lang) => {
+ console.log(i18n[lang]['rss']['title']);
const rssFeed = new RSS({
- title: 'ache: Blog personnel',
- description: 'Programmation, Algorithmique, Système, *pick you poison*',
+ title: i18n[lang]['rss']['title'],
+ description: i18n[lang]['rss']['description'],
// eslint-disable-next-line camelcase
+ custom_namespaces: {
+ 'visible_description': i18n[lang]['rss']['description']
+ },
+ site_url: "https://ache.one",
feed_url: `${baseUrl}rss.xml`,
canonical: `${baseUrl}rss.xml`,
// eslint-disable-next-line camelcase
site_url: baseUrl,
// eslint-disable-next-line camelcase
image_url: `${baseUrl}ache.svg`,
- language: 'fr',
- pubDate: (new Date().toLocaleString()),
+ language: lang,
+ pubDate: Date(),
ttl: '1440',
- // eslint-disable-next-line camelcase
- custom_elements: ['<?xml-stylesheet type="text/css" href="http://localhost:8080/s/css/style.css" ?>'],
});
for (const article of articles.slice(0, 10)) {
+ let image_url = (article.imageUrl[0] != '/') ? `/${article.imageUrl}` : article.imageUrl;
+
rssFeed.item({
title: article.title,
description: '<article>' + article.content + '</article>',
// eslint-disable-next-line camelcase
- image_url: article.imageUrl,
+ image_url,
url: `${baseUrl}articles/${article.name}`,
guid: article.domTitle,
author: 'ache',
- date: article.metaData.pubDate.toISOString(),
+ date: article.metaData.pubDateISO,
categories: article.metaData.tags,
// eslint-disable-next-line camelcase
custom_elements: [
- {logo: article.imageUrl},
+ {logo: image_url},
{intro: article.introDesc},
],
});
diff --git a/src/build/special_box.mjs b/src/build/special_box.mjs
index 0b284ec..1457a98 100644
--- a/src/build/special_box.mjs
+++ b/src/build/special_box.mjs
@@ -10,6 +10,7 @@ export default function specialBox() {
if (node.type === 'containerDirective' && (
node.name === 'attention'
|| node.name === 'question'
+ || node.name === 'note'
|| node.name === 'information')) {
const data = node.data || (node.data = {});
@@ -18,6 +19,20 @@ export default function specialBox() {
className: 'special-box ' + node.name,
};
}
+ if (node.type === 'containerDirective' && node.name === 'details') {
+ if(node.children.length > 0 && node.children[0].type == "paragraph") {
+ node.children[0] = {
+ type: "containerDirective",
+ data: {
+ hName: 'summary'
+ },
+ children: node.children[0].children
+ };
+
+ const data = node.data || (node.data = {});
+ data.hName = 'details';
+ }
+ }
});
};
}
diff --git a/src/build/to-html.mjs b/src/build/to-html.mjs
index 4bc3c06..786f91d 100644
--- a/src/build/to-html.mjs
+++ b/src/build/to-html.mjs
@@ -7,6 +7,7 @@ import remarkMath from 'remark-math';
import remarkFrontmatter from 'remark-frontmatter';
import remarkRehype from 'remark-rehype';
import rehypeSlug from 'rehype-slug';
+import rehypePicture from 'rehype-picture'
import rehypeKaTeX from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
@@ -14,6 +15,7 @@ import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';
import remarkSpecialBox from './special_box.mjs';
import remarkRemoveFootnoteHeader from './remove-footnote-header.mjs';
+import {getTocHeading} from './i18n.mjs';
const autoLinkOption = {
behavior: 'wrap',
@@ -23,10 +25,14 @@ const autoLinkOption = {
},
};
+const pictureOptions = {
+ 'png': {avif: 'image/avif'}
+}
+
const generator = unified()
.use(remarkParse)
.use(remarkGfm)
- .use(remarkToc, {heading: 'Sommaire', tight: true, ordered: true})
+ .use(remarkToc, {heading: getTocHeading(), tight: true, ordered: true})
.use(remarkMath)
.use(remarkDirective)
.use(remarkSpecialBox)
@@ -34,6 +40,7 @@ const generator = unified()
.use(remarkRehype, {allowDangerousHtml: true})
.use(rehypeRaw)
.use(remarkRemoveFootnoteHeader)
+ .use(rehypePicture, pictureOptions)
.use(rehypeKaTeX)
.use(rehypeSlug)
.use(rehypeHighlight)
@@ -43,7 +50,7 @@ const generator = unified()
const generatorMd = unified()
.use(remarkParse)
.use(remarkGfm)
- .use(remarkToc, {heading: 'Sommaire', tight: true, ordered: true})
+ .use(remarkToc, {heading: getTocHeading(), tight: true, ordered: true})
.use(remarkMath)
.use(remarkDirective)
.use(remarkSpecialBox)
@@ -53,6 +60,7 @@ const generatorHTML = unified()
.use(remarkRehype, {allowDangerousHtml: true})
.use(rehypeRaw)
.use(remarkRemoveFootnoteHeader)
+ .use(rehypePicture, pictureOptions)
.use(rehypeKaTeX)
.use(rehypeSlug)
.use(rehypeHighlight)
diff --git a/src/build/utils.mjs b/src/build/utils.mjs
new file mode 100644
index 0000000..fa85ee6
--- /dev/null
+++ b/src/build/utils.mjs
@@ -0,0 +1,10 @@
+import { minify } from 'html-minifier';
+
+export const minifyHTML = (html) => minify(html, {
+ removeAttributeQuotes: true,
+ removeComments: true,
+ minifyJS: true,
+ minifyCSS: true,
+ collapseWhitespace: true,
+ collapseBooleanAttributes: true,
+})