Compare commits

..

No commits in common. "80912cb6e2e33399010b2f5fd1034fc24f221c44" and "9fd97d17d8e319d48ac6069ebd732d7cefe51b4e" have entirely different histories.

14 changed files with 232 additions and 238 deletions

View File

@ -1,27 +0,0 @@
import { globSync } from "glob";
import { readFileSync } from "node:fs";
import { env } from "node:process";
import { join } from "node:path";
export const assetMap: Map<string, string> = new Map();
export function reloadAssets() {
assetMap.clear();
globSync("dist/_assets/*.manifest")
.map(
(manifest) =>
JSON.parse(readFileSync(manifest).toString()) as { [orig: string]: string },
)
.forEach((mapping) =>
Object.entries(mapping).forEach(([orig, hashed]) => assetMap.set(orig, hashed)),
);
}
export function asset(path: string): string {
const realPath = assetMap.has(path) ? assetMap.get(path) : path;
return env.ASSETS_HOSTS
? new URL(realPath, env.ASSETS_HOSTS).toString()
: join("/_assets/", realPath);
}

View File

@ -1,8 +1,19 @@
import { Transform } from "node:stream"; import { Transform } from "node:stream";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import File from "vinyl"; import File from "vinyl";
import { globSync } from "glob";
import { readFileSync } from "node:fs";
import { PRODUCTION } from "./environment.js"; import { PRODUCTION } from "./environment.js";
export function readAssetManifest(): { [entry: string]: string } {
return Object.assign(
{},
...globSync("dist/_assets/*.manifest").map((path) =>
JSON.parse(readFileSync(path).toString()),
),
);
}
function fileHash(buffer: Buffer) { function fileHash(buffer: Buffer) {
const hash = createHash("sha256"); const hash = createHash("sha256");
hash.update(buffer.toString()); hash.update(buffer.toString());

View File

@ -1,16 +1,16 @@
import Postcss from "postcss"; import Postcss from "postcss";
import { reloadAssets, assetMap } from "../assets.js"; import { readAssetManifest } from "../hash.js";
export default (): Postcss.Plugin => { export default (opts: any = {}): Postcss.Plugin => {
return { return {
postcssPlugin: "postcss-hashes", postcssPlugin: "postcss-hashes",
Once() { Once() {
reloadAssets(); opts._mappings = readAssetManifest();
}, },
Declaration(decl: Postcss.Declaration) { Declaration(decl: Postcss.Declaration) {
decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v, url) => { decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v, url) => {
if (assetMap.has(url)) { if (opts._mappings.hasOwnProperty(url)) {
return v.replace(url, assetMap.get(url)); return v.replace(url, opts._mappings[url]);
} }
return v; return v;

View File

@ -1,4 +1,6 @@
import { compileFile } from "pug";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { env } from "node:process";
import { join, basename, dirname } from "node:path"; import { join, basename, dirname } from "node:path";
import { Transform } from "node:stream"; import { Transform } from "node:stream";
import asciidoctor, { Document } from "asciidoctor"; import asciidoctor, { Document } from "asciidoctor";
@ -13,13 +15,7 @@ import { PRODUCTION, DEFAULT_DATE } from "../environment.js";
import rehypeExternalLinks from "rehype-external-links"; import rehypeExternalLinks from "rehype-external-links";
import visit from "unist-util-visit"; import visit from "unist-util-visit";
import { src, dest } from "gulp"; import { src, dest } from "gulp";
import renderLayout from "../../src/layout.js"; import { readAssetManifest } from "../hash.js";
import renderArticleLayout from "../../src/article.js";
import renderIndex from "../../src/index.js";
import { renderToStaticMarkup } from "preact-render-to-string";
import { SITE_TITLE, SITE_DESCRIPTION } from "../../src/constants.js";
import { JSX } from "preact/jsx-runtime";
import { reloadAssets } from "../assets.js";
const Asciidoctor = asciidoctor(); const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create(); const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
@ -33,6 +29,18 @@ EXTENSION_REGISTRY.inlineMacro("abbr", function () {
}); });
}); });
const SITE_TITLE = "Ad\xE6dra's blog";
const SITE_DESCRIPTION = [
"Ad\xE6dra",
"Software Developper in Paris, France",
"Rust, Ruby, Typescript, Linux",
].join(" \u2022 ");
const SITE_DEFAULT_META = {
"og:image": "https://adaedra.blob.core.windows.net/blog-assets/og_preview.png",
"og:image:alt": "Ad\xE6dra's mascot",
"og:locale": "en_GB",
};
function extractImages(gulp: Transform, file: File) { function extractImages(gulp: Transform, file: File) {
return function () { return function () {
return function (tree) { return function (tree) {
@ -53,13 +61,21 @@ function extractImages(gulp: Transform, file: File) {
}; };
} }
function renderDocument(root: JSX.Element): Buffer {
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
}
function renderArticle() { function renderArticle() {
const allArticles: [string, DateTime, Document][] = []; const allArticles: [string, DateTime, Document][] = [];
reloadAssets(); const renderLayout = compileFile("src/layout.pug");
const renderArticleLayout = compileFile("src/article.pug");
const assetManifest = readAssetManifest();
const asset = (path: string) => {
if (assetManifest.hasOwnProperty(path)) {
path = assetManifest[path];
}
return env.ASSETS_HOSTS
? new URL(path, env.ASSETS_HOSTS).toString()
: join("/_assets/", path);
};
return new Transform({ return new Transform({
readableObjectMode: true, readableObjectMode: true,
@ -87,26 +103,31 @@ function renderArticle() {
.use(rehypeStringify) .use(rehypeStringify)
.process(article.convert()); .process(article.convert());
const content = renderLayout({ const content = renderLayout({
title: article.getDoctitle({}) as string, SITE_TITLE,
meta: { asset,
title: article.getDoctitle({}),
meta: Object.assign({}, SITE_DEFAULT_META, {
description: article.getAttribute("description"), description: article.getAttribute("description"),
keywords: article.getAttribute("keywords"), keywords: article.getAttribute("keywords"),
"og:title": article.getDoctitle({}) as string, "og:title": article.getDoctitle({}),
"og:type": "article", "og:type": "article",
"og:article:published_time": article.getAttribute("docdate"), "og:article:published_time": article.getAttribute("docdate"),
"og:url": `https://adaedra.eu/${slug}/`, "og:url": `https://adaedra.eu/${slug}/`,
"og:description": article.getAttribute("description"), "og:description": article.getAttribute("description"),
"og:site_name": SITE_TITLE, "og:site_name": SITE_TITLE,
}, }),
Content: () => render() {
renderArticleLayout({ return renderArticleLayout({
asset,
article, article,
date, date,
DateTime,
body: vfile.toString(), body: vfile.toString(),
}), });
},
}); });
file.contents = renderDocument(content); file.contents = Buffer.from(content);
file.path = join(slug, "index.html"); file.path = join(slug, "index.html");
file.base = null; file.base = null;
@ -119,22 +140,22 @@ function renderArticle() {
final(callback) { final(callback) {
try { try {
allArticles.sort(([, a], [, b]) => b.diff(a).toMillis()); allArticles.sort(([, a], [, b]) => b.diff(a).toMillis());
const renderIndex = compileFile("src/index.pug");
const contents = renderLayout({ const contents = renderLayout({
meta: { SITE_TITLE,
asset,
meta: Object.assign({}, SITE_DEFAULT_META, {
description: SITE_DESCRIPTION, description: SITE_DESCRIPTION,
"og:title": SITE_TITLE, "og:title": SITE_TITLE,
"og:type": "website", "og:type": "website",
"og:url": `https://adaedra.eu`, "og:url": `https://adaedra.eu`,
}),
render() {
return renderIndex({ articles: allArticles, asset });
}, },
Content: () => renderIndex({ articles: allArticles }),
}); });
this.push( this.push(new File({ path: "index.html", contents: Buffer.from(contents) }));
new File({
path: "index.html",
contents: renderDocument(contents),
}),
);
callback(null); callback(null);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

116
package-lock.json generated
View File

@ -19,8 +19,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5", "postcss-nesting": "^12.1.5",
"preact": "^10.22.0", "pug": "^3.0.3",
"preact-render-to-string": "^6.5.5",
"rehype": "^13.0.1", "rehype": "^13.0.1",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0", "rehype-preset-minify": "^7.0.0",
@ -29,10 +28,12 @@
"unified": "^11.0.4" "unified": "^11.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21",
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/gulp": "^4.0.17", "@types/gulp": "^4.0.17",
"@types/gulp-postcss": "^8.0.6", "@types/gulp-postcss": "^8.0.6",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/pug": "^2.0.10",
"@types/tmp": "^0.2.6", "@types/tmp": "^0.2.6",
"prettier": "^3.3.1" "prettier": "^3.3.1"
} }
@ -373,12 +374,55 @@
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
}, },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/expect": { "node_modules/@types/expect": {
"version": "1.20.4", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz",
"integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==",
"dev": true "dev": true
}, },
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.3",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz",
"integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/glob": { "node_modules/@types/glob": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
@ -430,6 +474,12 @@
"@types/unist": "^2" "@types/unist": "^2"
} }
}, },
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
@ -444,6 +494,12 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"node_modules/@types/minimatch": { "node_modules/@types/minimatch": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@ -464,6 +520,45 @@
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
"dev": true "dev": true
}, },
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true
},
"node_modules/@types/qs": {
"version": "6.9.15",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
"integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/streamx": { "node_modules/@types/streamx": {
"version": "2.9.5", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@types/streamx/-/streamx-2.9.5.tgz", "resolved": "https://registry.npmjs.org/@types/streamx/-/streamx-2.9.5.tgz",
@ -4246,23 +4341,6 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
}, },
"node_modules/preact": {
"version": "10.22.0",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.0.tgz",
"integrity": "sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "6.5.5",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.5.tgz",
"integrity": "sha512-KiMFTKNTmT/ccE79BURR/r6XRc2I2TCTZ0MpeWqHW2XnllbeghXvwGsdAfF/MzMilUcTfODtSmMxgoRFL9TM5g==",
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",

View File

@ -15,8 +15,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5", "postcss-nesting": "^12.1.5",
"preact": "^10.22.0", "pug": "^3.0.3",
"preact-render-to-string": "^6.5.5",
"rehype": "^13.0.1", "rehype": "^13.0.1",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0", "rehype-preset-minify": "^7.0.0",
@ -25,10 +24,12 @@
"unified": "^11.0.4" "unified": "^11.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21",
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/gulp": "^4.0.17", "@types/gulp": "^4.0.17",
"@types/gulp-postcss": "^8.0.6", "@types/gulp-postcss": "^8.0.6",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/pug": "^2.0.10",
"@types/tmp": "^0.2.6", "@types/tmp": "^0.2.6",
"prettier": "^3.3.1" "prettier": "^3.3.1"
}, },

13
src/article.pug Normal file
View File

@ -0,0 +1,13 @@
nav
a(href="/" title="Back to home page")
img(src=asset("avatar.avif") alt="Adædra's avatar")
| Adædra
article
header.article-header
= date.toLocaleString(DateTime.DATE_FULL)
h1!= article.getDocumentTitle()
main
a(name="content")
!= body

View File

@ -1,28 +0,0 @@
import { Document } from "asciidoctor";
import { DateTime } from "luxon";
import { asset } from "../lib/assets.js";
type Props = {
article: Document;
date: DateTime;
body: string;
};
export default ({ article, date, body }: Props) => (
<>
<nav>
<a href="/" title="Back to home page">
<img src={asset("avatar.avif")} alt="Adædra's avatar" />
Adædra
</a>
</nav>
<article>
<header class="article-header">
{date.toLocaleString(DateTime.DATE_FULL)}
<h1 dangerouslySetInnerHTML={{ __html: article.getDocumentTitle() as string }} />
</header>
<a name="content" />
<main dangerouslySetInnerHTML={{ __html: body }} />
</article>
</>
);

View File

@ -1,13 +0,0 @@
export const SITE_TITLE = "Ad\xE6dra's blog";
export const SITE_DESCRIPTION = [
"Ad\xE6dra",
"Software Developper in Paris, France",
"Rust, Ruby, Typescript, Linux",
].join(" \u2022 ");
export const SITE_DEFAULT_META = {
viewport: "width=device-width, initial-scale=1",
"theme-color": "#DDCBA3",
"og:image": "https://adaedra.blob.core.windows.net/blog-assets/og_preview.png",
"og:image:alt": "Ad\xE6dra's mascot",
"og:locale": "en_GB",
};

39
src/index.pug Normal file
View File

@ -0,0 +1,39 @@
header.index-header
div(style="flex: 1;")
h1 Adædra
| Software developper<br />
| Rust, Ruby, TypeScript, Linux<br />
| Paris, France
img(src=asset("cariboudev.avif") alt="Adædra's mascot")/
h2 Latest articles
ul.index-list
- for ([slug, date, article] of articles)
li
= date.toISODate()
| &nbsp;
a(href=`/${slug}/`)!= article.getDocumentTitle()
h2 Find me elsewhere
ul.index-list
li: a(href="https://gitea.adaedra.eu/adaedra" target="_blank")
svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-label="Git Logo"): use(href="/logos.svg#git")/
| Gitea
li: a(href="https://nerdculture.de/@adaedra" target="_blank" rel="noopener")
svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-label="Mastodon Logo"): use(href="/logos.svg#mastodon")/
| Mastodon
li: a(href="https://www.linkedin.com/in/thibault-hamel/" target="_blank" rel="noopener")
svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-label="LinkedIn Logo"): use(href="/logos.svg#linkedin")/
| LinkedIn
footer.index-footer
| Mascot drawn by&nbsp;
a(href="https://linktr.ee/cutepencilcase" target="_blank" rel="noopener") CutePencilCase
| <br />Font&nbsp;
a(href="https://github.com/be5invis/Iosevka" target="_blank" rel="noopener") Iosevka
| &bull; Color theme&nbsp;
a(href="https://github.com/morhetz/gruvbox-contrib" target="_blank" rel="noopener") Gruvbox
| &bull; Social icons&nbsp;
a(href="https://icons8.com/" target="_blank" rel="noopener") Icons8
br/
a(href="https://gitea.adaedra.eu/adaedra/blog" target="_blank") Source code

View File

@ -1,79 +0,0 @@
import { Document } from "asciidoctor";
import { DateTime } from "luxon";
import { asset } from "../lib/assets.js";
const Header = () => (
<header class="index-header">
<div style={{ flex: 1 }}>
<h1>Adædra</h1>
Software developer
<br />
Rust, Ruby, TypeScript, Linux
<br />
Paris, France
</div>
<img src={asset("cariboudev.avif")} alt="Adaedra's mascot" />
</header>
);
type Props = {
articles: [string, DateTime, Document][];
};
const Articles = ({ articles }: Props) => (
<>
<h2>Latest articles</h2>
<ul class="index-list">
{articles.map(([slug, date, article]) => (
<li>
{date.toISODate()}&nbsp;
<a
href={`/${slug}/`}
dangerouslySetInnerHTML={{
__html: article.getDocumentTitle() as string,
}}
/>
</li>
))}
</ul>
</>
);
const SOCIALS: [string, string, string][] = [
["Gitea", "https://gitea.adaedra.eu", "git"],
["Mastodon", "https://nerdculture.de/@adaedra", "mastodon"],
["LinkedIn", "https://www.linkedin.com/in/thibault-hamel/", "linkedin"],
];
const Socials = () => (
<>
<h2>Find me elsewhere</h2>
<ul class="index-list">
{SOCIALS.map(([name, url, svgId]) => (
<li>
<a href={url} target="_blank" rel="noopener">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
aria-label={`${name} Logo`}
>
<use href={`/logos.svg#${svgId}`} />
</svg>
{name}
</a>
</li>
))}
</ul>
</>
);
const Footer = () => <footer class="index-footer"></footer>;
export default (props: Props) => (
<>
<Header />
<Articles {...props} />
<Socials />
<Footer />
</>
);

15
src/layout.pug Normal file
View File

@ -0,0 +1,15 @@
doctype html
html(lang="en")
head
meta(charset="utf-8")/
title
if title
!= `${title} &ndash; `
= SITE_TITLE
meta(name="viewport" content="width=device-width, initial-scale=1")/
meta(name="theme-color" content="#DDCBA3")/
- for([name, content] of Object.entries(meta))
meta(name=name content=content)/
link(rel="stylesheet" type="text/css" href=asset("index.css"))/
body: .main
!= render()

View File

@ -1,35 +0,0 @@
import { SITE_DEFAULT_META, SITE_TITLE } from "./constants.js";
import { JSX } from "preact/jsx-runtime";
import { asset } from "../lib/assets.js";
type Props = {
title?: string;
meta?: { [name: string]: string };
Content: () => JSX.Element;
};
export default ({ title, meta, Content }: Props) => {
const metaTags = Object.entries(Object.assign({}, SITE_DEFAULT_META, meta || {})).map(
([name, value]) => <meta name={name} value={value} />,
);
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<title
dangerouslySetInnerHTML={{
__html: [title, SITE_TITLE].filter(Boolean).join(" &ndash; "),
}}
/>
{metaTags}
<link rel="stylesheet" type="text/css" href={asset("index.css")} />
</head>
<body>
<div class="main">
<Content />
</div>
</body>
</html>
);
};

View File

@ -7,7 +7,5 @@
}, },
"compilerOptions": { "compilerOptions": {
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
} }
} }