diff options
Diffstat (limited to 'src/build')
-rw-r--r-- | src/build/article.mjs | 31 | ||||
-rw-r--r-- | src/build/i18n.mjs | 85 | ||||
-rw-r--r-- | src/build/index.mjs | 208 | ||||
-rw-r--r-- | src/build/list-articles.mjs | 7 | ||||
-rw-r--r-- | src/build/loadMD.mjs | 79 | ||||
-rw-r--r-- | src/build/rss.mjs | 26 | ||||
-rw-r--r-- | src/build/special_box.mjs | 15 | ||||
-rw-r--r-- | src/build/to-html.mjs | 12 | ||||
-rw-r--r-- | src/build/utils.mjs | 10 |
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, +}) |