dotfiles

My PC Dotfiles & Website
commit 0b1263ebb41a84c2896e34caefcd0fa3e7a78253
parent 6d10ee2f0971b99f3a4e173abf68f2b22e3e6f06
Author: maydayv7 <maydayv7@gmail.com>
Date:   Mon,  7 Sep 2026 01:12:55 +0530

feat(site): Performance + Accessibility

Diffstat:
Msite/.gitignore | 1+
Msite/README.md | 9+++++++++
Msite/content/blog/nixos/prelude.md | 4++--
Msite/content/blog/nixos/win11vm.md | 2+-
Msite/content/test.md | 6+++---
Msite/default.nix | 18++++++++++++------
Msite/git/default.nix | 3+++
Msite/git/index.js | 15++++++++-------
Msite/git/style.css | 3+++
Msite/git/wrangler.toml | 2+-
Msite/index.js | 29++++++++++++++++++++++++++---
Msite/sass/_comments.scss | 4++++
Msite/sass/_footer.scss | 4++++
Msite/sass/_header.scss | 8+++++++-
Msite/sass/_image.scss | 9++++++---
Msite/sass/_main.scss | 34+++++++++++++++++++++++++++++++++-
Msite/sass/_post.scss | 14+++++++-------
Msite/sass/_search.scss | 8+++++---
Msite/sass/fonts.scss | 4++++
Asite/scripts/optimize_fonts.py | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msite/shell.nix | 1+
Msite/static/js/clipboard.js | 5+++++
Msite/static/js/comments.js | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msite/static/js/footnote.js | 1+
Msite/static/js/load.js | 7++-----
Msite/static/js/mobile.js | 132++++++++++++++++++++++++++++++-------------------------------------------------
Msite/static/js/scroll.js | 6++++--
Msite/static/js/search.js | 285++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msite/static/js/theme.js | 178+++++++++++++++++++------------------------------------------------------------
Msite/templates/base.html | 25+++++++++++++++++++------
Msite/templates/index.html | 2+-
Msite/templates/macros/comments.html | 84++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msite/templates/macros/footer.html | 16++++++++--------
Msite/templates/macros/head.html | 37++++++++++++++++++++++++++++---------
Msite/templates/macros/header.html | 29++++++++++++++++++++++-------
Msite/templates/macros/javascript.html | 21+++++++++------------
Msite/templates/macros/logo.html | 2+-
Msite/templates/macros/menu.html | 16++++++++--------
Msite/templates/macros/pagination.html | 8+++++---
Msite/templates/macros/social.html | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msite/templates/macros/toc.html | 71+++++++++++++++--------------------------------------------------------
Msite/templates/page.html | 1-
Msite/templates/shortcodes/figure.html | 22++++++++++++++++++++--
Msite/templates/shortcodes/image.html | 16++++++++++++----
Msite/templates/shortcodes/url.html | 2+-
Msite/templates/shortcodes/youtube.html | 1+
Msite/zola.toml | 8++++----
47 files changed, 795 insertions(+), 589 deletions(-)

