This commit is contained in:
2026-03-21 13:47:17 +00:00
commit bbb2111ce6
28 changed files with 2905 additions and 0 deletions

147
theme/templates/base.html Normal file
View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="{{ config.language }}">
<head>
{% block head %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="Norgolith" />
{# Dynamic meta tags #}
{% if metadata.description and not metadata.description == "nil" %}
<meta name="description" content="{{ metadata.description }}" />
{% endif %}
{% if metadata.authors %}
<meta name="author" content="{{ metadata.authors | join(sep=", ") }}" />
{% endif %}
{% if metadata.categories %}
<meta name="keywords" content="{{ metadata.categories | join(sep=", ") }}" />
{% endif %}
{# SEO Meta Tags #}
{% block seo_tags %}{% endblock seo_tags %}
{# Syntax highlighter #}
{% if config.highlighter is defined and config.highlighter.enable %}
{# If highlighter is enabled but the engine is not defined then fallback to prismjs #}
{% if config.highlighter.engine is not string or config.highlighter.engine == "prism" %}
{# PrismJS #}
{# Themes from automadcms:
<link
rel="stylesheet"
href="https://unpkg.com/automad-prism-themes@0.3.1/dist/prism-catppuccin-frappe.light-dark.css"
/>
#}
<link rel="stylesheet" href="/assets/css/prism-sweetie.min.css" />
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
{% elif config.highlighter.engine is defined
and config.highlighter.engine == "hljs" %}
{# Highlight.js #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script> #}
{# Enable this one instead if you want all the `<code>` tags to be highlighted
<script>
document.addEventListener("DOMContentLoaded", (event) => {
document.querySelectorAll("code").forEach((block) => {
hljs.highlightBlock(block);
});
});
</script>
#}
{% elif config.highlighter.engine is string
and config.highlighter.engine not in ["prism", "hljs"] %}
<script>
window.alert("Warning: highlighter is enabled in the site configuration but its engine is not 'prism' nor 'hljs'");
</script>
{% endif %}
{% endif %}
{# AlpineJS #}
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("menu", () => ({
currentPage: window.location.pathname,
openMobile: false,
toggleMobile() {
this.openMobile = !this.openMobile;
}
}));
Alpine.data("toc", () => ({
open: false,
toggle() {
this.open = !this.open;
document.querySelector("#toc-toggle-icon").classList.toggle("rotate-90");
}
}));
Alpine.data("theme", () => ({
// Defaults to dark theme
current: "dark",
init() {
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark") {
this.current = "dark";
} else if (storedTheme === "light") {
this.current = "light";
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
this.current = "dark";
}
localStorage.setItem("theme", this.current);
if (this.current === "dark") {
document.documentElement.classList.add("dark");
}
},
toggle() {
this.current = this.current === "dark" ? "light" : "dark";
document.documentElement.classList.toggle("dark", this.current === "dark");
localStorage.setItem("theme", this.current);
}
}));
});
</script>
{# MermaidJS #}
{% if config.extra.enable_mermaid %}
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.9.0/mermaid.min.js"></script>
{% endif %}
{# Icons #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tabler-icons/3.28.1/tabler-icons.min.css">
{# Styling and favicon #}
<link rel="stylesheet" href="/assets/css/styles.min.css" />
{% if config.extra.favicon_path is defined and config.extra.favicon_path is string %}
<link rel="icon" href={{ config.extra.favicon_path }} />
{% else %}
<link rel="icon" href="/assets/norgolith.svg" />
{% endif %}
<title>{% block title %}{% endblock title %} - {{ config.title | title }}</title>
{% endblock head %}
</head>
<body>
<div class="transition-colors duration-150 ease-linear">
<header class="relative shadow-sm">
{% include "partials/nav.html" %}
</header>
<main class="container mx-auto min-h-screen pt-8 px-4 md:px-0">
{% block content %}{% endblock content %}
</main>
<footer class="mt-8 py-4 px-6 w-full font-mono">
{% include "partials/footer.html" %}
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="Categories - {{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ config.rootUrl }}/categories" />
<meta property="og:description" content="Posts categories" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
{# TODO: add support for og:image using metadata #}
<link rel="canonical" href="{{ config.rootUrl }}/categories" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}Categories{% endblock title %}
{% block content %}
<div>
<h1>Categories</h1>
<p class="text-sm text-text/70"><i>All the categories used in posts.</i></p>
<ul class="mt-6 pt-6 border-t border-t-base-alt dark:border-t-[#39394b]">
{% for category in categories | sort %}
{%- set_global cat_posts = 0 -%}
{% for post in posts %}
{% if category in post.categories %}
{%- set_global cat_posts = cat_posts + 1 -%}
{% endif %}
{% endfor %}
<li>
<a class="font-mono text-lg no-underline! hover:underline! hover:decoration-dashed" href="/categories/{{ category }}">{{ category }}</a>
<span class="text-grey">({{ cat_posts }} post{{ cat_posts | pluralize }})</span>
</li>
{% endfor %}
</ul>
</div>
{% endblock content %}

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="Category: {{ category }} - {{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ config.rootUrl }}/categories/{{ category }}" />
<meta property="og:description" content="Posts on category {{ category }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
{# TODO: add support for og:image using metadata #}
<link rel="canonical" href="{{ config.rootUrl }}/categories/{{ category }}" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}Category: {{ category }}{% endblock title %}
{% block content %}
<div>
<h1>Posts in {{ category }}</h1>
<p class="text-sm text-text/70"><i>All the posts with the category "{{ category }}"</i></p>
<ul class="mt-6 pt-6 border-t border-t-base-alt dark:border-t-[#39394b]">
{% for post in posts | filter(attribute="draft", value=false) | sort(attribute="created") | reverse %}
{% if category in post.categories %}
<li>
<div class="flex flex-col md:flex-row space-x-4">
<a class="font-mono text-lg w-fit no-underline! hover:underline! hover:decoration-dashed" href="{{ post.permalink }}">{{ post.title }}</a>
<time class="text-grey text-sm italic"> on <strong>{{ post.created | date(format="%B %e, %Y") }}</strong></time>
</div>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endblock content %}

View File

@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="{{ metadata.title | title }} - {{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ metadata.permalink }}" />
<meta property="og:description" content="{{ metadata.description }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
{# TODO: add support for og:image using metadata #}
<link rel="canonical" href="{{ metadata.permalink }}" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}{{ metadata.title | title }}{% endblock title %}
{% block content %}
{{ content | safe }}
{% endblock content %}

45
theme/templates/home.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="{{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ metadata.permalink }}" />
<meta property="og:description" content="{{ metadata.description }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
{# TODO: add support for og:image using metadata #}
<link rel="canonical" href="{{ config.rootUrl }}" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}{{ metadata.title | title }}{% endblock title %}
{% block content %}
{{ content | safe }}
{# Latest 5 non-draft blog posts, ordered automatically by date #}
{% if posts | filter(attribute="draft", value=false) | length > 0 %}
<section>
<h2>Recent posts</h2>
{% for post in posts | filter(attribute="draft", value=false) | sort(attribute="created") | reverse | slice(end=5) %}
<div class="bg-surface p-4 mt-4 border-2 border-base-alt rounded-md shadow-lg">
<div class="flex flex-col">
<h3 class="mt-0! text-text-alt"><a class="no-underline! hover:underline! hover:decoration-dashed" href="{{ post.permalink }}">{{ post.title }}</a></h3>
<time class="text-dark-grey dark:text-grey" datetime="{{ post.created }}">{{ post.created | date(format="%B %e, %Y") }}</time>
<span class="text-sm text-grey italic">{{ post.description }}</span>
</div>
<div class="flex flex-col">
<p>
{% if post.truncate_char and post.truncate_char is matching("^nil$") %}
{% set truncate_char = "" %}
{% else %}
{% set truncate_char = "…" %}
{% endif %}
{{ post.raw | striptags | truncate(length=post.truncate | default(value=100), end=truncate_char) | safe }}
</p>
<a class="no-underline! text-dark-grey font-semibold" href="{{ post.permalink }}">Read more …</a>
</div>
</div>
{% endfor %}
</section>
{% endif %}
{% endblock content %}

View File

@@ -0,0 +1,30 @@
<div
class="flex flex-col md:flex-row justify-between md:items-center font-medium text-xs md:text-sm text-text-alt space-y-4 md:space-y-0"
>
<span
>Copyright &copy; {{ now(format="%Y") }}
{% if config.extra.footer_author_link %}
<a
href="{{ config.extra.footer_author_link }}"
class="text-blue hover:underline">{{ config.author }}</a
>.
{% else %}
{{ config.author }}.
{% endif %}
{% if config.extra.license %}
<br class="md:hidden" /> Licensed under {{ config.extra.license }}.
{% endif %}
</span>
<div class="flex flex-inline">
{% for name, link in config.extra.footer %}
<div class="mr-4 md:mr-6 lg:mr-8 last:mr-0">
<a
href="{{ link }}"
class="hover:text-blue"
>
<span>{{ name | title }}</span>
</a>
</div>
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,72 @@
<nav x-data="menu" class="container mx-auto px-4 md:px-0 font-mono">
<div class="flex items-center justify-between h-16">
<!-- Logo and dark mode -->
<div x-data="theme" class="flex items-center shrink-0 space-x-2">
<a href="/" class="flex items-center space-x-2">
<!-- <img src="{{ config.rootUrl }}/assets/norgolith.svg" alt="Norgolith Logo" class="h-8 w-8"> -->
<span class="text-lg md:text-xl font-bold text-magenta">{{ config.title | title }}</span>
</a>
<button
@click="toggle()"
type="button"
class="rounded-lg flex items-center"
:aria-label="current === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
>
<i
:class="current === 'dark' ? 'ti-sun hover:text-yellow' : 'ti-moon hover:text-blue'"
class="ti text-xl md:text-2xl text-grey transition-colors duration-200 ease-in-out"
></i>
</button>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex md:items-center md:space-x-8">
{% for name, link in config.extra.nav %}
<a
:class="{ 'text-magenta!': currentPage.startsWith('{{ link }}') }"
class="text-text-alt hover:text-blue px-3 py-2 rounded-md text-sm font-medium"
href="{{ link }}"
>{{ name | title }}</a>
{% endfor %}
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button @click="toggleMobile" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-dark-grey hover:text-text-alt focus:outline-none" aria-controls="mobile-menu" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg x-show="!openMobile" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<!-- Close Icon -->
<svg x-show="openMobile" class="h-5 w-5 text-red" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div
x-show="openMobile"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.outside="openMobile = false"
class="md:hidden"
id="mobile-menu"
>
<div class="flex flex-row items-center justify-between pt-2 pb-4">
{% for name, link in config.extra.nav %}
<a
:class="{ 'text-blue!': currentPage.startsWith('{{ link }}') }"
class="text-text-alt hover:text-blue px-3 py-2 rounded-md text-sm font-medium"
href="{{ link }}"
>{{ name | title }}</a>
{% endfor %}
</div>
</div>
</nav>

77
theme/templates/post.html Normal file
View File

@@ -0,0 +1,77 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="{{ metadata.title | title }} - {{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ metadata.permalink }}" />
<meta property="og:description" content="{{ metadata.description }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
{# TODO: add support for og:image using metadata #}
<link rel="canonical" href="{{ metadata.permalink }}" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}{{ metadata.title | title }}{% endblock title %}
{% block content %}
<div>
<h1>{{ metadata.title }}</h1>
</div>
<div class="flex flex-col md:flex-row">
<div class="flex flex-col md:flex-row flex-1 justify-between">
<div class="flex flex-col space-y-2">
{# Date, reading time #}
{% set created = metadata.created | date(format="%B %e, %Y") %}
{% set updated = metadata.updated | date(format="%B %e, %Y") %}
<span class="text-grey text-sm tabular-nums text-opacity-70 dark:text-opacity-100 space-x-1">
<time datetime="{{ metadata.created }}">{{ created }}</time>
{% if created != updated %}
(Last edit: <time datetime="{{ metadata.updated }}">{{ metadata.updated | date(format="%B %e, %Y") }}</time>)
{% endif %}
</span>
</div>
{% if metadata.categories | length > 0 %}
<div class="flex flex-wrap items-center max-w-[50%]">
<span class="font-semibold mr-px">Tags:&nbsp;</span>
{% for category in metadata.categories %}
<a
class="w-fit bg-grey hover:bg-magenta text-base hover:text-base! no-underline! hover:no-underline! text-sm mr-2 last:mr-0 px-1 rounded-sm"
href="/categories/{{ category }}"
>
{{ category | capitalize }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% if metadata.toc %}
<div x-data="toc" class="mt-4">
{% set toc_html = generate_toc(toc=metadata.toc, list_type="ol") %}
<button @click="toggle()" type="button" class="inline-flex items-center">
<span class="sr-only">Close Table of Contents</span>
<h3 class="mt-0! mr-1">
Table of Contents
</h3>
<span
id="toc-toggle-icon"
class="transition-all duration-300 ease-in-out text-2xl hover:text-blue ti ti-arrow-badge-right-filled"
></span>
</button>
<nav
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-show="open"
class="toc"
>
{{ toc_html | safe }}
</nav>
</div>
{% endif %}
<div id="content" class="mt-12 break-keep pt-10 border-t border-t-base-alt dark:border-t-[#39394b]">
{{ content | safe }}
</div>
{% endblock content %}

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block seo_tags %}
<meta property="og:title" content="{{ metadata.title | title }} - {{ config.title | title }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ metadata.permalink }}" />
<meta property="og:description" content="Posts on {{ config.title | title }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta property="og:locale" content="{{ config.language }}" />
<link rel="canonical" href="{{ metadata.permalink }}" />
<meta name="robots" content="index, follow" />
{% endblock seo_tags %}
{% block title %}{{ metadata.title | title }}{% endblock title %}
{% block content %}
<h1 class="text-center">Posts</h1>
<div class="mt-6 pt-6 border-t border-t-base-alt dark:border-t-[#39394b] break-keep">
{% for post in posts | filter(attribute="draft", value=false) | sort(attribute="created") | reverse %}
<div class="bg-surface p-4 mt-4 border-2 border-base-alt rounded-xl shadow-lg">
<div class="flex flex-col">
<h3 class="mt-0! text-text-alt"><a class="no-underline! hover:underline! hover:decoration-dashed" href="{{ post.permalink }}">{{ post.title }}</a></h3>
<time class="text-dark-grey dark:text-grey" datetime="{{ post.created }}">{{ post.created | date(format="%B %e, %Y") }}</time>
<span class="text-sm text-grey italic">{{ post.description }}</span>
</div>
<div class="flex flex-col">
<p>
{% if post.truncate_char is defined and post.truncate_char is matching("^nil$") %}
{% set truncate_char = "" %}
{% else %}
{% set truncate_char = "…" %}
{% endif %}
{{ post.raw | striptags | truncate(length=post.truncate | default(value=300), end=truncate_char) | safe }}
</p>
<a class="no-underline! text-dark-grey font-semibold" href="{{ post.permalink }}">Read more …</a>
</div>
</div>
{% endfor %}
</div>
{% endblock content %}

33
theme/templates/rss.xml Normal file
View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ config.title }}</title>
<link>{{ config.rootUrl | escape_xml | safe }}</link>
<description>{{ config.rss.description | default(value="Latest posts")}}</description>
<generator>Norgolith</generator>
<language>{{ config.language }}</language>
<lastBuildDate>{{ now | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
<ttl>{{ config.rss.ttl | default(value=60) }}</ttl>
<atom:link href="{{ config.rootUrl }}/rss.xml" rel="self" type="application/rss+xml" />
<image>
<url>{{ config.rootUrl | escape_xml | safe }}{{ config.rss.image | default(value="/assets/favicon.svg") }}</url>
<title>{{ config.title }}</title>
<link>{{ config.rootUrl | escape_xml | safe }}</link>
<width>144</width>
<height>144</height>
</image>
{% for post in posts | filter(attribute="draft", value=false) | sort(attribute="created") | reverse %}
<item>
<title>{{ post.title }}</title>
<link>{{ post.permalink | escape_xml | safe }}</link>
<guid>{{ post.permalink | escape_xml | safe }}</guid>
<description>{{ post.description }}</description>
<author>{{ post.authors | join(sep=", ") }}</author>
<pubDate>{{ post.created | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
{% if post.categories %}{% for category in post.categories %}<category>{{ category }}</category>{% endfor %}{% endif %}
</item>
{% endfor %}
</channel>
</rss>