From 3a2d198d153efc8a69e7640f7dcde88207268ff3 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Fri, 22 Sep 2023 08:40:59 +0200 Subject: Replace zola with DOM-based static site generation code --- Caddyfile | 77 --------- Dockerfile | 31 ++-- Makefile | 11 +- bin/build.ts | 30 ++++ bun.lockb | Bin 7240 -> 14725 bytes config.toml | 20 +-- content/feed-styles/index.md | 4 - content/post/_index.md | 6 - flake.nix | 1 - package.json | 13 +- redis.Caddyfile | 2 - src/app.ts | 42 ++++- src/config.ts | 4 +- src/index.ts | 4 + src/posts.ts | 63 ++++++++ src/templates.ts | 238 ++++++++++++++++++++++++++++ templates/404.html | 37 +++++ templates/feed-styles.html | 84 ---------- templates/feed-styles.xsl | 82 ++++++++++ templates/feed.xml | 24 +++ templates/footer.html | 1 - templates/index.html | 99 +++++++----- templates/post.html | 50 ++++++ templates/style.css | 196 +++++++++++++++++++++++ templates/tag.html | 49 ++++++ templates/tags.html | 40 +++++ themes/bear/.envrc | 1 - themes/bear/.gitignore | 1 - themes/bear/LICENSE | 21 --- themes/bear/README.md | 83 ---------- themes/bear/config.toml | 35 ---- themes/bear/content/_index.md | 23 --- themes/bear/content/bear.md | 15 -- themes/bear/content/blog/_index.md | 3 - themes/bear/content/blog/markdown-syntax.md | 130 --------------- themes/bear/content/zola.md | 36 ----- themes/bear/flake.lock | 61 ------- themes/bear/flake.nix | 20 --- themes/bear/netlify.toml | 25 --- themes/bear/screenshot.png | Bin 262700 -> 0 bytes themes/bear/templates/404.html | 8 - themes/bear/templates/base.html | 30 ---- themes/bear/templates/footer.html | 5 - themes/bear/templates/header.html | 11 -- themes/bear/templates/index.html | 7 - themes/bear/templates/nav.html | 6 - themes/bear/templates/page.html | 29 ---- themes/bear/templates/section.html | 34 ---- themes/bear/templates/security_tags.html | 2 - themes/bear/templates/seo_tags.html | 2 - themes/bear/templates/style.css | 193 ---------------------- themes/bear/templates/style.css.html | 1 - themes/bear/templates/taxonomy_list.html | 14 -- themes/bear/templates/taxonomy_single.html | 34 ---- themes/bear/theme.toml | 25 --- tsconfig.json | 3 + 56 files changed, 942 insertions(+), 1124 deletions(-) delete mode 100644 Caddyfile create mode 100755 bin/build.ts delete mode 100644 content/feed-styles/index.md delete mode 100644 content/post/_index.md delete mode 100644 redis.Caddyfile create mode 100644 src/posts.ts create mode 100644 src/templates.ts create mode 100644 templates/404.html delete mode 100644 templates/feed-styles.html create mode 100644 templates/feed-styles.xsl create mode 100644 templates/feed.xml delete mode 100644 templates/footer.html create mode 100644 templates/post.html create mode 100644 templates/style.css create mode 100644 templates/tag.html create mode 100644 templates/tags.html delete mode 100644 themes/bear/.envrc delete mode 100644 themes/bear/.gitignore delete mode 100644 themes/bear/LICENSE delete mode 100644 themes/bear/README.md delete mode 100644 themes/bear/config.toml delete mode 100644 themes/bear/content/_index.md delete mode 100644 themes/bear/content/bear.md delete mode 100644 themes/bear/content/blog/_index.md delete mode 100644 themes/bear/content/blog/markdown-syntax.md delete mode 100644 themes/bear/content/zola.md delete mode 100644 themes/bear/flake.lock delete mode 100644 themes/bear/flake.nix delete mode 100644 themes/bear/netlify.toml delete mode 100644 themes/bear/screenshot.png delete mode 100644 themes/bear/templates/404.html delete mode 100644 themes/bear/templates/base.html delete mode 100644 themes/bear/templates/footer.html delete mode 100644 themes/bear/templates/header.html delete mode 100644 themes/bear/templates/index.html delete mode 100644 themes/bear/templates/nav.html delete mode 100644 themes/bear/templates/page.html delete mode 100644 themes/bear/templates/section.html delete mode 100644 themes/bear/templates/security_tags.html delete mode 100644 themes/bear/templates/seo_tags.html delete mode 100644 themes/bear/templates/style.css delete mode 120000 themes/bear/templates/style.css.html delete mode 100644 themes/bear/templates/taxonomy_list.html delete mode 100644 themes/bear/templates/taxonomy_single.html delete mode 100644 themes/bear/theme.toml diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index c779358..0000000 --- a/Caddyfile +++ /dev/null @@ -1,77 +0,0 @@ -{ - admin off - persist_config off - auto_https off - import globals/* - servers :80 { - metrics - protocols h1 h2c - trusted_proxies static private_ranges - } - servers :9091 { - protocols h1 - } -} - -:9091 { - metrics -} - -http://, -http://alanpearce.uk, -http://www.alanpearce.uk, -http://www.alanpearce.eu { - header { - Cache-Control max-age=31536000 - X-Content-Type-Options nosniff - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'" - } - redir https://alanpearce.eu{uri} permanent -} - -http://aln.pe { - header { - Cache-Control max-age=86400 - X-Content-Type-Options nosniff - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'" - } - - redir /pronouns https://en.pronouns.page/@alanpearce - redir /pronomen https://de.pronouns.page/@alanpearce - redir /git https://git.alanpearce.eu - redir /gpg https://alanpearce.eu/public_key.asc - redir /status https://stats.uptimerobot.com/GgzRkHBDr7 - redir /* https://alanpearce.eu/{uri} -} - -http://alanpearce.eu { - root * {$SITE_ROOT} - file_server { - precompressed br zstd gzip - } - header { - Cache-Control max-age=14400 - X-Content-Type-Options nosniff - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'; form-action 'none'; base-uri 'self'" - } - handle_errors { - @404 expression `{err.status_code} == 404` - handle @404 { - rewrite * /404.html - } - file_server { - precompressed br zstd gzip - } - } - header /feed-styles/ Content-Type text/xsl - error /feed-styles/index.html* 404 - respond /favicon.ico 204 - redir /index.xml /atom.xml - redir /post/index.xml /atom.xml - - @http header X-Forwarded-Proto http - redir @http https://alanpearce.eu{uri} -} diff --git a/Dockerfile b/Dockerfile index 24a273a..e7771fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,34 +25,32 @@ COPY --link bun.lockb package.json ./ RUN bun install --ci # Copy application code -COPY --link src src - -FROM ghcr.io/getzola/zola:v${ZOLA_VERSION} as ssg - -WORKDIR /web - COPY --link config.toml config.toml -COPY --link themes themes +COPY --link src src +COPY --link bin bin +COPY --link content content COPY --link templates templates COPY --link static static -COPY --link content content -RUN [ "zola", "build", "--force" ] +RUN ./bin/build.ts && rm -r node_modules -FROM alpine:20230901 as postprocess +ENV NODE_ENV=production +RUN [ "bun", "install", "--production", "--ci" ] -WORKDIR /web +FROM alpine:20230901 as postprocess RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories RUN apk add --no-cache prettier@testing make fd brotli gzip zstd +WORKDIR /app + COPY --link Makefile ./ -COPY --from=ssg /web/config.toml ./ -COPY --from=ssg /web/public public +COPY --link config.toml ./ +COPY --from=build /app/public public -RUN make postprocess format -RUN make -j4 compress +RUN make format +# RUN make -j4 compress # Final stage for app image FROM base @@ -60,9 +58,10 @@ FROM base # Copy built application COPY config.toml /app/ COPY --from=build /app /app -COPY --from=postprocess /web/public /app/public # Start the server by default, this can be overwritten at runtime EXPOSE 3000 EXPOSE 9091 + +ENV NODE_ENV=production CMD [ "bun", "run", "src/index.ts" ] diff --git a/Makefile b/Makefile index 288985d..ef95ece 100644 --- a/Makefile +++ b/Makefile @@ -7,18 +7,9 @@ gzip := @gzip --best --keep --force zstd := @zstd --force --no-pass-through -19 --quiet --stdout pr := @printf "%4s %s\n" -public/feed-styles.xsl: - @mv public/feed-styles/index.html $@ - @rm -fr public/feed-styles - -postprocess: .postprocessstamp - -.postprocessstamp: public/feed-styles.xsl - @touch .postprocessstamp - format: .formatstamp -.formatstamp: public/index.html .postprocessstamp +.formatstamp: @echo "Formatting HTML..." @prettier --write --parser html --print-width 120 "public/**/*.html" > /dev/null @touch .formatstamp diff --git a/bin/build.ts b/bin/build.ts new file mode 100755 index 0000000..d6b6df9 --- /dev/null +++ b/bin/build.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env bun +import fs from "node:fs"; +import log from "loglevel"; + +import generateSite from "../src/templates"; + +log.setLevel((Bun.env.LOG_LEVEL || "info") as log.LogLevelDesc); + +fs.mkdirSync("public", { recursive: true }); +log.info("Generating site..."); +try { + await generateSite(); +} catch (error) { + log.error("Error generating site", error); + process.exit(1); +} + +log.info("Copying static files..."); +if (!fs.existsSync("static")) { + log.error("static directory not found"); + process.exit(1); +} +fs.readdirSync("static").map((file) => { + log.info(`Copying static/${file}`); + fs.cpSync(`static/${file}`, `public/${file}`, { + dereference: true, + recursive: true, + preserveTimestamps: true, + }); +}); diff --git a/bun.lockb b/bun.lockb index 2608647..b4d52ec 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config.toml b/config.toml index f7ec542..b62dbe2 100644 --- a/config.toml +++ b/config.toml @@ -6,6 +6,9 @@ description = "Developer, Emacs User" generate_feed = true +domain_start_date = "2014-06-07" +original_domain = "alanpearce.eu" + theme = "bear" [markdown] @@ -39,20 +42,3 @@ content-security-policy = "default-src 'none'; img-src 'self'; object-src 'none' [[extra.main_menu]] name = "Repositories" url = "https://git.alanpearce.eu" - -[[extra.contact_menu]] - name = "alan@alanpearce.eu" - url = "mailto:alan@alanpearce.eu" - weight = 1 -[[extra.contact_menu]] - name = "Codeberg" - url = "https://codeberg.org/alanpearce" -[[extra.contact_menu]] - name = "GitHub" - url = "https://github.com/alanpearce" -[[extra.contact_menu]] - name = "Mastodon" - url = "https://ieji.de/@alanpearce" -[[extra.contact_menu]] - name = "Bluesky" - url = "https://bsky.app/profile/alanpearce.eu" diff --git a/content/feed-styles/index.md b/content/feed-styles/index.md deleted file mode 100644 index bac7916..0000000 --- a/content/feed-styles/index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -title = "feed-styles" -template = "feed-styles.html" -+++ diff --git a/content/post/_index.md b/content/post/_index.md deleted file mode 100644 index e0d2523..0000000 --- a/content/post/_index.md +++ /dev/null @@ -1,6 +0,0 @@ -+++ -title = "Posts" -sort_by = "date" -paginate_reversed = true -transparent = true -+++ diff --git a/flake.nix b/flake.nix index 3ccad0d..8786bb7 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,6 @@ let pkgs = nixpkgs.legacyPackages.${system}; nativeBuildInputs = with pkgs; [ - zola nodePackages.prettier fd brotli diff --git a/package.json b/package.json index acb0224..47922d2 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,25 @@ "name": "homestead", "module": "src/index.ts", "scripts": { - "start": "bun run ." + "start": "bun run .", + "build": "bun run bin/build.ts" }, "devDependencies": { - "bun-types": "latest" + "bun-types": "latest", + "cheerio": "1.0.0-rc.12", + "highlight.js": "^11.8.0", + "marked": "^9.0.3", + "marked-highlight": "^2.0.6", + "toml-matter": "^1.0.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "type": "module", "dependencies": { - "@sentry/node": "^7.69.0", + "@sentry/node": "^7.70.0", "bun-prometheus-client": "^0.0.2", + "loglevel": "^1.8.1", "toml": "^3.0.0" } } diff --git a/redis.Caddyfile b/redis.Caddyfile deleted file mode 100644 index c0f4bfc..0000000 --- a/redis.Caddyfile +++ /dev/null @@ -1,2 +0,0 @@ -storage redis { -} diff --git a/src/app.ts b/src/app.ts index 727ec2f..57ae71d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,17 +3,17 @@ import fs, { Stats } from "node:fs"; import type { BunFile, Serve } from "bun"; import * as Sentry from "@sentry/node"; import prom from "bun-prometheus-client"; +import log from "loglevel"; -import readConfig from "./config"; +import config from "./config"; + +log.setLevel((Bun.env.LOG_LEVEL || "info") as log.LogLevelDesc); Sentry.init({ release: `homestead@${Bun.env.FLY_MACHINE_VERSION}`, tracesSampleRate: 1.0, }); -const publicDir = "public" + path.sep; - -const config = readConfig(); const defaultHeaders = { ...config.extra.headers, vary: "Accept-Encoding", @@ -85,13 +85,14 @@ function walkDirectory(root: string, dir: string) { if (dir !== "") { registerFile(relPath, dir + path.sep, absPath, stat); } + } else { + registerFile(relPath, relPath, absPath, stat); } - registerFile(relPath, relPath, absPath, stat); } } } -walkDirectory(publicDir, ""); +walkDirectory("public/", ""); async function serveFile( file: File | undefined, @@ -194,8 +195,22 @@ export const server = { } transaction.setHttpStatus(200); transaction.setTag("http.content-encoding", "identity"); - return serveFile(file); + return serveFile(file, 200, { + "content-type": file.type, + }); } else { + if (files.has(pathname + "/")) { + log.info(`Redirecting to: ${pathname + "/"}`); + metrics.requests.inc({ + method: request.method, + path: pathname, + status_code: 302, + }); + return new Response("", { + status: 302, + headers: { location: pathname + "/" }, + }); + } metrics.requests.inc({ method: request.method, path: pathname, @@ -203,7 +218,18 @@ export const server = { }); transaction.setHttpStatus(404); transaction.setTag("http.content-encoding", "identity"); - return serveFile(files.get("/404.html"), 404); + const notfound = files.get("/404.html"); + if (notfound) { + return serveFile(notfound, 404, { + "content-type": "text/html; charset=utf-8", + }); + } else { + log.warn("404.html not found"); + return new Response("404 Not Found", { + status: 404, + headers: { "content-type": "text/plain", ...defaultHeaders }, + }); + } } } catch (error) { transaction.setTag("http.content-encoding", "identity"); diff --git a/src/config.ts b/src/config.ts index 8fbe3c1..a96b9c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,4 @@ import path from "node:path"; import fs from "node:fs"; import toml from "toml"; -export default function readConfig() { - return toml.parse(fs.readFileSync("config.toml", "utf-8")); -} +export default toml.parse(fs.readFileSync("config.toml", "utf-8")); diff --git a/src/index.ts b/src/index.ts index 83ad03d..c18ebb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ +import log from "loglevel"; + import { server, metricsServer } from "./app"; +log.setLevel((Bun.env.LOG_LEVEL || "info") as log.LogLevelDesc); + const metricsServed = Bun.serve(metricsServer); console.info(`Metrics server started on port ${metricsServed.port}`); diff --git a/src/posts.ts b/src/posts.ts new file mode 100644 index 0000000..c727e12 --- /dev/null +++ b/src/posts.ts @@ -0,0 +1,63 @@ +import path from "node:path"; +import fs from "node:fs/promises"; + +import { matter } from "toml-matter"; +import * as marked from "marked"; + +type MatterFile = ReturnType; + +export type Post = { + input: string; + output: string; + basename: string; + url: string; + title: string; + date: Date; + description: string | undefined; + taxonomies: Record; +}; + +export async function getPost(filename: string): Promise { + return matter(await Bun.file(filename).text()); +} + +export async function readPosts( + root: string, + inputDir: string, + outputDir: string, +): Promise<{ posts: Array; tags: Set }> { + let tags = new Set(); + let posts = new Array(); + const subdir = path.join(root, inputDir); + for (let pathname of await fs.readdir(subdir)) { + const pathFromDir = path.join(inputDir, pathname); + const pathFromRoot = path.join(subdir, pathname); + const stat = await fs.stat(pathFromRoot); + if (stat.isFile() && path.extname(pathname) === ".md") { + if (pathname !== "_index.md") { + const input = pathFromRoot; + const output = pathFromRoot + .replace(root, outputDir) + .replace(".md", "/index.html"); + const url = pathFromRoot.replace(root, "").replace(".md", "/"); + + const file = await getPost(input); + + file.data.taxonomies?.tags?.map((t: string) => + tags.add(t.toLowerCase()), + ); + posts.push({ + input, + output, + basename: path.basename(pathname, ".md"), + url, + ...file.data, + } as Post); + } + } + } + return { + posts: posts.sort((a, b) => b.date.getTime() - a.date.getTime()), + tags, + }; +} diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..50c45ab --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,238 @@ +import * as fs from "node:fs/promises"; +import * as cheerio from "cheerio"; +import { matter } from "toml-matter"; +import { Marked } from "marked"; +import log from "loglevel"; + +import config from "./config"; +import { getPost, readPosts, type Post } from "./posts"; + +const marked = new Marked(); +marked.use({ + gfm: true, +}); + +function addMenu( + parent: cheerio.Cheerio, + child: cheerio.Cheerio, +) { + for (const link of config.extra.main_menu) { + parent.append(child.clone().attr("href", link.url).text(link.name)); + } +} + +export async function layout( + $: cheerio.CheerioAPI, + pageTitle: string, +): Promise { + $("html").attr("lang", config.default_language); + $("head > link[rel=alternate]").attr("title", config.title); + addMenu($("nav"), $("nav a")); + $(".title").text(config.title); + $("title").text(pageTitle); + $(".p-name").text(pageTitle); + $("head") + .children("style") + .text(await Bun.file("templates/style.css").text()); + return $; +} + +async function render404Page(): Promise { + const $ = await layout( + cheerio.load(await Bun.file("templates/404.html").text()), + "404 Not Found", + ); + return $.html(); +} + +async function renderHomepage(posts: Array): Promise { + const file = matter(await Bun.file("content/_index.md").text()); + const $ = await layout( + cheerio.load(await Bun.file("templates/index.html").text()), + config.title, + ); + $("#content").html(marked.parse(file.content)); + const $feed = $(".h-feed"); + const $tpl = $(".h-entry").remove(); + + for (const post of posts) { + const $post = $tpl.clone(); + $post.find(".p-name").text(post.title); + $post.find(".u-url").attr("href", post.url); + $post + .find(".dt-published") + .attr("datetime", post.date.toISOString()) + .text(post.date.toISOString().slice(0, 10)); + $post.appendTo($feed); + } + return $.html(); +} + +async function renderPost(file: Post, content: string) { + const $ = await layout( + cheerio.load(await Bun.file("templates/post.html").text()), + file.title, + ); + + $(".h-entry .dt-published") + .attr("datetime", file.date.toISOString()) + .text(file.date.toISOString().slice(0, 10)); + $(".h-entry .e-content").html(content); + const categories = $(".h-entry .p-categories"); + const cat = categories.find(".p-category").parentsUntil(categories); + cat.remove(); + for (const tag of file.taxonomies.tags) { + categories.append( + cat + .clone() + .find(".p-category") + .attr("href", `/tags/${tag}/`) + .text(`#${tag}`) + .parent(), + ); + } + + return $.html(); +} + +async function renderListPage(tag: string, posts: Post[]) { + const $ = await layout( + cheerio.load(await Bun.file("templates/tag.html").text()), + tag || config.title, + ); + const $feed = $(".h-feed"); + const $tpl = $(".h-entry").remove(); + + for (const post of posts) { + const $post = $tpl.clone(); + $post.find(".p-name").text(post.title); + $post.find(".u-url").attr("href", post.url); + $post + .find(".dt-published") + .attr("datetime", post.date.toISOString()) + .text(post.date.toISOString().slice(0, 10)); + $post.appendTo($feed); + } + return $.html(); +} + +async function renderTags(tags: string[]) { + const $ = await layout( + cheerio.load(await Bun.file("templates/tags.html").text()), + config.title, + ); + const $tags = $(".tags"); + const $tpl = $(".h-feed"); + $tpl.remove(); + for (const tag of tags) { + const $tag = $tpl.clone(); + $tag.find("a").attr("href", `/tags/${tag}/`).text(`#${tag}`); + $tag.appendTo($tags); + } + return $.html(); +} + +const makeTagURI = (specific: string) => + `tag:${config.original_domain},${config.domain_start_date}:${specific}`; + +async function renderFeed(title: string, posts: Post[], tag?: string) { + const $ = cheerio.load(await Bun.file("templates/feed.xml").text(), { + xml: true, + }); + const $feed = $("feed"); + $feed.children("title").text(title); + $feed.children("link").attr("href", config.base_url); + $feed.children("id").text(makeTagURI(tag || "feed")); + $feed.children("updated").text(posts[0].date.toISOString()); + + const $tpl = $("feed > entry").remove(); + for (const post of posts) { + const $post = $tpl.clone(); + $post.children("title").text(post.title); + $post + .children("link") + .attr("href", new URL(post.url, config.base_url).href); + $post.children("id").text(makeTagURI(post.basename)); + $post.children("updated").text(post.date.toISOString()); + $post.find("author > name").text(config.title); + $post.children("summary").text(post.description || ""); + const content = marked.parse((await getPost(post.input)).content); + $post.children("content").html(Bun.escapeHTML(await content)); + $post.appendTo($feed); + } + + return $.xml(); +} + +async function renderFeedStyles() { + const $ = cheerio.load(await Bun.file("templates/feed-styles.xsl").text(), { + xml: true, + }); + $("style").text(await Bun.file("templates/style.css").text()); + return $.xml(); +} + +export default async function generateSite() { + const tasks = []; + const { posts, tags } = await readPosts("content", "post", "public"); + await fs.mkdir("public/post", { recursive: true }); + for (const post of posts) { + const content = await marked.parse((await getPost(post.input)).content); + await fs.mkdir(`public/post/${post.basename}`, { recursive: true }); + tasks.push(async () => { + log.debug(`Rendering post ${post.basename} to ${post.output}`); + return Bun.write(post.output, await renderPost(post, content)); + }); + } + await fs.mkdir("public/tags", { recursive: true }); + tasks.push(async () => { + log.debug("Rendering tags page to public/tags/index.html"); + return Bun.write("public/tags/index.html", await renderTags([...tags])); + }); + for (const tag of tags) { + log.debug(`Processing tag ${tag}`); + const matchingPosts = posts.filter((p) => p.taxonomies.tags.includes(tag)); + await fs.mkdir(`public/tags/${tag}`, { recursive: true }); + tasks.push(async () => { + log.debug(`Rendering tag ${tag} to public/tags/${tag}/index.html`); + return Bun.write( + `public/tags/${tag}/index.html`, + await renderListPage(tag, matchingPosts), + ); + }); + + tasks.push(async () => { + log.debug(`Rendering tag ${tag} feed to public/tags/${tag}/atom.xml`); + return Bun.write( + `public/tags/${tag}/atom.xml`, + await renderFeed(`${config.title} - ${tag}`, matchingPosts), + ); + }); + } + tasks.push(async () => { + log.debug("Rendering posts page to public/post/index.html"); + return Bun.write("public/post/index.html", await renderListPage("", posts)); + }); + tasks.push(async () => { + log.debug("Rendering site feed to public/atom.xml"); + return Bun.write("public/atom.xml", await renderFeed(config.title, posts)); + }); + tasks.push(async () => { + log.debug("Rendering feed styles to public/feed-styles.xsl"); + return Bun.write("public/feed-styles.xsl", await renderFeedStyles()); + }); + tasks.push(async () => { + log.debug("Rendering homepage to public/index.html"); + return Bun.write( + "public/index.html", + await renderHomepage(posts.slice(0, 3)), + ); + }); + tasks.push(async () => { + log.debug("Rendering 404 page to public/404.html"); + return Bun.write("public/404.html", await render404Page()); + }); + for (const task of tasks) { + await task(); + } +} diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..4e64fcc --- /dev/null +++ b/templates/404.html @@ -0,0 +1,37 @@ + + + + + + Site Title + + + + + + +
+

+ Site title +

+ +
+
+

404

+

ʕノ•ᴥ•ʔノ ︵ ┻━┻

+
+ + + diff --git a/templates/feed-styles.html b/templates/feed-styles.html deleted file mode 100644 index a66ddc0..0000000 --- a/templates/feed-styles.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - RSS Feed | <xsl:value-of select="/atom:feed/atom:title"/> - - - - - - -
-
- This is an RSS feed. Subscribe by copying the URL - from the address bar into your newsreader. Visit - About Feeds - to learn more and get started. It's free. -
-
-

- - - - - - - - - - RSS Feed Preview | - - - -

- - -
-
- - -
-
diff --git a/templates/feed-styles.xsl b/templates/feed-styles.xsl new file mode 100644 index 0000000..0197e87 --- /dev/null +++ b/templates/feed-styles.xsl @@ -0,0 +1,82 @@ + + + + + + + RSS Feed | <xsl:value-of select="/atom:feed/atom:title"/> + + + + + + +
+
+ This is an RSS feed. Subscribe by copying the URL + from the address bar into your newsreader. Visit + About Feeds + to learn more and get started. It's free. +
+
+

+ + + + + + + + + + RSS Feed Preview | + + + +

+ + +
+
+ + +
+
diff --git a/templates/feed.xml b/templates/feed.xml new file mode 100644 index 0000000..ddc90dd --- /dev/null +++ b/templates/feed.xml @@ -0,0 +1,24 @@ + + + + Example Feed + + urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 + 2003-12-13T18:30:02Z + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + +
+

This is the entry content.

+
+
+ + John Doe + +
+ +
diff --git a/templates/footer.html b/templates/footer.html deleted file mode 100644 index 422c90c..0000000 --- a/templates/footer.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/index.html b/templates/index.html index a79b1c8..b4832a9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,39 +1,64 @@ -{% extends "base.html" %} - -{% block body_attrs %} class="h-card vcard"{% endblock %} - -{% block title_class %} p-name fn{% endblock %} - -{% block main -%} -
-
- {{ section.content | safe -}} -
-
-

Latest Posts

-
    - {%- set section = get_section(path="post/_index.md") %} - {%- for page in section.pages | slice(end=3) %} + + + + + + Site Title + + + + + + +
    +

    + Site title +

    + +
    +
    +
    +
    +

    Latest Posts

    + -
    -
    -

    Elsewhere on the Internet

    -
      - {%- for item in config.extra.contact_menu %} -
    • - {%- if item.url is starting_with("mailto:") -%} - - {%- else -%} - {{ item.name }} - {%- endif -%} -
    • - {%- endfor %} -
    -
    -
    -{% endblock %} +
+
+
+

Elsewhere on the Internet

+ +
+
+ + + diff --git a/templates/post.html b/templates/post.html new file mode 100644 index 0000000..2a66058 --- /dev/null +++ b/templates/post.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + +
+

+ +

+ +
+
+
+

Post Title

+

+ +

+
+ Enim lobortis scelerisque fermentum dui faucibus in ornare quam + viverra. Eget egestas purus viverra accumsan in nisl nisi, scelerisque + eu ultrices vitae, auctor eu augue ut lectus arcu, bibendum at. +
+ +
+
+ + + diff --git a/templates/style.css b/templates/style.css new file mode 100644 index 0000000..23722e1 --- /dev/null +++ b/templates/style.css @@ -0,0 +1,196 @@ +body { + font-family: Verdana, sans-serif; + margin: auto; + padding: 20px; + max-width: 720px; + text-align: left; + background-color: #fff; + word-wrap: break-word; + overflow-wrap: break-word; + line-height: 1.5; + color: #444; +} + +.skip { + position: absolute; + top: -3em; + background: #fff; +} +.skip:focus { + top: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6, +strong, +b { + color: #222; +} + +a { + color: #3273dc; +} + +.title { + color: #222; + text-decoration: none; + border: 0; +} + +time { + font-style: italic; +} + +.title span { + font-weight: 400; +} + +nav a { + margin-right: 10px; +} + +.tags { + padding: unset; + font-size: small; +} + +.tags > li { + list-style: none; + display: inline-block; + padding-right: 1ex; +} + +textarea { + width: 100%; + font-size: 1rem; +} + +input { + font-size: 1rem; +} + +main, +article { + line-height: 1.6; +} + +table { + width: 100%; +} + +img { + max-width: 100%; +} + +code { + padding: 2px 5px; + background-color: #f2f2f2; +} + +pre code { + color: #222; + display: block; + padding: 20px; + white-space: pre-wrap; + font-size: 0.875rem; + overflow-x: auto; +} + +div.highlight pre { + background-color: initial; + color: initial; +} + +div.highlight code { + background-color: unset; + color: unset; +} + +blockquote { + border-left: 1px solid #999; + color: #222; + padding-left: 20px; + font-style: italic; +} + +footer { + padding: 25px; + text-align: center; +} + +.helptext { + color: #777; + font-size: small; +} + +.errorlist { + color: #eba613; + font-size: small; +} + +/* blog posts */ +ul.blog-posts { + list-style-type: none; + padding: unset; +} + +ul.blog-posts li { + display: flex; +} + +ul.blog-posts li span { + flex: 0 0 130px; +} + +ul.blog-posts li a:visited { + color: #8b6fcb; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #333; + color: #ddd; + } + + h1, + h2, + h3, + h4, + h5, + h6, + strong, + b, + .title { + color: #eee; + } + + a { + color: #8cc2dd; + } + + code { + background-color: #777; + } + + pre code { + color: #ddd; + } + + blockquote { + color: #ccc; + } + + textarea, + input { + background-color: #252525; + color: #ddd; + } + + .helptext { + color: #aaa; + } +} diff --git a/templates/tag.html b/templates/tag.html new file mode 100644 index 0000000..ae5b8e9 --- /dev/null +++ b/templates/tag.html @@ -0,0 +1,49 @@ + + + + + + Site Title + + + + + + +
+

+ Site Title +

+ +
+
+ +
+ + + diff --git a/templates/tags.html b/templates/tags.html new file mode 100644 index 0000000..a724c62 --- /dev/null +++ b/templates/tags.html @@ -0,0 +1,40 @@ + + + + + + Site Title + + + + + + +
+

+ Site title +

+ +
+
+ +
+ + + diff --git a/themes/bear/.envrc b/themes/bear/.envrc deleted file mode 100644 index 3550a30..0000000 --- a/themes/bear/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/themes/bear/.gitignore b/themes/bear/.gitignore deleted file mode 100644 index 87174b6..0000000 --- a/themes/bear/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/public/ diff --git a/themes/bear/LICENSE b/themes/bear/LICENSE deleted file mode 100644 index ddc924b..0000000 --- a/themes/bear/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2023 Alan Pearce -Copyright (c) 2020 Jan Raasch - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/themes/bear/README.md b/themes/bear/README.md deleted file mode 100644 index db77233..0000000 --- a/themes/bear/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Zola ʕ•ᴥ•ʔ Bear Blog - -[![Netlify Status](https://api.netlify.com/api/v1/badges/121b53ce-c913-4604-9179-eb3cca31cd2c/deploy-status)](https://app.netlify.com/sites/zola-bearblog/deploys) - -🧸 A [Zola](https://www.getzola.org/)-theme based on [Bear Blog](https://bearblog.dev). - -> Free, no-nonsense, super-fast blogging. - -## Demo - -For a current & working demo of this theme, please check out 🎯. - -## Screenshot - -![Screenshot][screenshot] - -When the user's browser is running »dark mode«, the dark color scheme will be used automatically. The default is the light/white color scheme. Check out the [`style.html`](https://codeberg.org/alanpearce/zola-bearblog/src/branch/main/templates/style.html)-file for the implementation. - -## Installation - -If you already have a Zola site on your machine, you can simply add this theme via - -``` -git submodule add https://codeberg.org/alanpearce/zola-bearblog themes/zola-bearblog -``` - -Then, adjust the `config.toml` as detailed below. - -For more information, read the official [setup guide][zola-setup-guide] of Zola. - -Alternatively, you can quickly deploy a copy of the theme site to Netlify using this button: - -[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://gitlab.com/alanpearce/zola-bearblog) - -(Note that this method makes it harder to keep up-to-date with theme updates, which might be necessary for newer versions of Zola.) - -## Adjust configuration / config.toml - -Please check out the included [config.toml](https://codeberg.org/alanpearce/zola-bearblog/src/branch/main/config.toml) - -## Content & structure - -### Menu - -Create an array in `extra` with a key of `main_menu`. `url` is passed to [`get_url`](https://www.getzola.org/documentation/templates/overview/#get-url) - -```toml -[[extra.main_menu]] -name = "Bear" -url = "@/bear.md" - -[[extra.main_menu]] -name = "Zola" -url = "@/zola.md" - -[[extra.main_menu]] -name = "Blog" -url = "@/blog/_index.md" -``` - -### Adding / editing content - -#### Index-Page - -The contents of the `index`-page may be changed by editing your `content/_index.md`-file. - - -### Adding your branding / colors / css - -Add a `custom_head.html`-file to your `templates/`-directory. In there you may add a ` - {% include "custom_head.html" ignore missing -%} - - - {% include "header.html" ignore missing -%} - {% block main %}{%- endblock -%} - {% include "footer.html" ignore missing -%} - {% include "custom_body.html" ignore missing -%} - - diff --git a/themes/bear/templates/footer.html b/themes/bear/templates/footer.html deleted file mode 100644 index c952a93..0000000 --- a/themes/bear/templates/footer.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/themes/bear/templates/header.html b/themes/bear/templates/header.html deleted file mode 100644 index 55c1756..0000000 --- a/themes/bear/templates/header.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-

- - {{- config.title -}} - -

- -
{{ "" -}} diff --git a/themes/bear/templates/index.html b/themes/bear/templates/index.html deleted file mode 100644 index 6caf1d5..0000000 --- a/themes/bear/templates/index.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
- {{ section.content | safe }} -
-{% endblock %} diff --git a/themes/bear/templates/nav.html b/themes/bear/templates/nav.html deleted file mode 100644 index fe5fdd6..0000000 --- a/themes/bear/templates/nav.html +++ /dev/null @@ -1,6 +0,0 @@ - Home -{%- if config.extra.main_menu %} - {%- for item in config.extra.main_menu %} - {{ item.name }} - {%- endfor %} -{%- endif -%} diff --git a/themes/bear/templates/page.html b/themes/bear/templates/page.html deleted file mode 100644 index 93611b5..0000000 --- a/themes/bear/templates/page.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ page.title }} | {{ super() }}{% endblock %} - -{% block main %} - {%- if not page.extra.menu %} -

{{ page.title }}

- {%- if page.date %} -

- -

- {%- endif %} - {%- endif %} -
- {{ page.content | trim | safe }} -
-
    - {%- if page.taxonomies %} - {%- for name, taxon in page.taxonomies %} - {{ name | capitalize }}: - {%- for item in taxon %} -
  • #{{ item }}
  • - {%- endfor %} - {%- endfor %} - {%- endif %} -
-{% endblock %} diff --git a/themes/bear/templates/section.html b/themes/bear/templates/section.html deleted file mode 100644 index f633036..0000000 --- a/themes/bear/templates/section.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
- {%- if taxonomy.term %} -

Filtering for "{{ section.title }}"

- - Remove filter - - {%- endif %} -
    - {%- for page in section.pages %} -
  • - - - - {{ page.title }} -
  • - {% else %} -
  • - No posts yet -
  • - {%- endfor %} -
-
    - {%- set tags = get_taxonomy(kind="tags") %} - {%- for post in tags.items %} -
  • #{{ post.name }}
  • - {%- endfor %} -
-
-{% endblock %} diff --git a/themes/bear/templates/security_tags.html b/themes/bear/templates/security_tags.html deleted file mode 100644 index 0f922ea..0000000 --- a/themes/bear/templates/security_tags.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/themes/bear/templates/seo_tags.html b/themes/bear/templates/seo_tags.html deleted file mode 100644 index 4eb2bc8..0000000 --- a/themes/bear/templates/seo_tags.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/themes/bear/templates/style.css b/themes/bear/templates/style.css deleted file mode 100644 index e1e12aa..0000000 --- a/themes/bear/templates/style.css +++ /dev/null @@ -1,193 +0,0 @@ -body { - font-family: Verdana, sans-serif; - margin: auto; - padding: 20px; - max-width: 720px; - text-align: left; - background-color: #fff; - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.5; - color: #444; -} - -.skip { - position: absolute; - top: -3em; - background: #fff; -} -.skip:focus { - top: 0; -} - -h1, -h2, -h3, -h4, -h5, -h6, -strong, -b { - color: #222; -} - -a { - color: #3273dc; -} - -.title { - color: #222; - text-decoration: none; - border: 0; -} - -time { - font-style: italic; -} - -.title span { - font-weight: 400; -} - -nav a { - margin-right: 10px; -} - -.tags { - padding: unset; - font-size: small; -} - -.tags > li { - list-style: none; - display: inline-block; -} - -textarea { - width: 100%; - font-size: 1rem; -} - -input { - font-size: 1rem; -} - -main,article { - line-height: 1.6; -} - -table { - width: 100%; -} - -img { - max-width: 100%; -} - -code { - padding: 2px 5px; - background-color: #f2f2f2; -} - -pre code { - color: #222; - display: block; - padding: 20px; - white-space: pre-wrap; - font-size: 0.875rem; - overflow-x: auto; -} - -div.highlight pre { - background-color: initial; - color: initial; -} - -div.highlight code { - background-color: unset; - color: unset; -} - -blockquote { - border-left: 1px solid #999; - color: #222; - padding-left: 20px; - font-style: italic; -} - -footer { - padding: 25px; - text-align: center; -} - -.helptext { - color: #777; - font-size: small; -} - -.errorlist { - color: #eba613; - font-size: small; -} - -/* blog posts */ -ul.blog-posts { - list-style-type: none; - padding: unset; -} - -ul.blog-posts li { - display: flex; -} - -ul.blog-posts li span { - flex: 0 0 130px; -} - -ul.blog-posts li a:visited { - color: #8b6fcb; -} - -@media (prefers-color-scheme: dark) { - body { - background-color: #333; - color: #ddd; - } - - h1, - h2, - h3, - h4, - h5, - h6, - strong, - b { - color: #eee; - } - - a { - color: #8cc2dd; - } - - code { - background-color: #777; - } - - pre code { - color: #ddd; - } - - blockquote { - color: #ccc; - } - - textarea, - input { - background-color: #252525; - color: #ddd; - } - - .helptext { - color: #aaa; - } -} diff --git a/themes/bear/templates/style.css.html b/themes/bear/templates/style.css.html deleted file mode 120000 index f6b71cc..0000000 --- a/themes/bear/templates/style.css.html +++ /dev/null @@ -1 +0,0 @@ -style.css \ No newline at end of file diff --git a/themes/bear/templates/taxonomy_list.html b/themes/bear/templates/taxonomy_list.html deleted file mode 100644 index abf4294..0000000 --- a/themes/bear/templates/taxonomy_list.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
- -
- {% set tags = get_taxonomy(kind="tags") %} - {% for post in tags.items %} - #{{ post.name }}  - {% endfor %} -
-
-
-{% endblock %} diff --git a/themes/bear/templates/taxonomy_single.html b/themes/bear/templates/taxonomy_single.html deleted file mode 100644 index a96139c..0000000 --- a/themes/bear/templates/taxonomy_single.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base.html" %} - -{% block rss -%} - - {%- set rss_path = "/tags/" ~ term.name ~ "/" ~ config.feed_filename %} - -{%- endblock -%} - -{% block main -%} -
- {%- if taxonomy.term %} -

Filtering for "{{ term.name }}"

- - Remove filter - - {%- endif %} -
    - {%- for page in term.pages %} -
  • - - - - {{ page.title }} -
  • - {% else %} -
  • - No posts yet -
  • - {%- endfor %} -
-
-{% endblock %} diff --git a/themes/bear/theme.toml b/themes/bear/theme.toml deleted file mode 100644 index 902c8ec..0000000 --- a/themes/bear/theme.toml +++ /dev/null @@ -1,25 +0,0 @@ -name = "Bear" -description = "Bear blog theme" -license = "MIT" -homepage = "https://codeberg.org/alanpearce/zola-bearblog" -# The minimum version of Zola required -min_version = "0.4.0" -# An optional live demo URL -demo = "https://zola-bearblog.netlify.app/" - -# Any variable there can be overridden in the end user `config.toml` -# You don't need to prefix variables by the theme name but as this will -# be merged with user data, some kind of prefix or nesting is preferable -# Use snake_casing to be consistent with the rest of Zola -[extra] -date_format="%d %b, %Y" -webserver_sends_csp_headers=false - -[author] -name = "Alan Pearce" -homepage = "https://alanpearce.eu" - -[original] -author = "janraasch" -repo = "https://github.com/janraasch/hugo-bearblog/" -homepage = "https://www.janraasch.com" diff --git a/tsconfig.json b/tsconfig.json index 1c542d7..79f7630 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,9 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": true, + "paths": { + "toml-matter": ["./node_modules/toml-matter"] + }, "types": [ "bun-types" // add Bun global ] -- cgit 1.4.1