diff --git a/site/.gitignore b/site/.gitignore @@ -3,3 +3,4 @@ public node_modules static/processed_images static/giallo.css +__pycache__ diff --git a/site/README.md b/site/README.md @@ -20,6 +20,15 @@ zola -r site serve Then click [here](http://localhost:1111) To build the site, run `nix build .#website`. +The build also subsets webfonts, retaining text glyphs, ligatures, and Nerd icons referenced by generated HTML/CSS/JS. + +To produce the same optimized assets from the `site` development shell: + +```sh +zola build +python3 scripts/optimize_fonts.py public +``` + To override the URL, run: <pre><code>nix build --impure --expr 'with (builtins.getFlake (toString ../.)).legacyPackages.x86_64-linux; callPackage ./site { site = "<b><i>URL</i></b>"; }'</code></pre> #### Continuous Integration diff --git a/site/content/blog/nixos/prelude.md b/site/content/blog/nixos/prelude.md @@ -66,7 +66,7 @@ But why stop there? I grabbed [**Spicetify**](https://github.com/Gerg-L/spicetif You have no idea [^4]. -I don't "install" development tools. I have a [directory](https://github.com/maydayv7/dotfiles/tree/main/shells) that defines isolated environments for C++, Python, JavaScript and so on. +I don't "install" development tools. I have a [directory](https://github.com/maydayv7/dotfiles/tree/stable/shells) that defines isolated environments for C++, Python, JavaScript and so on. When I `cd` into a project, [Direnv](https://direnv.net/) and [**Lorri**](https://github.com/nix-community/lorri) kick in, and the compiler, linter, LSP and whatnot along with all of their configuration just _appear_ in my shell. When I leave, they vanish. Every little thing is specified in code, from backups to network settings and even complex editor workflows (thanks for your concern, but I very likey my trusty [VS Code](https://code.visualstudio.com/)). @@ -128,7 +128,7 @@ You can check out the list of links below to resourceful Nix documentation/tutor - NixOS [Package Search](https://search.nixos.org/) - [`nixpkgs`](https://github.com/NixOS/nixpkgs) Package Repository - [NUR](https://github.com/nix-community/NUR) Nix User Repository -- NixOS [Hardware Modules](https://github.com/nixos/hardware) +- NixOS [Hardware Modules](https://github.com/nixos/nixos-hardware) - Home Manager [Options](https://nix-community.github.io/home-manager/options.html) ### Other Sources diff --git a/site/content/blog/nixos/win11vm.md b/site/content/blog/nixos/win11vm.md @@ -55,7 +55,7 @@ boot.kernelParams = [ ]; ``` -**Case B - the host renders on the _same_ GPU you want to pass** (Eg. NVIDIA PRIME Sync, where the desktop holds `nvidia_drm`). A NixOS [specialisation](https://nixos.org/manual/nixos/stable/#sec-specialisation) can be used: your default boot keeps the GPU for the desktop, and a different boot entry binds it to `vfio-pci` instead: +**Case B - the host renders on the _same_ GPU you want to pass** (Eg. NVIDIA PRIME Sync, where the desktop holds `nvidia_drm`). A NixOS [specialisation](https://wiki.nixos.org/wiki/Specialisation) can be used: your default boot keeps the GPU for the desktop, and a different boot entry binds it to `vfio-pci` instead: ``` specialisation.vfio.configuration = { diff --git a/site/content/test.md b/site/content/test.md @@ -84,7 +84,7 @@ fn foo(arg: String) -> Result<u32, Io::Error> { ## Math -Supports Math Type-Setting using [$\KaTeX$](http://khan.github.io/KaTeX/) +Supports Math Type-Setting using [KaTeX](https://katex.org/) ``` $$ @@ -107,11 +107,11 @@ I'm not a fool to be fooled by a fool, you fool ;) # Media -Supports wide range of Multi-Media using [_shortcodes_](https://www.getzola.org/documentation/content/shortcodes/) +Supports wide range of Multi-Media using [_shortcodes_](https://www.getzola.org/documentation/) ## Image -{{ figure(src="https://picsum.photos/700", alt="Random", style="border-style: inset; border-radius: 5px;", caption="Random Image", caption_style="font-weight: bold;") }} +{{ figure(src="https://picsum.photos/700", alt="Random", width=700, height=700, style="border-style: inset; border-radius: 5px;", caption="Random Image", caption_style="font-weight: bold;") }} ## Video diff --git a/site/default.nix b/site/default.nix @@ -8,13 +8,19 @@ pkgs.stdenvNoCC.mkDerivation { version = "stable"; src = ./.; - buildInputs = [pkgs.zola]; + nativeBuildInputs = [ + pkgs.zola + (pkgs.python3.withPackages (python: [python.fonttools python.brotli])) + ]; installPhase = "cp -r public $out"; - buildPhase = "zola build ${ - if (site != null) - then "--base-url " + site - else "" - }"; + buildPhase = '' + zola build ${ + if (site != null) + then "--base-url " + site + else "" + } + python3 scripts/optimize_fonts.py public + ''; meta = { description = "My Personal Website"; diff --git a/site/git/default.nix b/site/git/default.nix @@ -32,6 +32,7 @@ in coreutils git stagit-fork + (python3.withPackages (python: [python.fonttools python.brotli])) ]; text = '' @@ -104,6 +105,8 @@ in echo "Generating Index..." stagit-index -n ${sitename} "$BUILD_ROOT"/*.git > "$OUTPUT_DIR/index.html" + python3 ${../scripts/optimize_fonts.py} "$OUTPUT_DIR" --source "$FONTS_SRC" + echo "Successfully built to $OUTPUT_DIR!" ''; } diff --git a/site/git/index.js b/site/git/index.js @@ -14,9 +14,9 @@ export default { const url = new URL(request.url); // Theme Switching - if (url.pathname.endsWith("style.css")) { + if (url.pathname === "/style.css") { const theme = getCookie(request.headers.get("Cookie"), "theme_color"); - if (theme && theme !== "red") { + if (["black", "blue"].includes(theme)) { const themeUrl = new URL(url); themeUrl.pathname = themeUrl.pathname.replace( "style.css", @@ -28,7 +28,7 @@ export default { if (themed.ok) { const headers = new Headers(themed.headers); headers.set("Vary", "Cookie"); - headers.set("Cache-Control", "no-store"); + headers.set("Cache-Control", "private, no-cache"); return new Response(themed.body, { status: themed.status, headers, @@ -39,10 +39,11 @@ export default { // Serve assets const response = await env.ASSETS.fetch(request); - if (response.status === 404) { - const fallbackUrl = new URL(request.url); - fallbackUrl.pathname = "/index.html"; - return env.ASSETS.fetch(new Request(fallbackUrl, request)); + if (url.pathname === "/style.css") { + const headers = new Headers(response.headers); + headers.set("Vary", "Cookie"); + headers.set("Cache-Control", "private, no-cache"); + return new Response(response.body, { status: response.status, headers }); } return response; }, diff --git a/site/git/style.css b/site/git/style.css @@ -1,18 +1,21 @@ /* Fonts */ @font-face { font-family: "JetBrainsMono Nerd Font"; + font-display: swap; src: url("/fonts/JetBrainsMono-Nerd-Regular.woff2") format("woff2"); font-weight: normal; font-style: normal; } @font-face { font-family: "JetBrainsMono Nerd Font"; + font-display: swap; src: url("/fonts/JetBrainsMono-Nerd-Bold.woff2") format("woff2"); font-weight: bold; font-style: normal; } @font-face { font-family: "JetBrainsMono Nerd Font"; + font-display: swap; src: url("/fonts/JetBrainsMono-Nerd-Italic.woff2") format("woff2"); font-weight: normal; font-style: italic; diff --git a/site/git/wrangler.toml b/site/git/wrangler.toml @@ -5,7 +5,7 @@ compatibility_date = "2025-12-15" [assets] binding = "ASSETS" directory = "./public" -run_worker_first = true +run_worker_first = ["/style.css"] [env.production] routes = [ diff --git a/site/index.js b/site/index.js @@ -1,16 +1,39 @@ export default { async fetch(request, env) { + const url = new URL(request.url); + if (url.hostname === "www.maydayv7.cc") { + url.hostname = "maydayv7.cc"; + url.protocol = "https:"; + return Response.redirect(url, 308); + } // Curl const userAgent = request.headers.get("User-Agent") || ""; if (userAgent.toLowerCase().startsWith("curl")) { - const url = new URL(request.url); if (url.pathname === "/") { url.pathname = "/curl.txt"; - return env.ASSETS.fetch(new Request(url.toString())); + const response = await env.ASSETS.fetch(new Request(url, request)); + const headers = new Headers(response.headers); + headers.append("Vary", "User-Agent"); + return new Response(response.body, { + status: response.status, + headers, + }); } } // Serve Website - return env.ASSETS.fetch(request); + const response = await env.ASSETS.fetch(request); + if ( + response.ok && + /^\/processed_images\/[^/]+\.[a-f0-9]{16}\.[a-z0-9]+$/.test(url.pathname) + ) { + const headers = new Headers(response.headers); + headers.set("Cache-Control", "public, max-age=31536000, immutable"); + return new Response(response.body, { status: response.status, headers }); + } + if (url.pathname !== "/") return response; + const headers = new Headers(response.headers); + headers.append("Vary", "User-Agent"); + return new Response(response.body, { status: response.status, headers }); }, }; diff --git a/site/sass/_comments.scss b/site/sass/_comments.scss @@ -7,6 +7,10 @@ padding: 0 18px; transition: max-height 0.2s ease-out; + &-content { + display: flow-root; + } + &-collapsible { background-color: var(--background); border: 2px solid var(--accent-alpha-70); diff --git a/site/sass/_footer.scss b/site/sass/_footer.scss @@ -112,6 +112,10 @@ a.backlink { p { display: inline; color: rgba(255, 255, 255, 0.7); + + &:first-of-type { + margin-inline-start: 1ch; + } } &-label { diff --git a/site/sass/_header.scss b/site/sass/_header.scss @@ -72,7 +72,7 @@ } &-mobile { - @media (min-width: $phone-max-width) { + @media (min-width: ($phone-max-width + 1px)) { display: none; } @@ -144,6 +144,9 @@ top: 35px; &-trigger { + background: none; + border: none; + font: inherit; color: var(--accent-secondary); cursor: pointer; margin: 0; @@ -160,6 +163,9 @@ } &-trigger { + background: none; + font: inherit; + cursor: pointer; display: none; @media (max-width: $phone-max-width) { diff --git a/site/sass/_image.scss b/site/sass/_image.scss @@ -3,6 +3,7 @@ img { display: block; max-width: 100%; + height: auto; &.center { margin-left: auto; @@ -19,15 +20,17 @@ img { } figure { - display: table; - min-width: 300px; + display: block; + width: fit-content; + max-width: 100%; + min-width: min(300px, 100%); text-align: center; figcaption { background: var(--background); caption-side: bottom; color: var(--accent); - display: table-caption; + display: block; font-size: 14px; margin-top: 5px; padding: 5px 10px; diff --git a/site/sass/_main.scss b/site/sass/_main.scss @@ -67,6 +67,38 @@ body { .content { display: flex; + min-width: 0; +} + +.skip-link { + position: absolute; + left: 20px; + top: -100px; + padding: 10px; + background: var(--background); + z-index: 200; + + &:focus { + top: 10px; + } +} + +:focus-visible { + outline: 2px dotted var(--accent-secondary); + outline-offset: 3px; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition: none !important; + animation: none !important; + } } h1 { @@ -100,7 +132,7 @@ h6 { } .zola-anchor { - visibility: hidden; + opacity: 0; } } diff --git a/site/sass/_post.scss b/site/sass/_post.scss @@ -136,6 +136,8 @@ blockquote { .post { text-align: left; + min-width: 0; + overflow-wrap: anywhere; width: 100%; &:not(:last-of-type) { @@ -184,6 +186,7 @@ blockquote { } &-sub { + font-size: 1.2rem; --border: 2.5px dotted var(--accent-secondary) !important; color: var(--accent-secondary) !important; padding-top: 15px !important; @@ -293,18 +296,15 @@ blockquote { margin-left: 0.5rem; text-decoration: none; vertical-align: 1%; - visibility: hidden; + opacity: 0; } - &:hover { + &:hover, + &:focus-within { .zola-anchor { - visibility: visible; + opacity: 1; } } - - @media (max-width: $phone-max-width) { - visibility: visible; - } } } } diff --git a/site/sass/_search.scss b/site/sass/_search.scss @@ -3,7 +3,7 @@ .search { &-container { display: inline-block; - left: 666px; + align-self: flex-end; position: relative; top: -32px; transition: all 0.5s ease-out; @@ -20,7 +20,7 @@ } @media (max-width: $tablet-max-width) { - left: 300px; + align-self: flex-end; width: 300px; } @@ -58,7 +58,7 @@ &__number { font-size: small; text-align: end; - margin: -15px 5px -25px; + margin: 0 0 1rem; } &__item { @@ -82,6 +82,8 @@ &__items { list-style: none; + margin: 0; + padding: 0; } li { diff --git a/site/sass/fonts.scss b/site/sass/fonts.scss @@ -2,6 +2,7 @@ @font-face { font-family: "JetBrainsMono"; + font-display: swap; src: url("fonts/JetBrainsMono-Nerd-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; @@ -9,6 +10,7 @@ @font-face { font-family: "JetBrainsMono"; + font-display: swap; src: url("fonts/JetBrainsMono-Nerd-Bold.woff2") format("woff2"); font-weight: 700; font-style: normal; @@ -16,6 +18,7 @@ @font-face { font-family: "JetBrainsMono"; + font-display: swap; src: url("fonts/JetBrainsMono-Nerd-Italic.woff2") format("woff2"); font-weight: 400; font-style: italic; @@ -23,6 +26,7 @@ @font-face { font-family: "JetBrainsMono"; + font-display: swap; src: url("fonts/JetBrainsMono-Nerd-BoldItalic.woff2") format("woff2"); font-weight: 700; font-style: italic; diff --git a/site/scripts/optimize_fonts.py b/site/scripts/optimize_fonts.py @@ -0,0 +1,68 @@ +"""Subset generated webfonts, retaining all text glyphs and referenced Nerd icons""" + +import argparse +import html +import re +from pathlib import Path + +from fontTools import subset +from fontTools.ttLib import TTFont + + +def private_use(codepoint): + return ( + 0xE000 <= codepoint <= 0xF8FF + or 0xF0000 <= codepoint <= 0xFFFFD + or 0x100000 <= codepoint <= 0x10FFFD + ) + + +def optimize(output, source): + used = set() + for path in sorted(output.rglob("*")): + if path.suffix not in {".html", ".css", ".js", ".json", ".txt"}: + continue + text = html.unescape(path.read_text(encoding="utf-8")) + used.update(map(ord, text)) + # CSS content escapes and JavaScript/JSON Unicode escapes + used.update( + int(value, 16) for value in re.findall(r"\\([0-9a-fA-F]{1,6})", text) + ) + used.update( + int(value, 16) for value in re.findall(r"\\u\{?([0-9a-fA-F]{4,6})", text) + ) + + fonts = sorted(source.glob("JetBrainsMono-Nerd-*.woff2")) + if not fonts: + raise FileNotFoundError(f"No source webfonts in {source}") + for path in fonts: + font = TTFont(path, recalcTimestamp=False) + required = { + codepoint for codepoint in font.getBestCmap() if not private_use(codepoint) + } | used + options = subset.Options() + options.layout_features = ["*"] + options.name_IDs = ["*"] + options.name_legacy = True + options.name_languages = ["*"] + options.drop_tables += ["FFTM", "PfEd"] # FontForge editor metadata + subsetter = subset.Subsetter(options=options) + subsetter.populate(unicodes=required) + subsetter.subset(font) + target = output / "fonts" / path.name + target.parent.mkdir(parents=True, exist_ok=True) + font.save(target) + font.close() + print(f"{path.name}: {path.stat().st_size:,} → {target.stat().st_size:,} bytes") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("output", type=Path) + parser.add_argument("--source", type=Path, default=Path("static/fonts")) + args = parser.parse_args() + if not args.output.is_dir(): + parser.error("output must be an existing generated site directory") + if (args.output / "fonts").resolve() == args.source.resolve(): + parser.error("output fonts must be separate from the source fonts") + optimize(args.output, args.source) diff --git a/site/shell.nix b/site/shell.nix @@ -5,6 +5,7 @@ pkgs.mkShell { git zola wrangler + (python3.withPackages (python: [python.fonttools python.brotli])) ]; shellHook = ''echo "## Website Builder Shell ##"''; } diff --git a/site/static/js/clipboard.js b/site/static/js/clipboard.js @@ -4,6 +4,10 @@ document.querySelectorAll("pre > code").forEach(function (codeBlock) { button.type = "button"; button.innerText = " Copy"; button.addEventListener("click", function () { + if (!navigator.clipboard) { + button.innerText = "Select code to copy"; + return; + } navigator.clipboard.writeText(codeBlock.innerText).then( function () { button.blur(); @@ -19,6 +23,7 @@ document.querySelectorAll("pre > code").forEach(function (codeBlock) { }); var pre = codeBlock.parentNode; + pre.tabIndex = 0; var block = pre.parentNode.classList.contains("highlight") ? pre.parentNode : pre; diff --git a/site/static/js/comments.js b/site/static/js/comments.js @@ -1,21 +1,63 @@ -var buttons = document.getElementsByClassName("comments-collapsible"); -for (var i = 0; i < buttons.length; i++) { - buttons[i].addEventListener("click", function () { - this.classList.toggle("active"); - var content = this.nextElementSibling; - if (content.style.maxHeight) { - // Closing - content.style.maxHeight = content.scrollHeight + "px"; +document.querySelectorAll(".comments-collapsible").forEach((button) => { + const content = document.getElementById(button.getAttribute("aria-controls")); + const thread = content.querySelector(".comments-content"); + const loader = thread.querySelector("script[data-src]"); + const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)"); + let loaded = false; + let open = false; + let hideTimer; + + const resize = new ResizeObserver(() => { + if (open) content.style.maxHeight = `${thread.scrollHeight}px`; + }); + resize.observe(thread); + + button.addEventListener("click", () => { + open = !open; + clearTimeout(hideTimer); + button.setAttribute("aria-expanded", String(open)); + button.classList.toggle("active", open); + button.textContent = open ? "Hide Comments" : "Show Comments"; + content.inert = !open; + + if (open) { + content.hidden = false; void content.offsetHeight; - content.style.maxHeight = null; - this.innerHTML = "Show Comments"; + content.style.maxHeight = `${thread.scrollHeight}px`; } else { - // Opening - content.style.maxHeight = content.scrollHeight + "px"; - this.innerHTML = "Hide Comments"; - setTimeout(() => { - if (content.style.maxHeight) content.style.maxHeight = "none"; - }, 200); + content.style.maxHeight = "0px"; + if (reducedMotion.matches) content.hidden = true; + else + hideTimer = setTimeout(() => { + content.hidden = true; + }, 220); + } + + if (!open || loaded) return; + loaded = true; + thread.querySelector('[role="status"]')?.remove(); + if (thread.id === "disqus_thread") { + window.disqus_config = function () { + this.page.url = content.dataset.url; + this.page.identifier = content.dataset.url; + }; + } + const script = document.createElement("script"); + for (const attribute of loader.attributes) { + if (!["type", "data-src"].includes(attribute.name)) + script.setAttribute(attribute.name, attribute.value); } + script.src = loader.dataset.src; + script.async = true; + script.onerror = () => { + loaded = false; + script.remove(); + const status = document.createElement("p"); + status.setAttribute("role", "status"); + status.textContent = + "Comments could not load. Close and reopen to retry."; + thread.append(status); + }; + thread.append(script); }); -} +}); diff --git a/site/static/js/footnote.js b/site/static/js/footnote.js @@ -14,6 +14,7 @@ window.addEventListener("load", function () { backlink.href = "#" + refhash; backlink.className = "backlink"; backlink.innerText = "↩"; + backlink.setAttribute("aria-label", "Back to footnote reference " + hash); const paras = footnote.children; const lastPara = paras[paras.length - 1]; lastPara.appendChild(backlink); diff --git a/site/static/js/load.js b/site/static/js/load.js @@ -1,5 +1,2 @@ -var loading = document.querySelector(".loading"); - -window.addEventListener("load", function () { - loading.style.display = "none"; -}); +// The HTML is ready when this deferred script runs +document.querySelector(".loading")?.remove(); diff --git a/site/static/js/mobile.js b/site/static/js/mobile.js @@ -1,85 +1,53 @@ -!(function () { - var container = document.querySelector(".container"), - menu_mobile_trigger = document.querySelector(".menu-trigger"), - menu_mobile = document.querySelector(".menu-mobile"), - menu_desktop = - (document.querySelector(".menu-inner-desktop"), - document.querySelector(".menu-inner-list-more-trigger")), - menu_more = document.querySelector(".menu-inner-list-more"), - page_form = document.querySelector(".pagination__form"), - phone_width = getComputedStyle(document.body).getPropertyValue( - "--phoneWidth", - ), - is_phone = function () { - return window.matchMedia(phone_width).matches; - }, - was_phone = is_phone(), - toggle_mobile_menu = function () { - menu_mobile && menu_mobile.classList.toggle("hidden"); - }, - hide_mobile_menu = function () { - menu_mobile && menu_mobile.classList.add("hidden"); - }, - toggle_menu_more = function () { - menu_more && menu_more.classList.toggle("hidden"); - }, - hide_menu_more = function () { - menu_more && menu_more.classList.add("hidden"); - }, - toggle_vis = function () { - (menu_mobile && is_phone() && menu_mobile.classList.add("hidden"), - menu_more && !is_phone() && menu_more.classList.add("hidden")); - }; +(() => { + const phone = matchMedia( + getComputedStyle(document.documentElement) + .getPropertyValue("--phoneWidth") + .trim(), + ); + const menus = [ + [ + document.querySelector(".menu-trigger"), + document.getElementById("mobile-menu"), + ], + [ + document.querySelector(".menu-inner-list-more-trigger"), + document.getElementById("more-menu"), + ], + ].filter(([button, panel]) => button && panel); - (toggle_vis(), - page_form && - (page_form.onsubmit = function (event) { - var loc; - if (this.page.value == 1) { - loc = this.action.slice(0, -5); - } else { - loc = this.action += this.page.value + "/"; - } - event.preventDefault(); - window.location.href = loc; - }), - menu_mobile && - menu_mobile.addEventListener("click", function (event) { - return event.stopPropagation(); - }), - menu_more && - menu_more.addEventListener("click", function (event) { - return event.stopPropagation(); - }), - document.body.addEventListener("click", function () { - if (is_phone() || !menu_more || menu_more.classList.contains("hidden")) { - is_phone() && hide_mobile_menu(); - } else { - hide_menu_more(); + function setOpen(button, panel, open) { + panel.classList.toggle("hidden", !open); + button.setAttribute("aria-expanded", String(open)); + } + for (const [button, panel] of menus) { + setOpen(button, panel, false); + button.addEventListener("click", () => + setOpen(button, panel, button.getAttribute("aria-expanded") !== "true"), + ); + document.addEventListener("click", (event) => { + if (!button.contains(event.target) && !panel.contains(event.target)) + setOpen(button, panel, false); + }); + document.addEventListener("keydown", (event) => { + if ( + event.key === "Escape" && + button.getAttribute("aria-expanded") === "true" + ) { + setOpen(button, panel, false); + button.focus(); } - }), - window.addEventListener("resize", toggle_vis), - menu_mobile_trigger && - (menu_mobile_trigger.addEventListener("click", function (event) { - (event.stopPropagation(), toggle_mobile_menu()); - }), - menu_mobile_trigger.addEventListener("keyup", function (event) { - (event.stopPropagation(), - event.code === "Enter" && toggle_mobile_menu()); - })), - menu_desktop && - (menu_desktop.addEventListener("click", function (event) { - (event.stopPropagation(), - toggle_menu_more(), - menu_more.getBoundingClientRect().right > - container.getBoundingClientRect().right && - ((menu_more.style.left = "auto"), (menu_more.style.right = 0))); - }), - menu_desktop.addEventListener("keyup", function (event) { - (event.stopPropagation(), - event.code === "Enter" && toggle_menu_more(), - menu_more.getBoundingClientRect().right > - container.getBoundingClientRect().right && - ((menu_more.style.left = "auto"), (menu_more.style.right = 0))); - }))); + }); + phone.addEventListener("change", () => setOpen(button, panel, false)); + } + const form = document.querySelector(".pagination__form"); + if (form) + form.addEventListener("submit", (event) => { + event.preventDefault(); + if (!form.reportValidity()) return; + const page = Number(form.elements.page.value); + location.href = + page === 1 + ? form.action.replace(/page\/$/, "") + : `${form.action}${page}/`; + }); })(); diff --git a/site/static/js/scroll.js b/site/static/js/scroll.js @@ -16,10 +16,12 @@ if (scrollButton) { function scrollBack() { rootElement.scrollTo({ top: 0, - behavior: "smooth", + behavior: matchMedia("(prefers-reduced-motion: reduce)").matches + ? "instant" + : "smooth", }); } scrollButton.addEventListener("click", scrollBack); - document.addEventListener("scroll", handleScroll); + document.addEventListener("scroll", handleScroll, { passive: true }); } diff --git a/site/static/js/search.js b/site/static/js/search.js @@ -1,144 +1,165 @@ -function debounce(func, wait) { - var timeout; - - return function () { - var context = this; - var args = arguments; - clearTimeout(timeout); +(() => { + const input = document.getElementById("search"); + if (!input) return; + const container = input.closest(".search-container"); + const panel = document.getElementById("search-results"); + const items = panel.querySelector(".search-results__items"); + const status = panel.querySelector('[role="status"]'); + let engine; + let library; + let timer; + let revision = 0; + + function loadEngine() { + if (engine) return engine; + if (!library) { + library = new Promise((resolve, reject) => { + if (window.Fuse) return resolve(); + const script = document.createElement("script"); + script.src = input.dataset.library; + script.onload = resolve; + script.onerror = () => { + script.remove(); + library = null; + reject(new Error("Search library unavailable")); + }; + document.head.appendChild(script); + }); + } + engine = Promise.all([ + library, + fetch(input.dataset.index).then((response) => { + if (!response.ok) throw new Error("Search index unavailable"); + return response.json(); + }), + ]) + .then( + ([, index]) => + new Fuse(index, { + keys: [ + { name: "title", weight: 2 }, + { name: "body", weight: 1 }, + ], + minMatchCharLength: 2, + threshold: 0.3, + ignoreLocation: true, + }), + ) + .catch((error) => { + engine = null; + throw error; + }); + return engine; + } - timeout = setTimeout(function () { - timeout = null; - func.apply(context, args); - }, wait); - }; -} + function show(visible) { + panel.style.display = visible ? "block" : "none"; + } -function makeTeaser(body, terms) { - var TEASER_MAX_CHARS = 200; - var lowerBody = body.toLowerCase(); - var lowerTerms = terms.map(function (t) { - return t.toLowerCase(); - }); + function close() { + revision++; + clearTimeout(timer); + panel.removeAttribute("aria-busy"); + show(false); + } - // Find the first occurrence of any term - var firstIndex = -1; - for (var i = 0; i < lowerTerms.length; i++) { - var idx = lowerBody.indexOf(lowerTerms[i]); - if (idx !== -1 && (firstIndex === -1 || idx < firstIndex)) { - firstIndex = idx; + function teaser(body, terms) { + const lower = body.toLowerCase(); + const matches = terms + .map((term) => lower.indexOf(term.toLowerCase())) + .filter((index) => index >= 0); + const start = Math.max(0, (matches.length ? Math.min(...matches) : 0) - 40); + const text = + (start ? "…" : "") + + body.slice(start, start + 200) + + (body.length > start + 200 ? "…" : ""); + const paragraph = document.createElement("div"); + const pattern = new RegExp( + terms + .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("|"), + "gi", + ); + let position = 0; + for (const match of text.matchAll(pattern)) { + paragraph.append(text.slice(position, match.index)); + const bold = document.createElement("b"); + bold.textContent = match[0]; + paragraph.append(bold); + position = match.index + match[0].length; } + paragraph.append(text.slice(position)); + return paragraph; } - // Start a bit before the first match - var start = Math.max(0, firstIndex - 40); - var end = Math.min(body.length, start + TEASER_MAX_CHARS); - var teaser = - (start > 0 ? "…" : "") + - body.substring(start, end) + - (end < body.length ? "…" : ""); - - // Bold the matching terms - for (var i = 0; i < lowerTerms.length; i++) { - if (lowerTerms[i].length > 0) { - var regex = new RegExp( - "(" + lowerTerms[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", - "gi", - ); - teaser = teaser.replace(regex, "<b>$1</b>"); + async function search() { + const term = input.value.trim(); + const request = ++revision; + items.replaceChildren(); + if (!term) return show(false); + show(true); + status.textContent = "Searching…"; + panel.setAttribute("aria-busy", "true"); + try { + const fuse = await loadEngine(); + if (request !== revision) return; + const results = fuse.search(term); + status.textContent = results.length + ? `${results.length} ${results.length === 1 ? "result" : "results"}${results.length > 10 ? " (showing 10)" : ""}` + : "No results found."; + const fragment = document.createDocumentFragment(); + for (const { item } of results.slice(0, 10)) { + const url = new URL(item.url, location.href); + if (url.origin !== location.origin) continue; + const row = document.createElement("li"); + row.className = "search-results__item"; + const title = document.createElement("h2"); + title.className = "title"; + const link = document.createElement("a"); + link.href = url.href; + link.textContent = item.title; + title.append(link); + row.append( + title, + teaser((item.body || "").replace(/§/g, ""), term.split(/\s+/)), + ); + const more = document.createElement("a"); + more.href = url.href; + const label = document.createElement("i"); + label.textContent = "Read More →"; + more.append(label); + row.append(more); + fragment.append(row); + } + items.append(fragment); + } catch { + if (request === revision) + status.textContent = "Search could not load. Try typing again."; + } finally { + if (request === revision) panel.removeAttribute("aria-busy"); } } - return teaser; -} - -function formatSearchResultItem(item, terms) { - var body = (item.item.body || "").replace(/§/g, ""); - return ( - '<div class="search-results__item">' + - `<h1 class='title'>` + - `<a href="${item.item.url}">${item.item.title}</a>` + - `</h1>` + - `<div>${makeTeaser(body, terms)}</div>` + - `<a href='${item.item.url}'>` + - `<i>Read More <span class="icon is-small">→</span></i>` + - `</a>` + - "</div>" - ); -} - -function initSearch() { - var $searchInput = document.getElementById("search"); - var $searchResults = document.querySelector(".search-results"); - var $searchResultsItems = document.querySelector(".search-results__items"); - var MAX_ITEMS = 10; - var currentTerm = ""; - - var fuse = new Fuse(window.searchIndex, { - keys: [ - { name: "title", weight: 2 }, - { name: "body", weight: 1 }, - ], - includeMatches: true, - minMatchCharLength: 2, - threshold: 0.3, - ignoreLocation: true, + input.addEventListener("focus", () => { + loadEngine().catch(() => {}); + if (input.value.trim()) search(); }); - - $searchInput.addEventListener( - "keyup", - debounce(function () { - var term = $searchInput.value.trim(); - if (term === currentTerm) { - return; - } - - $searchResults.style.display = term === "" ? "none" : "block"; - $searchResultsItems.innerHTML = ""; - if (term === "") { - currentTerm = ""; - return; - } - - var results = fuse.search(term); - if (results.length === 0) { - $searchResults.style.display = "none"; - currentTerm = term; - return; - } - - var number = document.createElement("p"); - number.innerHTML = - '<div class="search-results__number">' + - `<b>${results.length}</b> ${ - results.length === 1 ? "result" : "results" - }` + - "</div>"; - $searchResultsItems.appendChild(number); - - currentTerm = term; - for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) { - var item = document.createElement("li"); - item.innerHTML = formatSearchResultItem(results[i], term.split(" ")); - $searchResultsItems.appendChild(item); - } - }, 150), - ); - - window.addEventListener("click", function (e) { - if ( - $searchResults.style.display == "block" && - !$searchResults.contains(e.target) - ) { - $searchResults.style.display = "none"; + input.addEventListener("input", () => { + revision++; + clearTimeout(timer); + if (!input.value.trim()) return close(); + timer = setTimeout(search, 150); + }); + container.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + input.focus(); + close(); } }); -} - -if ( - document.readyState === "complete" || - (document.readyState !== "loading" && !document.documentElement.doScroll) -) { - initSearch(); -} else { - document.addEventListener("DOMContentLoaded", initSearch); -} + document.addEventListener("click", (event) => { + if (!container.contains(event.target)) close(); + }); + container.addEventListener("focusout", (event) => { + if (!container.contains(event.relatedTarget)) close(); + }); +})(); diff --git a/site/static/js/theme.js b/site/static/js/theme.js @@ -1,138 +1,44 @@ -(function (switch_theme) { - const COOKIE_NAME = "theme_color"; - function getCookie(name) { - const v = document.cookie.match("(^|;) ?" + name + "=([^;]*)(;|$)"); - return v ? v[2] : null; - } - - function setCookie(name, value) { - document.cookie = `${name}=${value}; path=/; max-age=31536000; SameSite=Lax`; - } - - function saveTheme(themeName) { - setCookie(COOKIE_NAME, themeName); - } - - function clearTheme() { - document.cookie = `${COOKIE_NAME}=; path=/; max-age=0; SameSite=Lax`; - } - - const THEME = getCookie(COOKIE_NAME); - const STOP_BLINK_CSS_ID = "stop-blink"; - const STYLESHEET_CLASSNAME = "stylesheet"; - let previousLink = null; - let baseUrl = ""; - - const onLinkLoad = (event) => { - let link = event.currentTarget; - link.removeEventListener("load", onLinkLoad); - link.removeEventListener("error", onLinkError); - removeStylesheets(); - link.className += STYLESHEET_CLASSNAME; - previousLink = null; - showBody(); - }; - - const onLinkError = (event) => { - let link = event.currentTarget; - link.removeEventListener("load", onLinkLoad); - link.removeEventListener("error", onLinkError); - clearTheme(); - updateThemeSelect(link.id, false); - link.remove(); - if (previousLink) { - document.getElementsByTagName("head")[0].appendChild(previousLink); - } - let sheets = document.querySelectorAll(`.${STYLESHEET_CLASSNAME}`); - if (sheets.length > 0) updateThemeSelect(sheets[0].id, true); - showBody(); - }; - - function changeTheme(themeName, firstLoad) { - var fileref = document.createElement("link"); - fileref.rel = "stylesheet"; - fileref.type = "text/css"; - fileref.href = `${baseUrl}${ - baseUrl.slice(-1) !== "/" ? "/" : "" - }color/${themeName}.css`; - fileref.id = themeName; - - let link = document.getElementsByTagName("head")[0].appendChild(fileref); - link.addEventListener("load", onLinkLoad); - link.addEventListener("error", onLinkError); - - if (firstLoad) { - let sheets = document.querySelectorAll(`.${STYLESHEET_CLASSNAME}`); - if (sheets.length > 0) previousLink = sheets[0]; - } - - saveTheme(themeName); - } - - function removeStylesheets() { - document.querySelectorAll(`.${STYLESHEET_CLASSNAME}`).forEach((el) => { - if (el !== previousLink) el.remove(); - }); - } - - function hideBody() { - let head = document.getElementsByTagName("head")[0]; - let style = document.createElement("style"); - let css = "body{visibility:hidden;}"; - - style.id = STOP_BLINK_CSS_ID; - style.setAttribute("type", "text/css"); - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); +(() => { + const selector = document.getElementById("theme-select"); + let current = document.getElementById("site-theme"); + if (!selector || !current) return; + const allowed = new Set([...selector.options].map((option) => option.value)); + let pending; + + function changeTheme(theme) { + if (!allowed.has(theme)) return; + if (pending) { + pending.remove(); + pending = null; } - head.appendChild(style); - } - - function showBody() { - let css = document.getElementById(STOP_BLINK_CSS_ID); - if (css) css.remove(); - } - - function updateThemeSelect(theme, setSelected) { - let elements = document.querySelectorAll("#theme-select>option"); - if (elements.length) { - elements.forEach((element) => { - if (element.value === theme) { - if (setSelected) { - element.selected = "selected"; - } else { - element.remove(); - } - } - }); - } else { - window.addEventListener("load", () => { - updateThemeSelect(theme, setSelected); - }); + if (theme === current.dataset.theme) { + selector.value = theme; + return; } - } - - switch_theme.init = function (url) { - baseUrl = url; - if (THEME) { - setCookie(COOKIE_NAME, THEME); - if (!document.getElementById(THEME)) { - hideBody(); - changeTheme(THEME, true); - updateThemeSelect(THEME, true); - } - } - - window.addEventListener("load", () => { - let selector = document.getElementById("theme-select"); - if (selector) { - selector.onchange = function () { - changeTheme(this.value); - }; - } - }); - }; -})((window.switch_theme = window.switch_theme || {})); + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = new URL(`${theme}.css`, current.href).href; + link.dataset.theme = theme; + pending = link; + link.onload = () => { + if (pending !== link) return; + current.replaceWith(link); + link.id = "site-theme"; + current = link; + pending = null; + selector.value = theme; + document.cookie = `theme_color=${theme}; path=/; max-age=31536000; SameSite=Lax`; + }; + link.onerror = () => { + if (pending !== link) return; + link.remove(); + pending = null; + selector.value = current.dataset.theme; + }; + document.head.appendChild(link); + } + + selector.addEventListener("change", () => changeTheme(selector.value)); + const saved = document.cookie.match(/(?:^|;\s*)theme_color=([^;]*)/); + if (saved && allowed.has(saved[1])) changeTheme(saved[1]); +})(); diff --git a/site/templates/base.html b/site/templates/base.html @@ -20,13 +20,26 @@ squiggle -%} {% import "macros/edit.html" as edit -%} {% import head::styling(config=config) }} {%- endblock css -%} {%- block font -%} {{ head::font(config=config) }} {%- endblock font -%} {%- block rss -%} {%- endblock rss -%} {%- block favicon -%} {{ head::favicon(config=config) }} - {%- endblock favicon -%} {%- block math -%} {%- endblock math -%} + {%- endblock favicon -%} {%- block math -%} {%- endblock math -%} {% block + analytics -%} {{ analytics::google_analytics(config=config) }} {%- endblock + analytics %} + <noscript + ><style> + .js-only, + .header .menu-trigger, + .header .menu-mobile { + display: none !important; + } + .header .menu-desktop, + .header .menu-inner-desktop { + display: flex; + } + </style></noscript + > </head> - {% block analytics -%} {{ analytics::google_analytics(config=config) }} {%- - endblock analytics %} - <body class=""> + <a class="skip-link" href="#main-content">Skip to content</a> {%- if config.extra.full_width -%} {%- set container = "container full" -%} {%- elif config.extra.center -%} {%- set container = "container center" -%} {%- else -%} {%- set container = "container" -%} {%- endif -%} {%- if not @@ -36,9 +49,9 @@ squiggle -%} {% import "macros/edit.html" as edit -%} {% import {%- block header -%} {{ header::header(config=config, current_url=current_url) }} {%- endblock header -%} - <div class="content"> + <main class="content" id="main-content" tabindex="-1"> {%- block main -%} {{ lists::list_pages() }} {%- endblock main -%} - </div> + </main> {%- block pagination -%} {{ pagination::paginate(pagelink = current_url) }} {%- endblock pagination -%} {%- block footer -%} diff --git a/site/templates/index.html b/site/templates/index.html @@ -7,7 +7,7 @@ <div class="post-content">{{ section.content | safe }}</div> </article> - <h3 class="post-title post-title-sub">Recent Posts</h3> + <h2 class="post-title post-title-sub">Recent Posts</h2> {% set section = get_section(path="blog/_index.md") %} <ul class="post-list"> {% for page in section.pages -%} {%- if loop.index0 < 5 -%} diff --git a/site/templates/macros/comments.html b/site/templates/macros/comments.html @@ -1,44 +1,44 @@ -{% macro comments() %} {% if config.extra.disqus.enabled is defined and -config.extra.disqus.enabled == true %} {{ comments::disqus() }} {{ -js::comments() }} {% endif %} {% if config.extra.giscus.enabled is defined and -config.extra.giscus.enabled == true %} {{ comments::giscus() }} {{ -js::comments() }} {% endif %} {% endmacro comments %} {% macro disqus() %} -<div class="comments" id="disqus_thread"></div> -<script> - var disqus_config = function () { - this.page.url = "{{ page.permalink | safe }}"; - this.page.identifier = "{{ page.permalink | safe }}"; - }; - - (function () { - var d = document, - s = d.createElement("script"); - s.src = "https://{{ config.extra.disqus.short_name }}.disqus.com/embed.js"; - s.setAttribute("data-timestamp", +new Date()); - (d.head || d.body).appendChild(s); - })(); -</script> -<noscript - >Please enable JavaScript to view the - <a href="https://disqus.com/?ref_noscript" - >Comments powered by Disqus</a - ></noscript +{% macro comments() %} {% if config.extra.disqus.enabled or +config.extra.giscus.enabled %} {% if config.extra.disqus.enabled %}{% set thread += "disqus_thread" %}{% else %}{% set thread = "giscus_thread" %}{% endif %} +<button + type="button" + class="comments-collapsible js-only" + aria-expanded="false" + aria-controls="comments-{{ thread }}" > -{% endmacro disqus %} {% macro giscus() %} -<div class="comments" id="giscus_thread"> - <script - src="https://giscus.app/client.js" - data-repo="{{ config.extra.giscus.repo }}" - data-repo-id="{{ config.extra.giscus.repo_id }}" - data-category="{{ config.extra.giscus.category }}" - data-category-id="{{ config.extra.giscus.category_id }}" - data-mapping="pathname" - data-reactions-enabled="1" - data-emit-metadata="0" - data-input-position="top" - data-theme="transparent_dark" - data-lang="en" - crossorigin="anonymous" - async></script> + Show Comments +</button> +<div + class="comments" + id="comments-{{ thread }}" + data-url="{{ page.permalink }}" + hidden + inert +> + <div class="comments-content" id="{{ thread }}"> + {% if config.extra.disqus.enabled %} + <script + type="text/plain" + data-src="https://{{ config.extra.disqus.short_name }}.disqus.com/embed.js" + ></script> + {% else %} + <script + type="text/plain" + data-src="https://giscus.app/client.js" + data-repo="{{ config.extra.giscus.repo }}" + data-repo-id="{{ config.extra.giscus.repo_id }}" + data-category="{{ config.extra.giscus.category }}" + data-category-id="{{ config.extra.giscus.category_id }}" + data-mapping="pathname" + data-reactions-enabled="1" + data-emit-metadata="0" + data-input-position="top" + data-theme="transparent_dark" + data-lang="en" + crossorigin="anonymous" + ></script> + {% endif %} + </div> </div> -{% endmacro giscus %} +{{ js::comments() }} {% endif %} {% endmacro comments %} diff --git a/site/templates/macros/footer.html b/site/templates/macros/footer.html @@ -10,14 +10,14 @@ </span> </div> {% endmacro copyright %} {% macro theme(config) %} -<div class="theme-container"> +<div class="theme-container js-only"> <div class="theme_before"></div> <div class="theme-content"> - Theme: + <label for="theme-select">Theme:</label> <select id="theme-select"> {% for theme in config.extra.color_schemes %} {% if config.extra.theme_color %} {% set default_theme = - config.extra.theme_color %} {% else %} {% set default_theme = "blue" %} {% + config.extra.theme_color %} {% else %} {% set default_theme = "red" %} {% endif %} {% if default_theme == theme %} <option selected="selected" value="{{ theme }}">{{ theme }}</option> {% else %} @@ -30,11 +30,11 @@ {% endmacro theme %} {% macro webring(config) %} {% if config.extra.webring %} <div class="webring"> <a href="{{ config.extra.webring_url }}/prev/{{ config.extra.author }}" - ><- prev</a - > - <a href="{{ config.extra.webring_url }}">{{ config.extra.webring_name }}</a> - <a href="{{ config.extra.webring_url }}/next/{{ config.extra.author }}" - >next -></a + >&lt;- prev</a + >&nbsp;<a href="{{ config.extra.webring_url }}" + >{{ config.extra.webring_name }}</a + >&nbsp;<a href="{{ config.extra.webring_url }}/next/{{ config.extra.author }}" + >next -&gt;</a > </div> {% endif %} {% endmacro webring %} diff --git a/site/templates/macros/head.html b/site/templates/macros/head.html @@ -1,12 +1,14 @@ -{%- macro font(config) %} <link rel="stylesheet" href="{{ +{%- macro font(config) %} +<link rel="preload" href="{{ get_url(path="fonts/JetBrainsMono-Nerd-Regular.woff2") }}" as="font" type="font/woff2" crossorigin /> +<link rel="stylesheet" href="{{ get_url(path="fonts.css", trailing_slash=false) }}"> {% endmacro font -%} {% macro styling(config) %} <link rel="stylesheet" href="{{ get_url(path="style.css", trailing_slash=false) }}"> {% if config.extra.theme_color -%} {% set color = "color/" ~config.extra.theme_color ~ ".css" -%} -<link rel="stylesheet" href="{{ get_url(path=color, trailing_slash=false) }}" /> -{%- else -%} <link rel="stylesheet" href="{{ get_url(path="color/red.css", +<link id="site-theme" data-theme="{{ config.extra.theme_color }}" rel="stylesheet" href="{{ get_url(path=color, trailing_slash=false) }}" /> +{%- else -%} <link id="site-theme" data-theme="red" rel="stylesheet" href="{{ get_url(path="color/red.css", trailing_slash=false) }}"> {% endif %} {% endmacro styling %} {% macro feed_discovery(path) %} <link @@ -18,12 +20,29 @@ feed_discovery(path) %} config.extra.favicon %} <link rel="shortcut icon" - href="{{ get_url(path=config.extra.favicon, trailing_slash=false) }}?" + href="{{ get_url(path=config.extra.favicon, trailing_slash=false) }}" type="image/x-icon" /> {% endif %} {% endmacro favicon %} {% macro general_meta() %} -<meta http-equiv="content-type" content="text/html; charset=utf-8" /> -<meta - name="viewport" - content="width=device-width, initial-scale=1.0, maximum-scale=1" /> -<meta name="description" content="{{ config.description }}" /> +{% set description = config.description %} +{% set title = config.title %} +{% if page %} + {% set title = page.title %} + {% if page.description %}{% set description = page.description %} + {% else %}{% set description = page.content | striptags | truncate(length=160) %}{% endif %} +{% elif section %} + {% set title = section.title %} + {% if section.description %}{% set description = section.description %}{% endif %} +{% elif term %}{% set title = term.name %} +{% elif taxonomy %}{% set title = taxonomy.name %}{% endif %} +<meta name="description" content="{{ description }}" /> +<meta property="og:title" content="{{ title }}" /> +<meta property="og:description" content="{{ description }}" /> +<meta property="og:site_name" content="{{ config.title }}" /> +<meta property="og:type" content="{% if page and page.date %}article{% else %}website{% endif %}" /> +<meta name="twitter:card" content="summary" /> +{% if current_url %} +<link rel="canonical" href="{{ current_url }}" /> +<meta property="og:url" content="{{ current_url }}" /> +{% else %}<meta name="robots" content="noindex" />{% endif %} +{{ head::feed_discovery(path=config.extra.feed_path) }} {% endmacro general_meta %} diff --git a/site/templates/macros/header.html b/site/templates/macros/header.html @@ -3,16 +3,31 @@ <header class="header"> <div class="header-inner"> <div class="header-logo">{{ logo::logo(config=config) }}</div> - <div class="menu-trigger" tabindex="0"> - <div class="menu-trigger-button">menu</div> - </div> + <button + type="button" + class="menu-trigger menu-trigger-button" + aria-controls="mobile-menu" + aria-expanded="false" + > + menu + </button> </div> {{ menu::menu(config=config, current_url=current_url) }} {% if config.build_search_index %} - <div class="search-container js-only"> - <input class="input" id="search" type="search" placeholder="  Search" /> - <div class="search-results box"> - <div class="search-results__items"></div> + <div class="search-container js-only" role="search"> + <input + class="input" + id="search" + type="search" + placeholder="  Search" + aria-label="Search this site" + aria-controls="search-results" + data-library="{{ get_url(path='fuse.min.js') }}" + data-index="{{ get_url(path='search_index.en.json') }}" + /> + <div class="search-results box" id="search-results"> + <p class="search-results__number" role="status"></p> + <ul class="search-results__items"></ul> </div> </div> {% endif %} diff --git a/site/templates/macros/javascript.html b/site/templates/macros/javascript.html @@ -1,18 +1,15 @@ {% macro script() %} -<script src="{{ get_url(path="js/load.js") }}"></script> -<script src="{{ get_url(path="js/theme.js") }}"></script> -<script src="{{ get_url(path="js/scroll.js") }}"></script> -<script> switch_theme.init("{{config.base_url | safe}}"); </script> -<script src="{{ get_url(path="js/clipboard.js") }}"></script> -<script src="{{ get_url(path="js/footnote.js") }}"></script> +<script defer src="{{ get_url(path="js/load.js") }}"></script> +<script defer src="{{ get_url(path="js/theme.js") }}"></script> +{% if page %}<script defer src="{{ get_url(path="js/scroll.js") }}"></script>{% endif %} +{% if page and page.content is containing("<pre") %}<script defer src="{{ get_url(path="js/clipboard.js") }}"></script>{% endif %} +{% if page and page.content is containing("footnote-reference") %}<script defer src="{{ get_url(path="js/footnote.js") }}"></script>{% endif %} {% if config.build_search_index %} - <script src="{{ get_url(path="fuse.min.js") }}"></script> - <script src="{{ get_url(path="search_index.en.js") }}"></script> - <script src="{{ get_url(path="js/search.js") }}"></script> + <script defer src="{{ get_url(path="js/search.js") }}"></script> {% endif %} {% endmacro script %} -{% macro mobile() %} <script src="{{ get_url(path="js/mobile.js") }}"></script> {% endmacro mobile %} -{% macro comments() %} <script src="{{ get_url(path="js/comments.js") }}"></script> {% endmacro comments %} +{% macro mobile() %} <script defer src="{{ get_url(path="js/mobile.js") }}"></script> {% endmacro mobile %} +{% macro comments() %} <script defer src="{{ get_url(path="js/comments.js") }}"></script> {% endmacro comments %} {% macro katex() %} {% if config.extra.math_katex %} <link rel="stylesheet" href="{{ get_url(path="math/katex.css") }}"> <script defer type="text/javascript" src="{{ get_url(path="js/katex.js") | safe }}"></script> @@ -25,5 +22,5 @@ {left: '\\(', right: '\\)', display: false}, {left: '\\[', right: '\\]', display: true} ] - });"></script> + }); document.querySelectorAll('.katex-display').forEach(element => element.tabIndex = 0);"></script> {% endif %} {% endmacro katex %} diff --git a/site/templates/macros/logo.html b/site/templates/macros/logo.html @@ -3,7 +3,7 @@ href="{%- if config.extra.logo_home_link -%}{{ config.extra.logo_home_link }}{%- else -%}{{ config.base_url }}{%- endif -%}"> <div class="logo"> {{ config.extra.logo_text | default(value="logo") | safe }} - <div class="loading"></div> + <div class="loading js-only" aria-hidden="true"></div> </div> </a> {%- endmacro logo -%} diff --git a/site/templates/macros/menu.html b/site/templates/macros/menu.html @@ -5,24 +5,24 @@ if config.extra.main_menu_show -%} {%- set show_len = config.extra.main_menu_show -%} {%- set menu_more = config.extra.main_menu_more -%} {%- else -%} {%- set show_len = main_len -%} {%- set menu_more = "" -%} {%- endif -%} -<nav class="menu-desktop"> +<nav class="menu-desktop" aria-label="Main navigation"> <ul class="menu-inner menu-inner-desktop"> {{ menu::items(menu=menu_items | slice(end=show_len), current_url=current_url)}} {%- if main_len > show_len -%} - <ul class="menu-inner-list"> - <li class="menu-inner-list-more-trigger" tabindex="0"> + <li class="menu-inner-list"> + <button type="button" class="menu-inner-list-more-trigger" aria-controls="more-menu" aria-expanded="false"> {{ menu_more }} ▾ - </li> - <ul class="menu-inner-list-more"> + </button> + <ul class="menu-inner-list-more" id="more-menu"> {{ menu::items(menu=menu_items | slice(start=show_len), current_url=current_url) }} </ul> - </ul> + </li> {%- endif -%} </ul> </nav> -<nav class="menu-mobile"> - <ul class="menu-inner menu-inner-mobile hidden"> +<nav class="menu-mobile hidden" id="mobile-menu" aria-label="Main navigation"> + <ul class="menu-inner menu-inner-mobile"> {{ menu::items(menu=menu_items, current_url=current_url) }} </ul> </nav> diff --git a/site/templates/macros/pagination.html b/site/templates/macros/pagination.html @@ -5,7 +5,7 @@ {% if paginator.previous -%} <span class="pagination__buttons-previous"> <a href="{{ paginator.previous }}"> - <span class="button__text"><- newer</span> + <span class="button__text">&lt;- newer</span> </a> </span> {% endif -%} @@ -13,7 +13,8 @@ <form class="pagination__form" enctype="text/plain" - action="{{ paginator.base_url }}"> + action="{{ paginator.base_url }}" + > <label for="page">page</label> <input id="page" @@ -23,7 +24,8 @@ min="1" max="{{ paginator.number_pagers }}" oninput="this.style.width = this.value.length + 3 + 'ch'" - required />/ {{ paginator.number_pagers }} + required + />/ {{ paginator.number_pagers }} <button>Go</button> </form> </div> diff --git a/site/templates/macros/social.html b/site/templates/macros/social.html @@ -3,53 +3,116 @@ {%- if config.generate_feeds -%} <a href="{{ config.extra.feed_path }}" style="text-decoration: none"> {%- set rss_image = resize_image(path="../static/social/rss.png", width=33, - height=33, op="scale", format="png") -%} - <img src="{{ rss_image.url }}" style="display: inline" alt="RSS Badge" /> + height=33, op="scale", format="png") -%} {%- set rss_image_2x = + resize_image(path="../static/social/rss.png", width=66, height=66, + op="scale", format="png") -%} {%- set rss_image_3x = + resize_image(path="../static/social/rss.png", width=99, height=99, + op="scale", format="png") -%} + <img + src="{{ rss_image.url }}" + srcset="{{ rss_image_2x.url }} 2x, {{ rss_image_3x.url }} 3x" + width="33" + height="33" + loading="lazy" + decoding="async" + style="display: inline" + alt="RSS Badge" + /> </a> {%- endif -%} {%- if config.extra.github_profile -%} <a href="{{ config.extra.github_profile }}" style="text-decoration: none"> {%- set gh_image = resize_image(path="../static/social/github.png", - width=33, height=33, op="scale", format="png") -%} + width=33, height=33, op="scale", format="png") -%} {%- set gh_image_2x = + resize_image(path="../static/social/github.png", width=66, height=66, + op="scale", format="png") -%} {%- set gh_image_3x = + resize_image(path="../static/social/github.png", width=99, height=99, + op="scale", format="png") -%} <img src="{{ gh_image.url }}" + srcset="{{ gh_image_2x.url }} 2x, {{ gh_image_3x.url }} 3x" + width="33" + height="33" + loading="lazy" + decoding="async" style="display: inline" - alt="GitHub Profile Badge" /> + alt="GitHub Profile Badge" + /> </a> {%- endif -%} {%- if config.extra.email -%} <a href="{{ config.extra.email }}" style="text-decoration: none"> {%- set email_image = resize_image(path="../static/social/mail.png", - width=36, height=36, op="scale", format="png") -%} + width=36, height=36, op="scale", format="png") -%} {%- set email_image_2x = + resize_image(path="../static/social/mail.png", width=72, height=72, + op="scale", format="png") -%} {%- set email_image_3x = + resize_image(path="../static/social/mail.png", width=108, height=108, + op="scale", format="png") -%} <img src="{{ email_image.url }}" + srcset="{{ email_image_2x.url }} 2x, {{ email_image_3x.url }} 3x" + width="36" + height="36" + loading="lazy" + decoding="async" style="display: inline" - alt="Email Badge" /> + alt="Email Badge" + /> </a> {%- endif -%} {%- if config.extra.twitter -%} <a href="{{ config.extra.twitter }}" style="text-decoration: none"> {%- set twitter_image = resize_image(path="../static/social/twitter.png", - width=33, height=33, op="scale", format="png") -%} + width=33, height=33, op="scale", format="png") -%} {%- set twitter_image_2x + = resize_image(path="../static/social/twitter.png", width=66, height=66, + op="scale", format="png") -%} {%- set twitter_image_3x = + resize_image(path="../static/social/twitter.png", width=99, height=99, + op="scale", format="png") -%} <img src="{{ twitter_image.url }}" + srcset="{{ twitter_image_2x.url }} 2x, {{ twitter_image_3x.url }} 3x" + width="33" + height="33" + loading="lazy" + decoding="async" style="display: inline" - alt="Twitter Badge" /> + alt="Twitter Badge" + /> </a> {%- endif -%} {%- if config.extra.reddit_profile -%} <a href="{{ config.extra.reddit_profile }}" style="text-decoration: none"> {%- set reddit_image = resize_image(path="../static/social/reddit.png", - width=33, height=33, op="scale", format="png") -%} + width=33, height=33, op="scale", format="png") -%} {%- set reddit_image_2x = + resize_image(path="../static/social/reddit.png", width=66, height=66, + op="scale", format="png") -%} {%- set reddit_image_3x = + resize_image(path="../static/social/reddit.png", width=99, height=99, + op="scale", format="png") -%} <img src="{{ reddit_image.url }}" + srcset="{{ reddit_image_2x.url }} 2x, {{ reddit_image_3x.url }} 3x" + width="33" + height="33" + loading="lazy" + decoding="async" style="display: inline" - alt="Reddit Badge" /> + alt="Reddit Badge" + /> </a> {%- endif -%} {%- if config.extra.discord_profile -%} <a href="{{ config.extra.discord_profile }}" style="text-decoration: none"> {%- set discord_image = resize_image(path="../static/social/discord.png", - width=39, height=39, op="scale", format="png") -%} + width=39, height=39, op="scale", format="png") -%} {%- set discord_image_2x + = resize_image(path="../static/social/discord.png", width=78, height=78, + op="scale", format="png") -%} {%- set discord_image_3x = + resize_image(path="../static/social/discord.png", width=117, height=117, + op="scale", format="png") -%} <img src="{{ discord_image.url }}" + srcset="{{ discord_image_2x.url }} 2x, {{ discord_image_3x.url }} 3x" + width="39" + height="39" + loading="lazy" + decoding="async" style="display: inline" - alt="Discord Badge" /> + alt="Discord Badge" + /> </a> {%- endif -%} </div> diff --git a/site/templates/macros/toc.html b/site/templates/macros/toc.html @@ -1,57 +1,16 @@ -{% macro toc (t) %} {% if t %} -<div class="toc" id="nav-container"> +{% macro entries(headings) %} +<ul> + {% for heading in headings %} + <li> + <a href="{{ heading.permalink | safe }}">{{ heading.title }}</a> + {% if heading.children %}{{ toc::entries(headings=heading.children) }}{% + endif %} + </li> + {% endfor %} +</ul> +{% endmacro entries %} {% macro toc(t) %} {% if t %} +<nav class="toc" id="nav-container" aria-label="Table of contents"> <p class="toc-head">Table of Contents</p> - <div id="nav-content"> - <ul> - {% for h1 in page.toc %} - <li> - <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> - {% if h1.children %} - <ul> - {% for h2 in h1.children %} - <li> - <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> - </li> - {% if h2.children %} - <ul> - {% for h3 in h2.children %} - <li> - <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a> - {% if h3.children %} - <ul> - {% for h4 in h3.children %} - <li> - <a href="{{ h4.permalink | safe }}">{{ h4.title }}</a> - </li> - {% if h4.children %} - <ul> - {% for h5 in h4.children %} - <li> - <a href="{{ h5.permalink | safe }}">{{ h5.title }}</a> - {% if h5.children %} - <ul> - {% for h6 in h5.children %} - <li> - <a href="{{ h6.permalink | safe }}">{{ h6.title }}</a> - </li> - {% endfor %} - </ul> - {% endif %} - </li> - {% endfor %} - </ul> - {% endif %} {% endfor %} - </ul> - {% endif %} - </li> - {% endfor %} - </ul> - {% endif %} {% endfor %} - </ul> - {% endif %} - </li> - {% endfor %} - </ul> - </div> -</div> -{% endif %} {% endmacro %} + <div id="nav-content">{{ toc::entries(headings=t) }}</div> +</nav> +{% endif %} {% endmacro toc %} diff --git a/site/templates/page.html b/site/templates/page.html @@ -36,7 +36,6 @@ endblock math -%} {%- block title %} {% endif %} {% if config.extra.edit_page and page.extra.edit %} {{ edit::edit_page(current_path=current_path) }} {% endif %} {% if page.extra.comments %} - <button type="button" class="comments-collapsible">Show Comments</button> {{ comments::comments() }} {% endif %} {% if page.extra.github %} <script async defer src="https://buttons.github.io/buttons.js"></script> {% endif %} {{ pages::related() }} diff --git a/site/templates/shortcodes/figure.html b/site/templates/shortcodes/figure.html @@ -1,6 +1,22 @@ {% if src %} <figure class="{% if position %}{{ position }}{% else -%} center {%- endif %}"> <img + loading="lazy" + decoding="async" + {% + if + width + %}width="{{ width }}" + {% + endif + %} + {% + if + height + %}height="{{ height }}" + {% + endif + %} src="{{ src | safe }}" {% if @@ -16,7 +32,8 @@ style="{{ style }}" {% endif - %} /> + %} + /> {% if caption %} <figcaption class="{% if caption_position %}{{ caption_position }}{% else -%} center {%- endif %}" @@ -27,7 +44,8 @@ style="{{ caption_style | safe }}" {% endif - %}> + %} + > {{ caption }} </figcaption> {% endif %} diff --git a/site/templates/shortcodes/image.html b/site/templates/shortcodes/image.html @@ -1,7 +1,10 @@ <figure - class="{% if class is defined %}{{ class | safe }}{% else -%} center {%- endif %}"> + class="{% if class is defined %}{{ class | safe }}{% else -%} center {%- endif %}" +> {%- if static is defined -%} <img + loading="lazy" + decoding="async" src="{{ get_url(path=src) | safe }}" {% if @@ -10,9 +13,12 @@ alt="{{ alt }}" {% endif - %} /> + %} + /> {%- else -%} {%- set src = page.path ~ src -%} <img + loading="lazy" + decoding="async" src="{{ get_url(path=src) | safe }}" {% if @@ -21,7 +27,8 @@ alt="{{ alt }}" {% endif - %} /> + %} + /> {%- endif -%} {% if caption %} <figcaption {% @@ -31,7 +38,8 @@ style="{{ caption_style | safe }}" {% endif - %}> + %} + > {{ caption }} </figcaption> {% endif %} diff --git a/site/templates/shortcodes/url.html b/site/templates/shortcodes/url.html @@ -1,4 +1,4 @@ -{% if trail_slash -%} {% set url = get_url(path=path, +{% if trail_slash is defined -%} {% set url = get_url(path=path, trailing_slash=trail_slash) %} {%- else -%} {% set url = get_url(path=path) %} {%- endif -%} <a href="{{ url }}">{{ text }}</a> diff --git a/site/templates/shortcodes/youtube.html b/site/templates/shortcodes/youtube.html @@ -1,5 +1,6 @@ <div class="youtube is-flex is-justify-content-center is-align-items-center"> <iframe + title="{{ title | default(value="YouTube video") }}" width="100%" src="https://www.youtube.com/embed/{{ id }}" frameborder="0" diff --git a/site/zola.toml b/site/zola.toml @@ -1,13 +1,13 @@ # Site URL -base_url = "/" +base_url = "https://maydayv7.cc" title = "maydayv7" description = "My Personal Website" # Default Language used in Feeds default_language = "en" -# Compile all '.scss' files in './sass' -compile_sass = true +compile_sass = true # Compile all '.scss' files in './sass' +minify_html = true # Build search index to be used later by JS Library build_search_index = true @@ -47,7 +47,7 @@ include_title = true include_description = false include_path = false include_content = true -index_format = "fuse_javascript" +index_format = "fuse_json" [extra] # Site Theme