Site for my Nix docs nix.ladas552.me

init on tangled

Ladas552 2850b332

+3226
+1
.envrc
··· 1 + use flake . --no-pure-eval
+7
.gitignore
··· 1 + # theme 2 + .theme_backup 3 + 4 + # direnv 5 + .direnv 6 + 7 + public
+21
.tangled/workflows/deploy.yaml
··· 1 + engine: nixery 2 + when: 3 + - event: ["push"] 4 + branch: ["master"] 5 + 6 + clone: 7 + depth: 1 8 + 9 + dependencies: 10 + nixpkgs/nixpkgs-unstable: 11 + - norgolith 12 + - openssh 13 + 14 + steps: 15 + - name: build site 16 + command: | 17 + lith build 18 + - name: deploy 19 + command: | 20 + scp -r public/ ladas552@nix.ladas552.me:~/sites/nix 21 +
+181
assets/favicon.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="1280" 6 + height="1280" 7 + viewBox="0 0 338.66667 338.66666" 8 + version="1.1" 9 + id="svg1" 10 + xml:space="preserve" 11 + inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" 12 + sodipodi:docname="norgolith.svg" 13 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 14 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 15 + xmlns="http://www.w3.org/2000/svg" 16 + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview 17 + id="namedview1" 18 + pagecolor="#ffffff" 19 + bordercolor="#000000" 20 + borderopacity="0.25" 21 + inkscape:showpageshadow="2" 22 + inkscape:pageopacity="0.0" 23 + inkscape:pagecheckerboard="true" 24 + inkscape:deskcolor="#d1d1d1" 25 + inkscape:document-units="px" 26 + inkscape:zoom="0.51213161" 27 + inkscape:cx="530.13717" 28 + inkscape:cy="504.75307" 29 + inkscape:window-width="1920" 30 + inkscape:window-height="1009" 31 + inkscape:window-x="-8" 32 + inkscape:window-y="-8" 33 + inkscape:window-maximized="1" 34 + inkscape:current-layer="layer1" 35 + showgrid="false" /><defs 36 + id="defs1"><inkscape:path-effect 37 + effect="fillet_chamfer" 38 + id="path-effect15" 39 + is_visible="true" 40 + lpeversion="1" 41 + nodesatellites_param="" 42 + radius="0" 43 + unit="px" 44 + method="auto" 45 + mode="F" 46 + chamfer_steps="1" 47 + flexible="false" 48 + use_knot_distance="true" 49 + apply_no_radius="true" 50 + apply_with_radius="true" 51 + only_selected="false" 52 + hide_knots="false" /><linearGradient 53 + id="swatch10" 54 + inkscape:swatch="solid"><stop 55 + style="stop-color:#32b6be;stop-opacity:1;" 56 + offset="0" 57 + id="stop13" /></linearGradient><linearGradient 58 + id="SVGID_1_" 59 + gradientUnits="userSpaceOnUse" 60 + x1="205.27161" 61 + y1="136.063" 62 + x2="800.72571" 63 + y2="855.02252" 64 + gradientTransform="matrix(1,0,0,-1,0,1024)"> 65 + <stop 66 + offset="0" 67 + style="stop-color:#33AAE6" 68 + id="stop1" /> 69 + <stop 70 + offset="0.9915" 71 + style="stop-color:#4B3892" 72 + id="stop2" /> 73 + </linearGradient><linearGradient 74 + id="SVGID_2_" 75 + gradientUnits="userSpaceOnUse" 76 + x1="531.92999" 77 + y1="357.42569" 78 + x2="693.40002" 79 + y2="357.42569" 80 + gradientTransform="matrix(1,0,0,-1,0,1024)"> 81 + <stop 82 + offset="0" 83 + style="stop-color:#3D85C9" 84 + id="stop3" /> 85 + <stop 86 + offset="1" 87 + style="stop-color:#4A4198" 88 + id="stop4" /> 89 + </linearGradient><linearGradient 90 + id="SVGID_3_" 91 + gradientUnits="userSpaceOnUse" 92 + x1="531.91998" 93 + y1="288.905" 94 + x2="944.48761" 95 + y2="288.905" 96 + gradientTransform="matrix(1,0,0,-1,0,1024)"> 97 + <stop 98 + offset="0" 99 + style="stop-color:#61C67C" 100 + id="stop5" /> 101 + <stop 102 + offset="1" 103 + style="stop-color:#366794" 104 + id="stop6" /> 105 + </linearGradient><linearGradient 106 + id="SVGID_4_" 107 + gradientUnits="userSpaceOnUse" 108 + x1="843.47998" 109 + y1="404.53009" 110 + x2="969.75562" 111 + y2="404.53009" 112 + gradientTransform="matrix(1,0,0,-1,0,1024)"> 113 + <stop 114 + offset="0" 115 + style="stop-color:#2F6B7F" 116 + id="stop7" /> 117 + <stop 118 + offset="1" 119 + style="stop-color:#325D87" 120 + id="stop8" /> 121 + </linearGradient><linearGradient 122 + id="SVGID_5_" 123 + gradientUnits="userSpaceOnUse" 124 + x1="513.32288" 125 + y1="718.00439" 126 + x2="900.78381" 127 + y2="143.70779" 128 + gradientTransform="matrix(1,0,0,-1,0,1024)"> 129 + <stop 130 + offset="0" 131 + style="stop-color:#3B7FC4" 132 + id="stop9" /> 133 + <stop 134 + offset="0.4665" 135 + style="stop-color:#90B1DF" 136 + id="stop10" /> 137 + <stop 138 + offset="0.764" 139 + style="stop-color:#7381C0" 140 + id="stop11" /> 141 + <stop 142 + offset="1" 143 + style="stop-color:#5F5FAB" 144 + id="stop12" /> 145 + </linearGradient></defs><g 146 + inkscape:label="Capa 1" 147 + inkscape:groupmode="layer" 148 + id="layer1"><g 149 + id="g17" 150 + transform="matrix(0.95818663,0,0,0.95818663,7.080397,7.080397)"><g 151 + id="g1" 152 + transform="matrix(1.189346,0,0,1.189346,10.198838,10.198838)" 153 + style="fill:#425dae;fill-opacity:1"><path 154 + fill="#000000" 155 + d="M 219.6,48.1 C 196.7,25.2 166.2,12.6 133.8,12.6 101.4,12.6 71,25.2 48.1,48.1 25.2,71 12.6,101.5 12.6,133.9 c 0,32.4 12.6,62.9 35.5,85.8 22.9,22.9 53.4,35.5 85.8,35.5 32.4,0 62.9,-12.6 85.8,-35.5 22.9,-22.9 35.5,-53.4 35.5,-85.8 0,-32.4 -12.7,-62.9 -35.6,-85.8 z" 156 + id="path1" 157 + style="fill:#425dae;fill-opacity:1" /><path 158 + fill="#000000" 159 + d="M 133.8,0 C 59.9,0 0,59.9 0,133.8 c 0,73.9 59.9,133.8 133.8,133.8 73.9,0 133.8,-59.9 133.8,-133.8 C 267.6,59.9 207.8,0 133.8,0 Z m 0,260.1 C 64.1,260.1 7.5,203.6 7.5,133.8 7.5,64 64.1,7.6 133.8,7.6 c 69.7,0 126.3,56.5 126.3,126.3 0,69.8 -56.5,126.2 -126.3,126.2 z" 160 + id="path2" 161 + style="fill:#425dae;fill-opacity:1" /></g><g 162 + id="g2" 163 + transform="matrix(2.4193807,0,0,2.4193807,48.359493,48.359967)" 164 + style="fill:#e6e6e6;fill-opacity:1"><g 165 + id="g16"><path 166 + d="m 46.355,96.716162 a 5.14,4.3759472 0 0 0 3.644,1.283839 5.14,4.3759472 0 0 0 3.645,-1.283839 l 23.129,-19.69091 a 5.127,4.3648797 0 0 0 1.512,-3.102325 v -6.967463 a 5.138,4.3742445 0 0 0 -3.184,-4.054136 5.144,4.3793526 0 0 0 -5.617,0.950961 L 50.816,79.741915 a 1.147,0.97650029 0 0 1 -1.633,0 L 30.52,63.822491 c -1.48,-1.260001 -3.684,-1.635447 -5.617,-0.953514 -1.933,0.681933 -3.184,2.270555 -3.184,4.053285 v 7.00407 a 5.1,4.3418931 0 0 0 1.512,3.102325 z" 167 + id="path1-4" 168 + style="fill:#e6e6e6;fill-opacity:1;stroke-width:0.922688" /><path 169 + d="M 46.355,2.7570265 33.742,16.125 c -0.965217,0.967485 -1.508728,2.277377 -1.512,3.644 v 31.027 c 0,1.38 0.535,2.672 1.512,3.645 l 12.613,12.617 c 0.966006,0.966822 2.280272,2.415079 3.646986,2.412968 1.36706,0.0024 2.675764,-1.4459 3.642014,-2.412968 L 66.257,54.44 c 0.970123,-0.96461 1.514494,-2.276934 1.512,-3.645 V 19.77 C 67.7651,18.403508 67.221664,17.093854 66.257,16.126 L 53.644,2.7530265 c -2.016047,-2.00746088 -5.276158,-2.00567208 -7.29,0.004 z" 170 + id="path2-2" 171 + style="fill:#e6e6e6;fill-opacity:1" 172 + sodipodi:nodetypes="cccsccccccccccc" /></g></g></g></g><style 173 + type="text/css" 174 + id="style1"> 175 + .st0{fill:none;} 176 + .st1{fill:url(#SVGID_1_);} 177 + .st2{fill:url(#SVGID_2_);} 178 + .st3{fill:url(#SVGID_3_);} 179 + .st4{fill:url(#SVGID_4_);} 180 + .st5{fill:url(#SVGID_5_);} 181 + </style></svg>
+28
content/index.norg
··· 1 + @document.meta 2 + title: Welcome to Flake-Ocean 3 + description: This is a static site made using norgolith. 4 + authors: [ 5 + ladas552 6 + ] 7 + categories: [] 8 + created: 2025-11-12 9 + layout: home 10 + version: 1.1.1 11 + @end 12 + 13 + * Nix 14 + Hi, this is Ladas552 speaking 15 + 16 + You are reading beta version of docs for my NixOS config, It will explain how it works and why it works! 17 + 18 + Not because it's complicated, but for people to learn and grow with me. 19 + 20 + Have fun reading some articles, hopefully more and better down bellow. 21 + 22 + Feel free to ask questions to improve them to plank scale limits. 23 + 24 + The site is build using norgolith and tailwind css. But keep in mind, the front end was vibecoded, so don't go looking for that source code - it's pure ass. 25 + 26 + If you don't wanna read the whole thing just because it was build using an llm that actively destroys out planet. I can't blame you. 27 + 28 + But if you have some tailwind CSS or web knowledge please let me know to improve this site with human hands. Because I don't wanna maintain 2 different sites at the same time all by myself.
+15
content/posts/Flake-Parts.norg
··· 1 + @document.meta 2 + title: Flake Parts 3 + description: Deep dive into Nix Flakes 4 + authors: ladas552 5 + categories: [ 6 + tutorial 7 + flakes 8 + ] 9 + created: 2025-12-02 10 + layout: post 11 + draft: true 12 + version: 1.1.1 13 + @end 14 + 15 + * Flake Parts
+17
content/posts/Hjem-rum.norg
··· 1 + @document.meta 2 + title: Hjem-rum 3 + description: Introduction to Nix and NixOS 4 + authors: ladas552 5 + categories: [ 6 + tutorial 7 + basics 8 + ] 9 + created: 2025-12-01T10:00:00+05:00 10 + updated: 2025-12-26T19:52:40 11 + layout: post 12 + draft: true 13 + version: 1.1.1 14 + @end 15 + 16 + * Hjem-rum 17 +
+20
content/posts/Home-manager.norg
··· 1 + @document.meta 2 + title: Home Manager 3 + description: Lazy house keeping service 4 + authors: [ 5 + ladas552 6 + ] 7 + categories: [ 8 + Home 9 + dotfiles 10 + ] 11 + created: 2026-02-08T21:13:36+05:00 12 + updated: 2026-02-08T21:13:36+05:00 13 + draft: true 14 + layout: post 15 + version: 1.1.1 16 + @end 17 + 18 + * Home Manager 19 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut 20 + labore et dolore magna aliqua. Lobortis scelerisque fermentum dui faucibus in ornare.
+558
content/posts/Impermanence.norg
··· 1 + @document.meta 2 + title: Impermanence on NixOS with ZFS and tmpfs 3 + description: Guide for my impermanence setup 4 + authors: [ 5 + ladas552 6 + ] 7 + categories: [ 8 + Impermanence 9 + ZFS 10 + Guide 11 + ] 12 + created: 2026-01-02 13 + draft: false 14 + layout: post 15 + version: 1.1.1 16 + @end 17 + 18 + ** What is Impermanence 19 + ___ 20 + It wipes your `/root` on reboot and your startup is a blank canvas, but you can persist mounts and bind mount directories from it in your normal root to save stuff like cache and tokens. So you wipe all the junk and save actually useful stuff. 21 + 22 + For example you can install full KDE Plasma session, run it, and if you get bored. Just disable it and no KDE junk left. 23 + 24 + *Important to note*: That impermanence of my setup uses tmpfs, so it writes `/root` to RAM, so nothing actually gets erased on the Disk. Meaning no continuous I/O rewrites wearing out your Drive(/not that it would mater in practice/). But the state isn't saved between reboots, as with anything on RAM 25 + 26 + Also when I refer to `/root`, it's actually the whole `/`, not just root user directory. 27 + *** Why did you set it up 28 + ___ 29 + I was bored. I don't find benefits of impermanence so crucial to completely overhaul how your system behaves and I don't trust myself to maintain it. 30 + 31 + But there are some benefits to it: 32 + - I only backup important files, no cache, no states, only files and media; 33 + - I always know what's on my system because it's declared in the config; 34 + - It opens up possibilities to experiment more with my system, because if I could setup impermanence and not loose all my files, I am unstoppable; 35 + *** What's the meaning of writing this page? 36 + ___ 37 + It's not that hard to setup impermanence, but to requires reading a lot of stuff, and if you don't use ZFS or BTRFS even full reinstall for rearranging partitions. I have read several articles, watched videos, and stole code from many GitHub repos. 38 + 39 + Plus most guides just go to the wipe stage right away, without saying how to persist, or how it practically works for the user to not loose their files. I will try to compete in these aspects. 40 + 41 + *** What is your current setup? 42 + ___ 43 + 44 + I got ZFS with tmpfs, with 2 persistence datasets. `/cache` and `/persist`. And a plaint vFAT `/boot` partition, where GRUB or Systemd-boot will put generation images. 45 + 46 + Most people assume you gotta have 64Gb or RAM for tmpfs to be convenient, but actually you just need a well structured disk layout. Then the tmpfs usually only takes 50MB to 100MB of RAM. 47 + 48 + Template structure of ZFS datasets that will be essential: 49 + 50 + - `/cache` is for rust targets, everything in `~/.cache`, .local states, etc. 51 + 52 + - `/persist` is for Media, browser profiles, Projects, etc. This is the only datasets that get's backed up by `sanoid`. 53 + 54 + - `/nix` for /nix/store. NixOS won't boot without it. All the files that aren't persisted, but appear on my system are symlinked from `/nix`. That includes config files and services. 55 + 56 + - `/tmp` for /tmp. yeah, anyways it's to not overload tmpfs when downloading something on browser. with `boot.tmp.cleanOnBoot = true;` it is cleared on boot anyways. 57 + 58 + tmpfs is erased on reboot, so `/` and everything below it, including `/home` is gone, unless put into `/cache` or `/persist` datasets. 59 + tmpfs is on RAM, so it can overload if exceeds certain size, to prevent that I got several more zfs datasets, that aren't persisted, meaning they don't have connection to files in other datasets, but aren't erased by default. 60 + 61 + ** What we need? 62 + *** A ZFS setup 63 + This isn't a *ZFS guide* so, unless you have *ZFS setup* on your *NixOS*, you can kiss this Guide goodbye. Go read OpenZFS documentation. But here is my installation script in case you'd like more guiding. But seriously, go read docs, real engineers know a lot more than I do. 64 + 65 + {https://github.com/Ladas552/Flake-Ocean/blob/e460837d18f37723510f9b74c46636fd2b5b4f25/install/impermanence.norg}[permalink in the github repo, please insure the branch is up to date before checking in], it contains actual descriptions to each line as a Norg file format that you can tangle. Like bible in org mode. 66 + @code sh 67 + sudo zpool create -f \ 68 + -o ashift=12 \ 69 + -o autotrim=on \ 70 + -O compression=zstd \ 71 + -O acltype=posixacl \ 72 + -O atime=off \ 73 + -O xattr=sa \ 74 + -O normalization=formD \ 75 + -O mountpoint=none \ 76 + zroot "/dev/sda2" 77 + 78 + sudo zfs create -o mountpoint=legacy zroot/root 79 + sudo mount -t zfs zroot/root /mnt 80 + 81 + sudo mount --mkdir "$BOOTDISK" /mnt/boot 82 + # All the stuff below will be explained later 83 + sudo zfs create -o mountpoint=legacy zroot/nix 84 + sudo mount --mkdir -t zfs zroot/nix /mnt/nix 85 + 86 + sudo zfs create -o mountpoint=legacy zroot/tmp 87 + sudo mount --mkdir -t zfs zroot/tmp /mnt/tmp 88 + 89 + sudo zfs create -o mountpoint=legacy zroot/cache 90 + sudo mount --mkdir -t zfs zroot/cache /mnt/cache 91 + 92 + sudo zfs create -o mountpoint=legacy zroot/persist 93 + sudo zfs snapshot zroot/persist@blank 94 + sudo mount --mkdir -t zfs zroot/persist /mnt/persist 95 + # All the stuff above will be explained later 96 + sudo nixos-install --no-root-password --flake "github:Ladas552/Nix-Is-Unbreakable#NixVM" 97 + @end 98 + 99 + *** Partitions 100 + ___ 101 + A new way to manage your system. NixOS. 102 + 103 + Tho you probably already use NixOS if you are reading this, if you don't then get out while you can. 104 + 105 + On a more serious note, you need ZFS setup, with 2 particular datasets. 106 + @code nix 107 + fileSystems = { 108 + "/nix" = { 109 + device = "zroot/nix"; 110 + fsType = "zfs"; 111 + }; 112 + "/tmp" = { 113 + device = "zroot/tmp"; 114 + fsType = "zfs"; 115 + }; 116 + }; 117 + @end 118 + 119 + If you don't have them, but have ZFS installed, just create them using commands 120 + @code sh 121 + sudo zfs create -o mountpoint=legacy zroot/tmp 122 + sudo zfs create -o mountpoint=legacy zroot/nix 123 + @end 124 + 125 + This will insure that you won't delete your `/nix/store` and it stays intact between reboots. And for this particular setup the `tmp` dataset will be used so our `tmpfs` *root* will insure that it won't randomly overload. 126 + 127 + *** Impermanence module 128 + ___ 129 + The [Impermanence module]{https://github.com/nix-community/impermanence} is a NixOS flake that creates `mount binds`. The main purpose of it is to just put stuff in special `/persist` dataset, and still be able to access it from `/root` and `/home`. More about technicality of bind mounts later 130 + 131 + Basically you define certain directories names in it, and it creates them, then binds them to specific relevant locations, like `".config/nvim"` will be located in `~/.config/nvim`. And if you put your Neovim config there, neovim will still follow the config, but it will be located on different dataset, and won't be wiped on boot. 132 + 133 + Neat right? Not really, because if directory already exists, Impermanence will override that old directory with new empty one. *Don't panic*. Data isn't lost, it was just reallocated, you can delete the directory from impermanence module and it will comeback. 134 + 135 + That's the main reason why most people reinstall their OS if they want to use Impermanence, because it's a pain in the glands to move the files from directories before persisting it and moving things back. There are projects that circumvent that, but I didn't use them. For example: [Persist-retro]{https://github.com/Geometer1729/persist-retro}. 136 + 137 + Also to persist an individual file, you need to move the file, and manually copy it to persist directory. Otherwise it complains about the original file being in the way of a mount bind. 138 + 139 + **** You forgot to tell installation instructions 140 + ___ 141 + It's nix so here is just a snippet of code. Works for flakes. 142 + @code nix 143 + #flake.nix 144 + { 145 + inputs.impermanence.url = "github:nix-community/impermanence"; 146 + } 147 + @end 148 + And then just import the module, like: 149 + @code nix 150 + imports = [ 151 + inputs.impermanence.nixosModules.impermanence 152 + ]; 153 + @end 154 + 155 + We will only use the `nixosModule` because I don't have standalone Home-Manager and not planning to adopt impermanence for distros outside of NixOS. 156 + 157 + I have seen people use impermanence module on non flake setups, but I am not so interested in them to find and link a good one. 158 + *** Immutable users 159 + ___ 160 + As we delete everything in `/root`, it means passwords for users, and most importantly `root` user will be deleted. 161 + 162 + So just make them immutable. You can store the password file in sops, or just provide raw path from `/persist` directory. 163 + 164 + @code nix 165 + { 166 + # setup immutable users for impermanence 167 + 168 + # silence warning about setting multiple user password options 169 + # https://github.com/NixOS/nixpkgs/pull/287506#issuecomment-1950958990 170 + # Stolen from Iynaix https://github.com/iynaix/dotfiles/blob/4880969e7797451f4adc3475cf33f33cc3ceb86e/nixos/users.nix#L18-L24 171 + options = { 172 + warnings = lib.mkOption { 173 + apply = lib.filter ( 174 + w: !(lib.hasInfix "If multiple of these password options are set at the same time" w) 175 + ); 176 + }; 177 + }; 178 + 179 + config = { 180 + # disabling user mutability 181 + users.mutableUsers = false; 182 + 183 + # defining regular user, ME! 184 + users.users.ladas552 = { 185 + isNormalUser = true; 186 + description = "Ladas552"; 187 + extraGroups = [ 188 + "networkmanager" 189 + "wheel" 190 + ]; 191 + initialPassword = "pass"; 192 + # Use a path or your encryption method here 193 + hashedPasswordFile = config.sops.secrets."mystuff/host_pwd".path; 194 + }; 195 + 196 + nix.settings.trusted-users = [ "ladas552" ]; 197 + 198 + # Setting root user 199 + users.users.root = { 200 + initialPassword = "pass"; 201 + hashedPasswordFile = config.sops.secrets."mystuff/host_pwd".path; 202 + }; 203 + }; 204 + } 205 + @end 206 + 207 + Other features for immutable users: 208 + - Can use `--no-root-password` flag in `nixos-install` command. Meaning you don't ever have to monitor it, it will install password automatically. 209 + - Can't use `passwd <user>` command. So if you mess up your password path the first time, you have to reboot to previous generation to set it correctly. 210 + 211 + The `initialPassword` is set as plain text because it suppose to be a backup if sops decryption failed, so you won't leave with useless system state. Otherwise, it's unused and won't have security implications for your host. 212 + 213 + *** When do we start deleting stuff? 214 + ___ 215 + Not so fast bakaru, we first need to save our stuff. 216 + 217 + So you need to create persisted directories 218 + @code sh 219 + sudo zfs create -o mountpoint=legacy zroot/persist 220 + sudo zfs create -o mountpoint=legacy zroot/cache 221 + @end 222 + 223 + And now we add them to be mounted on boot 224 + @code nix 225 + # persist mount 226 + fileSystems."/persist" = { 227 + device = "zroot/persist"; 228 + fsType = "zfs"; 229 + # so it's required to boot, and you won't reboot into empty desktop 230 + neededForBoot = true; 231 + }; 232 + 233 + # cache are files that should be persisted, but not to snapshot 234 + # e.g. npm, cargo cache etc, that could always be redownload 235 + "/cache" = { 236 + device = "zroot/cache"; 237 + fsType = "zfs"; 238 + neededForBoot = true; 239 + }; 240 + @end 241 + 242 + I also recommend setting up backups with sanoid if you didn't already. 243 + 244 + @code nix 245 + services.sanoid = { 246 + enable = true; 247 + # if you have sanoid options somewhere else, lib.mkForce 248 + # will override anything, so you only have snapshots that matter 249 + datasets = lib.mkForce { 250 + "zroot/persist" = { 251 + hourly = 50; 252 + daily = 15; 253 + weekly = 3; 254 + monthly = 1; 255 + }; 256 + }; 257 + }; 258 + @end 259 + 260 + Now we have basic datasets that will store out stuff, Impermanence can wait now, we need to assign bind mounts for directories and files! 261 + 262 + Don't know what bind mounts are? Well simply put. They are mounts that make some directory to appear in normal location, but actually it's in a different dataset all together. 263 + 264 + So for example: `/home/alice225/Downloads` will be deleted on boot. But, not it's content. On the next boot, the content of `Downloads` that is in `/persist` dataset will remount itself to `/home/alice225/Downloads` path. 265 + 266 + It will appear seamless to other applications and to yourself. But now you can access the same files in both `/home/alice225/Downloads` and in `/persist/home/alice225/Downloads`. And remember, it is *not a symlink*. Symlinks fool programs, while bind mounts genuinely make files accessible in several locations. But be careful, because they share permissions, and can also be deleted. 267 + 268 + *** Okay, we have locations, let's move some files into them 269 + ___ 270 + So, with Impermanence module, there are some options available. Simplest approach would be to use it directly 271 + 272 + @code nix 273 + environment.persistence = { 274 + # one of out datasets 275 + "/persist" = { 276 + # useful option 277 + hideMounts = true; 278 + directories = [ 279 + # absolute path to directories in string values 280 + "/var/log" 281 + "/var/lib/nixos" 282 + "/etc/NetworkManager/" 283 + ]; 284 + }; 285 + }; 286 + @end 287 + 288 + Now you are able to just define your paths as normal and save them. But this is just normal impermanence, This blog post is about *My* setup specifically, so how about we add some abstractions to the vanilla scheme. 289 + 290 + **** New options 291 + ___ 292 + `directories` and `files` in impermanence module are just a list of string, so we can make pseudo options to add more strings to the list and `++` them with actual persistence option, or just reference our list. 293 + 294 + It will make it easier to define directories under different scopes. For example, setting files in `home.nix` file, but they will be used in `configuration.nix` file. 295 + 296 + @code nix 297 + {lib,...}: 298 + { 299 + options = { 300 + # options to put directories in, persistence but shortened 301 + # stolen from @iynaix 302 + root = { 303 + directories = lib.mkOption { 304 + type = lib.types.listOf lib.types.str; 305 + default = [ ]; 306 + description = "Directories to persist in root filesystem"; 307 + }; 308 + files = lib.mkOption { 309 + type = lib.types.listOf lib.types.str; 310 + default = [ ]; 311 + description = "Files to persist in root filesystem"; 312 + }; 313 + cache = { 314 + directories = lib.mkOption { 315 + type = lib.types.listOf lib.types.str; 316 + default = [ ]; 317 + description = "Directories to persist, but not to snapshot"; 318 + }; 319 + files = lib.mkOption { 320 + type = lib.types.listOf lib.types.str; 321 + default = [ ]; 322 + description = "Files to persist, but not to snapshot"; 323 + }; 324 + }; 325 + }; 326 + home = { 327 + directories = lib.mkOption { 328 + type = lib.types.listOf lib.types.str; 329 + default = [ ]; 330 + description = "Directories to persist in home directory"; 331 + }; 332 + files = lib.mkOption { 333 + type = lib.types.listOf lib.types.str; 334 + default = [ ]; 335 + description = "Files to persist in home directory"; 336 + }; 337 + cache = { 338 + directories = lib.mkOption { 339 + type = lib.types.listOf lib.types.str; 340 + default = [ ]; 341 + description = "Directories to persist, but not to snapshot"; 342 + }; 343 + files = lib.mkOption { 344 + type = lib.types.listOf lib.types.str; 345 + default = [ ]; 346 + description = "Files to persist, but not to snapshot"; 347 + }; 348 + }; 349 + }; 350 + }; 351 + } 352 + # Holy cow nix is indented to all suns 353 + @end 354 + 355 + You can add more options if need be. In this we only define lists for `/persist` and `/cache`, but they are separated into `root` and `home`. So add `root` and `home` options to `nixocConfiguration` and add only `home` to home-manager for example. 356 + 357 + `home` is just a simple way to separate directories in `/home/ladas552` from just `/`. 358 + 359 + But, you know, I use flake-parts and I don't need to add these options to different files and scope. I can just inherit them all in one file! 360 + 361 + @code nix 362 + { lib, ... }: 363 + # link to snippet in my config 364 + # https://github.com/Ladas552/Flake-Ocean/blob/85ee207aa2e5e0d2e44aad0a0818a533ceca72cf/modules/nixosModules/Impermanence/imp-options.nix 365 + { 366 + flake.modules = 367 + let 368 + # options to put directories in, persistence but shortened 369 + # stolen from @iynaix 370 + 371 + root = {}; 372 + home = {}; 373 + # Same thing as above 374 + in 375 + { 376 + nixos.options.options.custom.imp = { inherit root home; }; 377 + hjem.options.options.custom.imp = { inherit home; }; 378 + homeManager.options.options.custom.imp = { inherit home; }; 379 + }; 380 + } 381 + @end 382 + 383 + This code block is from my Dendrithic config with several module classes. So each `nixos`, `hjem` and `homeManager` classes have their own `options` module that inherit the same type of options in each module scope. 384 + 385 + You probably didn't get any of that, but you don't need to tbh. My setup is My setup, do whatever you want. 386 + 387 + **** Persist in action 388 + ___ 389 + Now we can define some important directories and files to persist 390 + @code nix 391 + custom.imp = { 392 + root = { 393 + directories = [ 394 + "/etc/NetworkManager/" 395 + "/var/lib/NetworkManager" 396 + "/var/lib/iwd" 397 + ]; 398 + }; 399 + home = { 400 + directories = [ 401 + ".librewolf" 402 + ]; 403 + cache = { 404 + files = [ ".local/share/com.jeffser.Alpaca/alpaca.db" ]; 405 + directories = [ 406 + ".local/share/nvim" 407 + ".local/state/nvim" 408 + ".config/libreoffice" 409 + ".cache/librewolf" 410 + ".cache/keepassxc" 411 + ".config/keepassxc" 412 + ".cache/nix" 413 + ".cache/nix-index" 414 + ]; 415 + }; 416 + }; 417 + }; 418 + @end 419 + 420 + But if you set this thing up, and rebuild it wouldn't do a thing. Remember, these options and list are just place holders. Meant to be easy to write and read. Now we gotta add them to an actual Impermanence module options. 421 + 422 + @code nix 423 + { lib, config, ... }: 424 + let 425 + cfg = config.custom.imp; 426 + # config.custom.meta.user is just my placeholder for `username` 427 + # just hard code the value, or replace it with your own solution to select a user 428 + cfghm = config.home-manager.users."${config.custom.meta.user}".custom.imp; 429 + cfghj = config.hjem.users."${config.custom.meta.user}".custom.imp; 430 + in 431 + { 432 + environment.persistence = { 433 + "/persist" = { 434 + hideMounts = true; 435 + # referencing files via our abstraction option 436 + files = lib.unique cfg.root.files; 437 + directories = lib.unique ( 438 + # here you can define directories normally 439 + [ 440 + "/var/log" 441 + "/var/lib/nixos" 442 + ] 443 + # and concatenate too! 444 + ++ cfg.root.directories 445 + ); 446 + # add persists to `/home/user` path 447 + users."${config.custom.meta.user}" = { 448 + files = lib.unique ([ ] ++ cfghm.home.files ++ cfghj.home.files); 449 + directories = lib.unique ( 450 + [ ] ++ cfg.home.directories ++ cfghm.home.directories ++ cfghj.home.directories 451 + ); 452 + }; 453 + }; 454 + # same as above 455 + "/cache" = { 456 + hideMounts = true; 457 + files = lib.unique cfg.root.cache.files; 458 + directories = lib.unique cfg.root.cache.directories; 459 + users."${config.custom.meta.user}" = { 460 + files = lib.unique (cfg.home.cache.files ++ cfghm.home.cache.files ++ cfghj.home.cache.files); 461 + directories = lib.unique ( 462 + cfg.home.cache.directories ++ cfghm.home.cache.directories ++ cfghj.home.cache.directories 463 + ); 464 + }; 465 + }; 466 + }; 467 + } 468 + @end 469 + 470 + You will need to adjust this code snippets to your own config. For example, if you don't use `hjem`. And replace `config.custom.meta.user` with your own username. 471 + 472 + I am not stating this approach is the best, but it just how I ended up using Impermanence module, and it might be useful for you to know. 473 + 474 + Now upon rebuild Impermanence module should add all the interesting directories to datasets and establish bind mounts. 475 + 476 + Wait, did we forget something? Uhhh... 477 + 478 + *** DELETE ERASE REDUCTED 479 + ___ 480 + @code nix 481 + # replace the root mount with tmpfs 482 + # wipes everything if you don't have proper persists, be warned 483 + fileSystems."/" = lib.mkForce { 484 + device = "tmpfs"; 485 + fsType = "tmpfs"; 486 + neededForBoot = true; 487 + options = [ 488 + "defaults" 489 + # whatever size feels comfortable, smaller is better 490 + "size=1G" 491 + "mode=755" 492 + ]; 493 + }; 494 + @end 495 + 496 + This is all you need. So simple in comparison to the whole page above, right? 497 + 498 + The main reasons for that are: 499 + - It's harder to keep what you have gained, but trivial to loose everything you ever had; 500 + - Also the `size=1G` wouldn't make it possible to use tmpfs as a main without persists and bind mounts. Bind mount only makes files accessible in 2 locations, but only stored in ZFS dataset. 501 + 502 + This should be all you need to start using tmpfs for impermanence on ZFS. 503 + 504 + There are some niceties I want to share tho, to make your life easier. 505 + 506 + *** Some sprinkles to your epitome of agony 507 + **** Snippets 508 + ___ 509 + Set this so you aren't lectured by `you know what you are doing` lecture from sudo every boot 510 + @code nix 511 + security.sudo.extraConfig = "Defaults lecture=never"; 512 + @end 513 + 514 + If you use {https://github.com/Mic92/sops-nix}[sops-nix], set ssh paths to `/persist` because otherwise `nixos-install` won't find the keys. 515 + @code nix 516 + sops.age.sshKeyPaths = [ 517 + "/persist/home/vimjoyer/.ssh/ssh-key" 518 + ]; 519 + sops.age.keyFile = lib.mkDefault "/persist/home/vimoyer/.config/sops/age/keys.txt"; 520 + @end 521 + 522 + **** Persist everything 523 + ___ 524 + Persist every bit that might be useful to you, tokens, cookies and all that if they matter to you. Depending on the application, you might wanna persist only one file. But for something like steam, it compiles shader cache, which is persistable with this snippet 525 + @code nix 526 + # persist steam 527 + custom.imp.home = { 528 + cache.directories = [ 529 + ".local/share/Steam" 530 + ".cache/mesa_shader_cache" 531 + ".cache/mesa_shader_cache_db" 532 + ".cache/radv_builtin_shaders" 533 + ]; 534 + }; 535 + @end 536 + 537 + But how would you know what to persist? Well, first you might want to look at others people config files. Because the best way to avoid pit falls is following the walked road. 538 + 539 + Some pretty extended persist list can be found in following configurations: 540 + - {https://github.com/search?q=repo%3Aiynaix%2Fdotfiles+custom.persist&type=code}[Iynaix dotfiles]; 541 + - {https://github.com/saygo-png/nixos}[Saygo's config]; 542 + - {https://github.com/xarvex/dotfyls}[Xarvex dotfyls]; 543 + 544 + If they don't use the same modules as you, figure it out on your own. Most of the time programs follow xdg conventions and store files in `.config .cache .local/state .local/share`. Or instead of persisting the settings, symlink raw files. 545 + 546 + **** Credits and suggestions 547 + ___ 548 + You can suggest anything you'd like to add to {*** Some sprinkles to your epitome of agony}. Cool configs using impermanence, tips, snippets and so on. Just ping me on Discord or write an issue on github. Your username will be added below as a privilege for being so awesome! 549 + 550 + List of awesome people: 551 + - *Iynaix*'s impermanence abstraction structure and ZFS setup 552 + - *Vimjoyer*'s Impermanence video introducing basic concept to me 553 + - *talyz* for making Impermanence module and extensive readme for the project 554 + - *Graham* for {https://grahamc.com/blog/erase-your-darlings/} 555 + - *Willbush* for {https://willbush.dev/blog/impermanent-nixos/} 556 + - *Elis Hirwing* for {https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/} 557 + - {https://github.com/fliplus}[Flipus] and *snohater* for providing feedback on readability of the thing. 558 + - *You* for reading this all, good luck with your NixOS config. Hope this article helped you the same way all the people mentioned above helped me. Without such a vast community, I wouldn't be able to figure all these things out. And I didn't reinvent a wheel, it's all the work of Open Source contributors. So, hopefully you also share your knowledge in the future. Improve on what's old or reduce it to atoms, it's up to you. It's your setup, and only for you to decide, whether you really want to setup it up.
+16
content/posts/Nixvim.norg
··· 1 + @document.meta 2 + title: Nixvim 3 + description: How to configure NixOS declaratively 4 + authors: ladas552 5 + categories: [ 6 + nixos 7 + configuration 8 + ] 9 + created: 2025-12-03T10:00:00+05:00 10 + updated: 2025-12-26T19:52:50 11 + layout: post 12 + draft: true 13 + version: 1.1.1 14 + @end 15 + 16 + * Nixvim
+101
content/posts/Nvfetcher.norg
··· 1 + @document.meta 2 + title: Nvfetcher 3 + description: Inputs outside of flakes 4 + authors: [ 5 + ladas552 6 + ] 7 + categories: [ 8 + inputs 9 + un-flake 10 + ] 11 + created: 2026-02-08 12 + draft: false 13 + layout: post 14 + version: 1.1.1 15 + @end 16 + 17 + ** What is Nvfetcher? 18 + It's a Haskell program that takes values from a toml file and generates fetched inputs that I can use in my config. Without having to manually update the Hash, but still remain locked on per commit basis. Same as `flake.lock`. 19 + ** Why don't you just use flakes? 20 + Even tho I love flakes, I can acknowledge that they have issues. Namely, in the context of inputs: 21 + - Unchanged `follows` for inputs will download unnecessary code, that in context of something big as nixpkgs can cost 40MiB of download data per input and per `nix flake update`; 22 + - You can't define inputs to download stuff only when needed. The `ghostty-shaders` input will be downloaded during update on both Desktop, a server and even on the phone. Can't decide whether it's needed there or not; 23 + - Inputs are hella slow. No, I am not benchmarking it, just feels like this. 24 + 25 + In this case, Nvfetcher comes in handy. Fetchers work only if you import the input. 26 + *** What about Npins? 27 + I don't like using cli interface to generate fetches. Also It has problems with AppImages. 28 + ** How to use Nvfetcher? 29 + Go read the docs in the {https://github.com/berberman/nvfetcher}[project's repository]. 30 + 31 + TLDR: define source and fetcher in toml file, 2 files are generated, you import nix file with `pkgs.callPackage`, or read json file directly with `builtins.fromJSON` and `builtins.readFile` if `pkgs` isn't available on the scope, or you want to import a fetched nix file inside of your nix module. 32 + 33 + For example, it works wonders for fetching neovim plugins. 34 + @code nix 35 + # nvfetcher pins 36 + sources = pkgs.callPackage "${self}/_sources/generated.nix" { }; 37 + heirline-components = pkgs.vimUtils.buildVimPlugin { 38 + name = "heirline-components.nvim"; 39 + doCheck = false; 40 + src = sources.heirline-components.src; 41 + }; 42 + @end 43 + 44 + Then I can call `heirline-components` in a package list to install it. I use `self` to avoid using relative paths, and keep my config modules independent of location. 45 + 46 + And here is a custom example of using the json file directly. 47 + 48 + @code nix 49 + sourcesJson = builtins.fromJSON (builtins.readFile ./_sources/generated.json); 50 + 51 + modules = builtins.mapAttrs ( 52 + name: value: 53 + let 54 + src = fetchTarball { 55 + url = "${value.src.url}/archive/${value.src.rev}.tar.gz"; 56 + sha256 = value.src.sha256; 57 + }; 58 + in 59 + value // { inherit src; } 60 + ) sourcesJson; 61 + @end 62 + 63 + This snippet is in my `flake.nix`. I use it to create a `specialArgs` in my config called `modules`. In any file I can add `modules` to the top and import some file from Nvfetcher inputs. 64 + 65 + Demonstration of one of my hosts that is NixOS-WSL based. 66 + 67 + @code nix 68 + { modules, ... }: 69 + { 70 + flake.modules.nixos.NixwsL = 71 + { config, ... }: 72 + { 73 + imports = [ 74 + "${modules.nixos-wsl.src}/modules" 75 + ]; 76 + networking.hostName = "NixwsL"; 77 + wsl = { 78 + enable = true; 79 + # if you are wondering what these config options are 80 + # it will be explained in another article 81 + defaultUser = "${config.custom.meta.user}"; 82 + startMenuLaunchers = true; 83 + tarball.configPath = "${config.custom.meta.self}"; 84 + usbip.enable = true; 85 + useWindowsDriver = true; 86 + }; 87 + 88 + system.stateVersion = "24.05"; # Do you like bunnies? 89 + 90 + nixpkgs.hostPlatform = "x86_64-linux"; 91 + }; 92 + } 93 + @end 94 + 95 + `"${modules.nixos-wsl.src}/modules"` is equivalent to importing `inputs.nixos-wsl.nixosModules.default`. But it will be fetched only if I use NixOS-WSL, and not on other hosts. 96 + ** Roll the credits 97 + Okay, that's about all I know and use Nvfetcher for, as always, Thanks goes to: 98 + - {https://github.com/berberman}[@berberman] - for creating and maintaining such a nice program; 99 + - {https://github.com/iynaix}[@iynaix] - for information that Nvfetcher exists; 100 + - {https://git.gay/sunworms}[@sunworms] - for sharing a way to parse json file directly; 101 + - *You* - for reading to the end, have a nice day and reproducible builds.
+15
content/posts/Rust-devshells.norg
··· 1 + @document.meta 2 + title: Rust Devshells 3 + description: Managing packages with Nix 4 + authors: ladas552 5 + categories: [ 6 + packages 7 + nix 8 + ] 9 + layout: post 10 + draft: true 11 + created: 2025-12-04 12 + version: 1.1.1 13 + @end 14 + 15 + * Rust Devshells
+21
content/posts/hjem.norg
··· 1 + @document.meta 2 + title: Hjem 3 + description: link your home and feel superiour 4 + authors: [ 5 + ladas552 6 + ] 7 + categories: [ 8 + Home 9 + linker 10 + dotfiles 11 + ] 12 + created: 2026-02-08T21:09:52+05:00 13 + updated: 2026-02-08T21:09:52+05:00 14 + draft: true 15 + layout: post 16 + version: 1.1.1 17 + @end 18 + 19 + * Hjem 20 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut 21 + labore et dolore magna aliqua. Lobortis scelerisque fermentum dui faucibus in ornare.
+5
content/posts/index.norg
··· 1 + @document.meta 2 + title: Posts 3 + description: All posts on this site 4 + layout: posts 5 + @end
+24
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1770197578, 6 + "narHash": "sha256-8JyM5HqkRXBXMnsIooEW0/hOoKrelRdj61VbGBV8tX4=", 7 + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", 8 + "type": "tarball", 9 + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre940249.00c21e4c93d9/nixexprs.tar.xz?lastModified=1770197578&rev=00c21e4c93d963c50d4c0c89bfa84ed6e0694df2" 10 + }, 11 + "original": { 12 + "type": "tarball", 13 + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" 14 + } 15 + }, 16 + "root": { 17 + "inputs": { 18 + "nixpkgs": "nixpkgs" 19 + } 20 + } 21 + }, 22 + "root": "root", 23 + "version": 7 24 + }
+44
flake.nix
··· 1 + { 2 + description = "Shell with norgolith"; 3 + 4 + inputs = { 5 + nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; 6 + }; 7 + 8 + outputs = 9 + { nixpkgs, ... }: 10 + { 11 + devShells = { 12 + x86_64-linux = 13 + let 14 + pkgs = nixpkgs.legacyPackages.x86_64-linux; 15 + pk = pkgs.writeShellScriptBin; 16 + in 17 + { 18 + default = pkgs.mkShell { 19 + packages = with pkgs; [ 20 + rustywind # Organize Tailwind CSS classes 21 + watchman # required by tailwindcss CLI for watch functionality 22 + tailwindcss_4 23 + tailwindcss-language-server 24 + mprocs # Run multiple commands in parallel 25 + 26 + norgolith 27 + (pk "serve" "lith dev --drafts") 28 + (pk "serveh" # bash 29 + '' 30 + wl-copy "http://$(ip route get 1 | awk '{print $7}'):3030" 31 + lith dev --drafts --host 32 + '' 33 + ) 34 + (pk "preview" "lith preview") 35 + (pk "build" "lith build -m") 36 + (pk "new" "lith new -k norg posts/$1") 37 + (pk "edit" "nvim ./content/posts") 38 + (pk "update" "nix flake update") 39 + ]; 40 + }; 41 + }; 42 + }; 43 + }; 44 + }
+6
mprocs.yaml
··· 1 + procs: 2 + norgolith: 3 + cmd: ["lith", "dev", "--drafts"] 4 + tailwind: 5 + cmd: ["tailwindcss", "-i", "theme/assets/css/tailwind.css", "-o", "theme/assets/css/styles.min.css", "--minify", "--watch"] 6 +
+28
norgolith.toml
··· 1 + rootUrl = 'https://nix.ladas552.me' 2 + language = 'en-US' 3 + title = 'Nix' 4 + author = 'ladas552' 5 + 6 + # Code blocks highlighting 7 + [highlighter] 8 + enable = true 9 + engine = 'prism' # Can be 'prism' or 'hljs'. Defaults to 'prism' 10 + 11 + # RSS feed 12 + [rss] 13 + enable = true 14 + description = 'Latest posts' 15 + ttl = 60 16 + image = '/assets/favicon.png' 17 + 18 + # Extras 19 + [extra] 20 + license = "GPLv3" 21 + favicon_path = '/assets/favicon.png' 22 + enable_mermaid = true 23 + 24 + [extra.nav] 25 + Tags = '/categories' 26 + 27 + [extra.footer] 28 + GitHub = 'https://github.com/Ladas552/Flake-Ocean'
+33
templates/rss.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> 3 + <channel> 4 + <title>{{ config.title }}</title> 5 + <link>{{ config.rootUrl | escape_xml | safe }}</link> 6 + <description>{{ config.rss.description | default(value="Latest posts")}}</description> 7 + <generator>Norgolith</generator> 8 + <language>{{ config.language }}</language> 9 + <lastBuildDate>{{ now | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate> 10 + <ttl>{{ config.rss.ttl | default(value=60) }}</ttl> 11 + <atom:link href="{{ config.rootUrl }}/rss.xml" rel="self" type="application/rss+xml" /> 12 + 13 + <image> 14 + <url>{{ config.rootUrl | escape_xml | safe }}{{ config.rss.image | default(value="/assets/favicon.png") }}</url> 15 + <title>{{ config.title }}</title> 16 + <link>{{ config.rootUrl | escape_xml | safe }}</link> 17 + <width>144</width> 18 + <height>144</height> 19 + </image> 20 + 21 + {% for post in posts | filter(attribute="draft", value=false) %} 22 + <item> 23 + <title>{{ post.title }}</title> 24 + <link>{{ post.permalink | escape_xml | safe }}</link> 25 + <guid>{{ post.permalink | escape_xml | safe }}</guid> 26 + <description>{{ post.description }}</description> 27 + <author>{{ post.authors }}</author> 28 + <pubDate>{{ post.created | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate> 29 + {% if post.categories %}{% for category in post.categories %}<category>{{ category }}</category>{% endfor %}{% endif %} 30 + </item> 31 + {% endfor %} 32 + </channel> 33 + </rss>
+3
theme/.metadata.toml
··· 1 + repo = "github:NTBBloodbath/norgowind" 2 + version = "0.3.0" 3 + pin = false
+97
theme/README.md
··· 1 + # Norgowind 2 + Norgolith <3 TailwindCSS. 3 + 4 + ## Installation 5 + ```bash 6 + lith theme pull github:NTBBloodbath/norgowind 7 + ``` 8 + 9 + > [!IMPORTANT] 10 + > 11 + > Currently, Norgowind requires the latest Norgolith commit in the master branch in order to work. 12 + 13 + ## Usage 14 + 15 + > [!TIP] 16 + > 17 + > As Norgowind has been written using the standalone TailwindCSS CLI, you might want to use it if 18 + > you plan to modify the CSS of the theme by hand. See the [Tailwind Reloading](#tailwind-reloading) section. 19 + 20 + ### Configuration 21 + Besides the default `norgolith.toml` configuration options, Norgowind theme also requires the following configuration fields to be present: 22 + 23 + ```toml 24 + # Custom additional configuration options with example values 25 + [extra] 26 + license = "GPLv2" # Optional 27 + favicon_path = "/assets/norgolith.svg" # Fallback to the default norgolith favicon 28 + footer_author_link = "https://github.com/NTBBloodbath" # Optional 29 + enable_mermaid = true # If you want to use Mermaid.js for diagrams and charts 30 + 31 + # Link_name = "url" 32 + # e.g. 33 + # blog = "/posts" 34 + # GitHub = "https://github.com/NTBBloodbath/norgolith" 35 + [extra.nav] 36 + 37 + # Link_name = "url" 38 + # GitHub = "https://github.com/NTBBloodbath/norgolith" 39 + [extra.footer] 40 + ``` 41 + 42 + ### Templates 43 + Norgowind provides the following templates: 44 + ``` 45 + templates 46 + ├── partials 47 + │ ├── footer.html <- Footer content 48 + │ └── nav.html <- Header navbar 49 + ├── base.html <- Main template which gets extended by any other template 50 + ├── categories.html <- Categories list 51 + ├── category.html <- Category posts list 52 + ├── default.html <- Default template for all content 53 + ├── home.html <- Homepage 54 + ├── post.html <- Blog post 55 + └── posts.html <- Posts list 56 + ``` 57 + 58 + In order to use a certain template, use the `layout` metadata field in your content files, e.g. if 59 + you are writing a blog post: 60 + ```norg 61 + layout: post 62 + ``` 63 + 64 + > [!TIP] 65 + > 66 + > Remember that Norgolith expects your blog posts to reside in the `content/posts` directory. 67 + 68 + ### Additional styling 69 + Norgowind adds certain additional styling classes for blockquotes (add them to your blockquotes 70 + using `+html.class` weak carryover tags): 71 + - `tip` (green) 72 + - `note` (blue) 73 + - `important` (violet) 74 + - `warning` (yellow) 75 + - `error` (red) 76 + 77 + ![blockquotes.png](https://github.com/user-attachments/assets/d45e2e97-5e3b-43cb-8077-a16f737259b9) 78 + 79 + ### Additional metadata fields 80 + Norgowind also accepts and uses the following opt-in content metadata: 81 + 82 + - `truncate`: configures the truncate characters length in the recent post cards. 83 + - `truncate_char`: configures the truncate character, do not define it to use the default ellipsis. Leave it empty to disable the truncate character. 84 + 85 + ### Tailwind Reloading 86 + By default, Tailwind's configuration in Norgowind will see content files, along with user and theme 87 + templates. Each new class added to content using a weak carryover tag `+html.class` will 88 + automatically be added to the styling file. 89 + 90 + It is highly recommended to have the TailwindCSS CLI installed and run the following command during 91 + development: 92 + ```sh 93 + tailwindcss -i theme/assets/css/tailwind.css -o theme/assets/css/styles.min.css --watch 94 + ``` 95 + 96 + ## License 97 + Norgowind is licensed under MIT license.
+76
theme/assets/css/prism-custom.css
··· 1 + /* Custom Prism enhancements - Font, Line Spacing, and Scrollbar */ 2 + /* This file adds improvements without changing Catppuccin colors */ 3 + 4 + /* Better line spacing and font - Terminal-like tight spacing */ 5 + pre[class*="language-"] { 6 + line-height: 1.2 !important; 7 + } 8 + 9 + pre>code[class*="language-"] { 10 + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace !important; 11 + line-height: 1.2 !important; 12 + } 13 + 14 + /* Remove Tab navigation - code blocks shouldn't be focusable */ 15 + pre[class*="language-"], 16 + code[class*="language-"] { 17 + outline: none !important; 18 + } 19 + 20 + pre[class*="language-"]:focus, 21 + code[class*="language-"]:focus { 22 + outline: none !important; 23 + } 24 + 25 + /* Prevent code blocks from being tabbable */ 26 + pre[class*="language-"] { 27 + -webkit-user-select: text; 28 + -moz-user-select: text; 29 + user-select: text; 30 + } 31 + 32 + /* Smaller, refined scrollbar for code blocks */ 33 + pre[class*="language-"]::-webkit-scrollbar { 34 + width: 6px; 35 + height: 6px; 36 + } 37 + 38 + pre[class*="language-"]::-webkit-scrollbar-track { 39 + background: transparent; 40 + border-radius: 3px; 41 + } 42 + 43 + pre[class*="language-"]::-webkit-scrollbar-thumb { 44 + background: rgba(0, 0, 0, 0.2); 45 + border-radius: 3px; 46 + transition: background-color 0.3s ease; 47 + } 48 + 49 + pre[class*="language-"]::-webkit-scrollbar-thumb:hover { 50 + background: rgba(0, 0, 0, 0.35); 51 + } 52 + 53 + /* Dark theme scrollbar adjustments */ 54 + html[class*="-dark"] pre[class*="language-"]::-webkit-scrollbar-thumb, 55 + html[class*="dark-"] pre[class*="language-"]::-webkit-scrollbar-thumb, 56 + .dark pre[class*="language-"]::-webkit-scrollbar-thumb { 57 + background: rgba(255, 255, 255, 0.2); 58 + } 59 + 60 + html[class*="-dark"] pre[class*="language-"]::-webkit-scrollbar-thumb:hover, 61 + html[class*="dark-"] pre[class*="language-"]::-webkit-scrollbar-thumb:hover, 62 + .dark pre[class*="language-"]::-webkit-scrollbar-thumb:hover { 63 + background: rgba(255, 255, 255, 0.35); 64 + } 65 + 66 + /* Firefox scrollbar styling */ 67 + pre[class*="language-"] { 68 + scrollbar-width: thin; 69 + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; 70 + } 71 + 72 + html[class*="-dark"] pre[class*="language-"], 73 + html[class*="dark-"] pre[class*="language-"], 74 + .dark pre[class*="language-"] { 75 + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; 76 + }
+383
theme/assets/css/prism-sweetie.css
··· 1 + /* Prism Sweetie Theme - Improved Code Block Styling */ 2 + 3 + /* Base code block styles */ 4 + pre[class*="language-"] { 5 + --padding-y: var(--am-prism-padding-y, 1rem); 6 + --padding-x: var(--am-prism-padding-x, 1rem); 7 + padding: var(--padding-y) var(--padding-x); 8 + overflow: auto; 9 + font-size: var(--am-prism-font-size, 0.85em); 10 + border-radius: var(--am-prism-border-radius, 0.4em); 11 + line-height: 1.6; 12 + /* Fixed: More consistent line spacing */ 13 + } 14 + 15 + /* Code inside pre blocks */ 16 + pre>code[class*="language-"] { 17 + padding: initial; 18 + font-size: 1em; 19 + font-weight: 400; 20 + font-family: var(--am-prism-font-family, "JetBrains Mono", "Fira Code", ui-monospace), monospace; 21 + line-height: 1.6; 22 + /* Fixed: Consistent line height */ 23 + background-color: initial; 24 + } 25 + 26 + /* General code/pre language settings */ 27 + code[class*="language-"], 28 + pre[class*="language-"] { 29 + text-align: left; 30 + white-space: pre; 31 + word-spacing: normal; 32 + word-break: normal; 33 + word-wrap: normal; 34 + -moz-tab-size: 4; 35 + -o-tab-size: 4; 36 + tab-size: 4; 37 + -webkit-hyphens: none; 38 + -moz-hyphens: none; 39 + -ms-hyphens: none; 40 + hyphens: none; 41 + } 42 + 43 + /* Line numbers */ 44 + pre[class*="language-"] .line-numbers-rows { 45 + box-sizing: content-box; 46 + margin: calc(var(--padding-y) * -1) 0; 47 + padding: var(--padding-y) 0; 48 + } 49 + 50 + .line-numbers.line-numbers .line-numbers-rows { 51 + border-right-width: var(--am-prism-border-width, 1px); 52 + border-right-color: var(--am-prism-border-color); 53 + } 54 + 55 + .line-numbers .line-numbers-rows>span:before { 56 + color: var(--am-prism-line-numbers-color); 57 + } 58 + 59 + /* Toolbar positioning */ 60 + div.code-toolbar>.toolbar { 61 + top: 0.3rem !important; 62 + right: 0.3rem !important; 63 + } 64 + 65 + /* Copy button styling */ 66 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button { 67 + display: inline-flex; 68 + padding: 0 0.75em; 69 + font-size: var(--am-prism-font-size, 0.8em); 70 + font-family: var(--am-prism-font-family, "JetBrains Mono", "Fira Code", ui-monospace), monospace; 71 + font-weight: 600 !important; 72 + line-height: 2.25em; 73 + color: var(--am-prism-copy-color); 74 + background-color: var(--am-prism-copy-bg); 75 + border-radius: calc(var(--am-prism-border-radius, 0.4em) - 0.1em); 76 + cursor: pointer; 77 + box-shadow: none; 78 + opacity: 1; 79 + transition: opacity 0.2s; 80 + } 81 + 82 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button:hover { 83 + opacity: 0.8; 84 + } 85 + 86 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button:focus { 87 + opacity: 1; 88 + } 89 + 90 + /* ==================== Light Theme ==================== */ 91 + code[class*="language-"], 92 + pre[class*="language-"] { 93 + color: #202023; 94 + } 95 + 96 + pre[class*="language-"] { 97 + background: #bbbbce; 98 + } 99 + 100 + :not(pre)>code[class*="language-"] { 101 + padding: 0.1em 0.3em; 102 + border-radius: 0.3em; 103 + color: #202023; 104 + background: #bbbbce; 105 + } 106 + 107 + /* Line highlighting */ 108 + pre[data-line] { 109 + position: relative; 110 + } 111 + 112 + pre[class*="language-"]>code[class*="language-"] { 113 + position: relative; 114 + z-index: 1; 115 + } 116 + 117 + .line-highlight { 118 + position: absolute; 119 + left: 0; 120 + right: 0; 121 + padding: inherit 0; 122 + margin-top: 1em; 123 + background: #fff8c5; 124 + box-shadow: inset 5px 0 0 #eed888; 125 + z-index: 0; 126 + pointer-events: none; 127 + line-height: inherit; 128 + white-space: pre; 129 + } 130 + 131 + /* Tokens - Light theme */ 132 + .namespace { 133 + opacity: 0.7; 134 + } 135 + 136 + .token.comment, 137 + .token.prolog, 138 + .token.doctype, 139 + .token.cdata { 140 + color: #8989a9; 141 + } 142 + 143 + .token.punctuation, 144 + .token.operator { 145 + color: #0c5090; 146 + } 147 + 148 + .token.property, 149 + .token.tag, 150 + .token.boolean, 151 + .token.number, 152 + .token.constant, 153 + .token.symbol, 154 + .token.deleted, 155 + .token.class-name { 156 + color: #ae580e; 157 + } 158 + 159 + .token.selector, 160 + .token.attr-name, 161 + .token.string, 162 + .token.char, 163 + .token.builtin, 164 + .token.inserted { 165 + color: #287f0d; 166 + } 167 + 168 + .token.entity, 169 + .token.url, 170 + .language-css .token.string, 171 + .style .token.string { 172 + color: #287f0d; 173 + } 174 + 175 + .token.atrule, 176 + .token.attr-value, 177 + .token.keyword { 178 + color: #0c5090; 179 + } 180 + 181 + .token.function { 182 + color: #a00c79; 183 + } 184 + 185 + .token.regex, 186 + .token.important, 187 + .token.variable, 188 + .token.macro { 189 + color: #9437ff; 190 + } 191 + 192 + .token.important, 193 + .token.bold { 194 + font-weight: 700; 195 + } 196 + 197 + .token.italic { 198 + font-style: italic; 199 + } 200 + 201 + .token.entity { 202 + cursor: help; 203 + } 204 + 205 + /* CSS Variables - Light theme */ 206 + :root { 207 + --am-prism-line-numbers-color: rgba(24, 24, 26, 0.47); 208 + --am-prism-border-color: rgba(24, 24, 26, 0.13); 209 + --am-prism-copy-color: rgba(32, 32, 35, 0.67); 210 + --am-prism-copy-bg: rgba(120, 120, 157, 0.13); 211 + } 212 + 213 + /* ==================== Dark Theme ==================== */ 214 + html[class*="-dark"], 215 + html[class*="dark-"], 216 + .dark { 217 + 218 + & code[class*="language-"], 219 + & pre[class*="language-"] { 220 + color: #d3d7de; 221 + } 222 + 223 + & pre[class*="language-"] { 224 + background: #303040; 225 + } 226 + 227 + & :not(pre)>code[class*="language-"] { 228 + padding: 0.1em 0.3em; 229 + border-radius: 0.3em; 230 + color: #bdbdbd; 231 + background: #303040; 232 + } 233 + 234 + & pre[data-line] { 235 + position: relative; 236 + } 237 + 238 + & pre[class*="language-"]>code[class*="language-"] { 239 + position: relative; 240 + z-index: 1; 241 + } 242 + 243 + & .line-highlight { 244 + position: absolute; 245 + left: 0; 246 + right: 0; 247 + padding: inherit 0; 248 + margin-top: 1em; 249 + background: #2f2a1e; 250 + box-shadow: inset 5px 0 0 #674c16; 251 + z-index: 0; 252 + pointer-events: none; 253 + line-height: inherit; 254 + white-space: pre; 255 + } 256 + 257 + & .namespace { 258 + opacity: 0.7; 259 + } 260 + 261 + & .token.comment, 262 + & .token.prolog, 263 + & .token.doctype, 264 + & .token.cdata { 265 + color: #798399; 266 + } 267 + 268 + & .token.punctuation, 269 + & .token.operator, 270 + & .token.variable { 271 + color: #73a3f3; 272 + } 273 + 274 + & .token.property, 275 + & .token.tag, 276 + & .token.boolean, 277 + & .token.number, 278 + & .token.constant, 279 + & .token.symbol, 280 + & .token.deleted, 281 + & .token.class-name { 282 + color: #e7a06a; 283 + } 284 + 285 + & .token.selector, 286 + & .token.attr-name, 287 + & .token.string, 288 + & .token.char, 289 + & .token.builtin, 290 + & .token.inserted { 291 + color: #89c252; 292 + } 293 + 294 + & .token.entity, 295 + & .token.url, 296 + & .language-css .token.string, 297 + & .style .token.string { 298 + color: #89c252; 299 + background: #161b22; 300 + } 301 + 302 + & .token.atrule, 303 + & .token.keyword { 304 + color: #73a3f3; 305 + } 306 + 307 + & .token.attr-value, 308 + & .token.function { 309 + color: #d087e8; 310 + } 311 + 312 + & .token.regex, 313 + & .token.important, 314 + & .token.macro { 315 + color: #b094e2; 316 + } 317 + 318 + & .token.important, 319 + & .token.bold { 320 + font-weight: 700; 321 + } 322 + 323 + & .token.italic { 324 + font-style: italic; 325 + } 326 + 327 + & .token.entity { 328 + cursor: help; 329 + } 330 + 331 + /* CSS Variables - Dark theme */ 332 + --am-prism-line-numbers-color: rgba(189, 189, 189, 0.33); 333 + --am-prism-border-color: rgba(189, 189, 189, 0.13); 334 + --am-prism-copy-color: #d3d7de; 335 + --am-prism-copy-bg: rgba(112, 123, 135, 0.13); 336 + } 337 + 338 + /* ==================== Custom Scrollbar Styling ==================== */ 339 + /* Fixed: Smaller, more refined scrollbar */ 340 + pre[class*="language-"]::-webkit-scrollbar { 341 + width: 6px; 342 + height: 6px; 343 + } 344 + 345 + pre[class*="language-"]::-webkit-scrollbar-track { 346 + background: transparent; 347 + border-radius: 3px; 348 + } 349 + 350 + pre[class*="language-"]::-webkit-scrollbar-thumb { 351 + background: rgba(0, 0, 0, 0.2); 352 + border-radius: 3px; 353 + transition: background-color 0.3s ease; 354 + } 355 + 356 + pre[class*="language-"]::-webkit-scrollbar-thumb:hover { 357 + background: rgba(0, 0, 0, 0.35); 358 + } 359 + 360 + /* Dark theme scrollbar adjustments */ 361 + html[class*="-dark"] pre[class*="language-"]::-webkit-scrollbar-thumb, 362 + html[class*="dark-"] pre[class*="language-"]::-webkit-scrollbar-thumb, 363 + .dark pre[class*="language-"]::-webkit-scrollbar-thumb { 364 + background: rgba(255, 255, 255, 0.2); 365 + } 366 + 367 + html[class*="-dark"] pre[class*="language-"]::-webkit-scrollbar-thumb:hover, 368 + html[class*="dark-"] pre[class*="language-"]::-webkit-scrollbar-thumb:hover, 369 + .dark pre[class*="language-"]::-webkit-scrollbar-thumb:hover { 370 + background: rgba(255, 255, 255, 0.35); 371 + } 372 + 373 + /* Firefox scrollbar styling */ 374 + pre[class*="language-"] { 375 + scrollbar-width: thin; 376 + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; 377 + } 378 + 379 + html[class*="-dark"] pre[class*="language-"], 380 + html[class*="dark-"] pre[class*="language-"], 381 + .dark pre[class*="language-"] { 382 + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; 383 + }
+365
theme/assets/css/prism-sweetie.min.css
··· 1 + pre[class*=language-] { 2 + --padding-y: var(--am-prism-padding-y, 1rem); 3 + --padding-x: var(--am-prism-padding-x, 1rem); 4 + padding: var(--padding-y) var(--padding-x); 5 + overflow: auto; 6 + font-size: var(--am-prism-font-size, .85em); 7 + border-radius: var(--am-prism-border-radius, .4em); 8 + line-height: 1.6 9 + } 10 + 11 + pre>code[class*=language-] { 12 + padding: initial; 13 + font-size: 1em; 14 + font-weight: 400; 15 + font-family: var(--am-prism-font-family, "JetBrains Mono", "Fira Code", ui-monospace), monospace; 16 + line-height: 1.6; 17 + background-color: initial 18 + } 19 + 20 + code[class*=language-], 21 + pre[class*=language-] { 22 + text-align: left; 23 + white-space: pre; 24 + word-spacing: normal; 25 + word-break: normal; 26 + word-wrap: normal; 27 + -moz-tab-size: 4; 28 + -o-tab-size: 4; 29 + tab-size: 4; 30 + -webkit-hyphens: none; 31 + -moz-hyphens: none; 32 + -ms-hyphens: none; 33 + hyphens: none 34 + } 35 + 36 + pre[class*=language-] .line-numbers-rows { 37 + box-sizing: content-box; 38 + margin: calc(var(--padding-y) * -1) 0; 39 + padding: var(--padding-y) 0 40 + } 41 + 42 + .line-numbers.line-numbers .line-numbers-rows { 43 + border-right-width: var(--am-prism-border-width, 1px); 44 + border-right-color: var(--am-prism-border-color) 45 + } 46 + 47 + .line-numbers .line-numbers-rows>span:before { 48 + color: var(--am-prism-line-numbers-color) 49 + } 50 + 51 + div.code-toolbar>.toolbar { 52 + top: .3rem !important; 53 + right: .3rem !important 54 + } 55 + 56 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button { 57 + display: inline-flex; 58 + padding: 0 .75em; 59 + font-size: var(--am-prism-font-size, .8em); 60 + font-family: var(--am-prism-font-family, "JetBrains Mono", "Fira Code", ui-monospace), monospace; 61 + font-weight: 600 !important; 62 + line-height: 2.25em; 63 + color: var(--am-prism-copy-color); 64 + background-color: var(--am-prism-copy-bg); 65 + border-radius: calc(var(--am-prism-border-radius, .4em) - .1em); 66 + cursor: pointer; 67 + box-shadow: none; 68 + opacity: 1; 69 + transition: opacity .2s 70 + } 71 + 72 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button:hover { 73 + opacity: .8 74 + } 75 + 76 + div.code-toolbar>.toolbar>.toolbar-item>button.copy-to-clipboard-button:focus { 77 + opacity: 1 78 + } 79 + 80 + code[class*=language-], 81 + pre[class*=language-] { 82 + color: #202023 83 + } 84 + 85 + pre[class*=language-] { 86 + background: #bbbbce 87 + } 88 + 89 + :not(pre)>code[class*=language-] { 90 + padding: .1em .3em; 91 + border-radius: .3em; 92 + color: #202023; 93 + background: #bbbbce 94 + } 95 + 96 + pre[data-line] { 97 + position: relative 98 + } 99 + 100 + pre[class*=language-]>code[class*=language-] { 101 + position: relative; 102 + z-index: 1 103 + } 104 + 105 + .line-highlight { 106 + position: absolute; 107 + left: 0; 108 + right: 0; 109 + padding: inherit 0; 110 + margin-top: 1em; 111 + background: #fff8c5; 112 + box-shadow: inset 5px 0 0 #eed888; 113 + z-index: 0; 114 + pointer-events: none; 115 + line-height: inherit; 116 + white-space: pre 117 + } 118 + 119 + .namespace { 120 + opacity: .7 121 + } 122 + 123 + .token.cdata, 124 + .token.comment, 125 + .token.doctype, 126 + .token.prolog { 127 + color: #8989a9 128 + } 129 + 130 + .token.operator, 131 + .token.punctuation { 132 + color: #0c5090 133 + } 134 + 135 + .token.boolean, 136 + .token.constant, 137 + .token.deleted, 138 + .token.number, 139 + .token.property, 140 + .token.class-name, 141 + .token.symbol, 142 + .token.tag { 143 + color: #ae580e 144 + } 145 + 146 + .token.attr-name, 147 + .token.builtin, 148 + .token.char, 149 + .token.inserted, 150 + .token.selector, 151 + .token.string { 152 + color: #287f0d 153 + } 154 + 155 + .language-css .token.string, 156 + .style .token.string, 157 + .token.entity, 158 + .token.url { 159 + color: #287f0d 160 + } 161 + 162 + .token.atrule, 163 + .token.attr-value, 164 + .token.keyword { 165 + color: #0c5090 166 + } 167 + 168 + .token.function { 169 + color: #a00c79 170 + } 171 + 172 + .token.macro, 173 + .token.important, 174 + .token.regex, 175 + .token.variable { 176 + color: #9437ff 177 + } 178 + 179 + .token.bold, 180 + .token.important { 181 + font-weight: 700 182 + } 183 + 184 + .token.italic { 185 + font-style: italic 186 + } 187 + 188 + .token.entity { 189 + cursor: help 190 + } 191 + 192 + :root { 193 + --am-prism-line-numbers-color: #18181a77; 194 + --am-prism-border-color: #18181a22; 195 + --am-prism-copy-color: #202023aa; 196 + --am-prism-copy-bg: #78789d22 197 + } 198 + 199 + html[class*="-dark"], 200 + html[class*="dark-"], 201 + .dark { 202 + 203 + & code[class*=language-], 204 + & pre[class*=language-] { 205 + color: #d3d7de 206 + } 207 + 208 + & pre[class*=language-] { 209 + background: #303040 210 + } 211 + 212 + & :not(pre)>code[class*=language-] { 213 + padding: .1em .3em; 214 + border-radius: .3em; 215 + color: #bdbdbd; 216 + background: #303040 217 + } 218 + 219 + & pre[data-line] { 220 + position: relative 221 + } 222 + 223 + & pre[class*=language-]>code[class*=language-] { 224 + position: relative; 225 + z-index: 1 226 + } 227 + 228 + & .line-highlight { 229 + position: absolute; 230 + left: 0; 231 + right: 0; 232 + padding: inherit 0; 233 + margin-top: 1em; 234 + background: #2f2a1e; 235 + box-shadow: inset 5px 0 0 #674c16; 236 + z-index: 0; 237 + pointer-events: none; 238 + line-height: inherit; 239 + white-space: pre 240 + } 241 + 242 + & .namespace { 243 + opacity: .7 244 + } 245 + 246 + & .token.cdata, 247 + & .token.comment, 248 + & .token.doctype, 249 + & .token.prolog { 250 + color: #798399 251 + } 252 + 253 + & .token.punctuation, 254 + & .token.operator, 255 + & .token.variable { 256 + color: #73a3f3 257 + } 258 + 259 + & .token.boolean, 260 + & .token.constant, 261 + & .token.deleted, 262 + & .token.number, 263 + & .token.property, 264 + & .token.symbol, 265 + & .token.class-name, 266 + & .token.tag { 267 + color: #e7a06a 268 + } 269 + 270 + & .token.attr-name, 271 + & .token.builtin, 272 + & .token.char, 273 + & .token.inserted, 274 + & .token.selector, 275 + & .token.string { 276 + color: #89c252 277 + } 278 + 279 + & .language-css .token.string, 280 + & .style .token.string, 281 + & .token.entity, 282 + & .token.url { 283 + color: #89c252; 284 + background: #161b22 285 + } 286 + 287 + & .token.atrule, 288 + & .token.keyword { 289 + color: #73a3f3 290 + } 291 + 292 + & .token.attr-value, 293 + & .token.function { 294 + color: #d087e8 295 + } 296 + 297 + & .token.macro, 298 + & .token.important, 299 + & .token.regex { 300 + color: #b094e2 301 + } 302 + 303 + & .token.bold, 304 + & .token.important { 305 + font-weight: 700 306 + } 307 + 308 + & .token.italic { 309 + font-style: italic 310 + } 311 + 312 + & .token.entity { 313 + cursor: help 314 + } 315 + 316 + & { 317 + --am-prism-line-numbers-color: #bdbdbd55; 318 + --am-prism-border-color: #bdbdbd22; 319 + --am-prism-copy-color: #d3d7de; 320 + --am-prism-copy-bg: #707b8722 321 + } 322 + } 323 + 324 + pre[class*=language-]::-webkit-scrollbar { 325 + width: 6px; 326 + height: 6px 327 + } 328 + 329 + pre[class*=language-]::-webkit-scrollbar-track { 330 + background: transparent; 331 + border-radius: 3px 332 + } 333 + 334 + pre[class*=language-]::-webkit-scrollbar-thumb { 335 + background: rgba(0, 0, 0, .2); 336 + border-radius: 3px; 337 + transition: background-color .3s ease 338 + } 339 + 340 + pre[class*=language-]::-webkit-scrollbar-thumb:hover { 341 + background: rgba(0, 0, 0, .35) 342 + } 343 + 344 + html[class*="-dark"] pre[class*=language-]::-webkit-scrollbar-thumb, 345 + html[class*="dark-"] pre[class*=language-]::-webkit-scrollbar-thumb, 346 + .dark pre[class*=language-]::-webkit-scrollbar-thumb { 347 + background: rgba(255, 255, 255, .2) 348 + } 349 + 350 + html[class*="-dark"] pre[class*=language-]::-webkit-scrollbar-thumb:hover, 351 + html[class*="dark-"] pre[class*=language-]::-webkit-scrollbar-thumb:hover, 352 + .dark pre[class*=language-]::-webkit-scrollbar-thumb:hover { 353 + background: rgba(255, 255, 255, .35) 354 + } 355 + 356 + pre[class*=language-] { 357 + scrollbar-width: thin; 358 + scrollbar-color: rgba(0, 0, 0, .2) transparent 359 + } 360 + 361 + html[class*="-dark"] pre[class*=language-], 362 + html[class*="dark-"] pre[class*=language-], 363 + .dark pre[class*=language-] { 364 + scrollbar-color: rgba(255, 255, 255, .2) transparent 365 + }
+2
theme/assets/css/styles.min.css
··· 1 + /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ 2 + @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:"Geist Sans",ui-sans-serif,system-ui,sans-serif;--font-mono:"JetBrains Mono","Fira Code",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--spacing:.25rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.static{position:static}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.h-full{height:100%}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.items-center{align-items:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.px-4{padding-inline:calc(var(--spacing)*4)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (min-width:48rem){.md\:ml-8{margin-left:calc(var(--spacing)*8)}.md\:flex{display:flex}.md\:hidden{display:none}.md\:items-center{align-items:center}.md\:gap-6{gap:calc(var(--spacing)*6)}}}:root{--bg:#dddde7;--bg-secondary:#ccccda;--text:#202023;--text-muted:#78789d;--border:#bbbbce;--link:#0c5090;--link-hover:#0b658e;--code-bg:#bbbbce;--sidebar-bg:#ccccda;--sidebar-active:#0c5090;--hr-border:#9999b8;--accent-red:#d20f39;--space-xs:.5em;--space-sm:1em;--space-md:1.5em;--space-lg:3em;--shadow-card:0 4px 12px #0000001a}:root.dark{--bg:#24273a;--bg-secondary:#1e2030;--text:#cad3f5;--text-muted:#a5adcb;--border:#363a4f;--link:#8aadf4;--link-hover:#7dc4e4;--code-bg:#363a4f;--sidebar-bg:#1e2030;--sidebar-active:#8aadf4;--hr-border:#494d64;--accent-red:#ed8796;--shadow-card:0 4px 16px #0000004d}::view-transition-old(root){mix-blend-mode:normal;animation:none}::view-transition-new(root){mix-blend-mode:normal;animation:none}::view-transition-old(root){z-index:1}::view-transition-new(root){z-index:999;animation:.5s ease-in-out scale-up}@keyframes scale-up{0%{clip-path:circle(0% at var(--x,50%)var(--y,50%))}to{clip-path:circle(150% at var(--x,50%)var(--y,50%))}}html{scroll-behavior:smooth;font-family:var(--font-sans)}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;line-height:1.6}h1,h2,h3,h4,h5,h6{color:var(--text);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.3}h1{margin-top:0;font-size:2em}h2{font-size:1.5em}h3{font-size:1.25em}h4{font-size:1.1em}h5{font-size:1em;font-weight:600}h6{font-size:1em;font-weight:500}p{margin:1em 0}a{color:var(--link);text-decoration:none;transition:color .15s}a:hover{color:var(--link-hover);text-decoration:underline}strong,b{color:var(--accent-red);font-weight:600}.page-wrapper{min-height:100vh;display:flex}.sidebar{background:var(--sidebar-bg);border-right:1px solid var(--border);width:300px;transition:width .3s,transform .3s;position:fixed;top:50px;bottom:0;left:0;overflow:hidden auto}.sidebar.collapsed{width:50px}.content-wrapper{flex:1;margin-left:300px;padding-top:50px;transition:margin-left .3s}.content-wrapper.sidebar-collapsed{margin-left:50px}@media (max-width:768px){.sidebar{transition:transform .3s;transform:translate(-100%);width:300px!important}.sidebar.open{transform:translate(0)}.sidebar.collapsed{width:300px!important}.content-wrapper{margin-left:0!important}}header{background:var(--bg);border-bottom:1px solid var(--border);z-index:100;align-items:center;height:50px;padding:0 15px;display:flex;position:fixed;top:0;left:0;right:0}.header-title{color:var(--text);margin-left:10px;font-size:1.2em;font-weight:600}.header-title:hover{text-decoration:none}.nav-link{color:var(--text);font-size:.95em;text-decoration:none;transition:color .15s}.nav-link:hover{color:var(--link);text-decoration:none}.nav-link.active-nav{color:var(--link);font-weight:600}.mobile-menu{background:var(--bg);border-bottom:1px solid var(--border);z-index:60;padding:10px 15px;position:absolute;top:50px;left:0;right:0;box-shadow:0 2px 4px #0000001a}.mobile-menu a{color:var(--text);border-radius:3px;padding:8px 10px;text-decoration:none;display:block}.mobile-menu a:hover{background:var(--bg-secondary)}.mobile-menu a.active{color:var(--link);font-weight:600}.menu-btn{cursor:pointer;width:30px;height:30px;color:var(--text);background:0 0;border:none;padding:6px}.theme-toggle{cursor:pointer;width:30px;height:30px;color:var(--text);background:0 0;border:none;margin-left:auto;padding:6px}code{font-family:var(--font-mono);background:var(--code-bg);border-radius:3px;padding:2px 6px;font-size:.9em}pre{background:var(--code-bg);border:1px solid var(--border);border-radius:3px;margin:1.5em 0;padding:1em;overflow-x:auto}pre code{background:0 0;padding:0;font-size:.875em;line-height:1.5}blockquote{border-left:4px solid var(--border);background:var(--bg-secondary);color:var(--text-muted);margin:1.5em 0;padding:.5em 1em}blockquote p{margin:.5em 0}ul{list-style-type:disc}ol{list-style-type:decimal}ul,ol{margin:1em 0;padding-left:2em}li{margin:.5em 0}table{border-collapse:collapse;width:100%;margin:1.5em 0}th,td{border:1px solid var(--border);text-align:left;padding:8px 12px}th{background:var(--bg-secondary);font-weight:600}.sidebar nav{padding:15px}.nav-section{margin-bottom:1.5em}.nav-section h3{text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin:0 0 .5em;padding:0 12px;font-size:.75em;font-weight:600}.sidebar.collapsed .nav-section h3{opacity:0;visibility:hidden}.sidebar nav a{color:var(--text);white-space:nowrap;text-overflow:ellipsis;border-radius:3px;padding:8px 12px;text-decoration:none;transition:background .15s;display:block;overflow:hidden}.sidebar.collapsed nav a{text-indent:-9999px;padding:8px}.sidebar nav a:hover{background:var(--bg)}.sidebar nav a.active{color:var(--sidebar-active);background:var(--bg);font-weight:600}.sidebar-toggle{background:var(--sidebar-bg);border:1px solid var(--border);cursor:pointer;width:30px;height:30px;color:var(--text);z-index:10;border-radius:50%;justify-content:center;align-items:center;transition:background .15s;display:flex;position:absolute;top:50%;right:-15px;transform:translateY(-50%)}.sidebar-toggle:hover{background:var(--bg)}.sidebar-toggle svg{width:16px;height:16px}.content{box-sizing:border-box;overflow-wrap:break-word;word-wrap:break-word;max-width:min(750px,100vw - 40px);margin:0 auto;padding:20px 15px}.content *{box-sizing:border-box;max-width:100%}.content img,.content video,.content iframe{height:auto}.content table,.content pre{display:block;overflow-x:auto}@media (max-width:768px){.content{word-break:break-word;-webkit-hyphens:auto;hyphens:auto;padding:15px 10px}}.doc-grid{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.5em;margin-top:1.5em;display:grid}.doc-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:1.5em;text-decoration:none;transition:transform .2s ease-out,box-shadow .2s ease-out;display:block}.doc-card:hover{box-shadow:var(--shadow-card);text-decoration:none;transform:translateY(-4px)}.doc-card h3{color:var(--link);margin:0 0 .5em;font-size:1.1em}.doc-card p{color:var(--text-muted);margin:0;font-size:.9em;line-height:1.5}.doc-tags{flex-wrap:wrap;gap:.5em;margin-top:1em;display:flex}.doc-tag{background:var(--code-bg);color:var(--text-muted);border-radius:12px;padding:.25em .75em;font-size:.75em}.text-muted{color:var(--text-muted)}.text-muted-sm{color:var(--text-muted);font-size:.9em}.page-description{color:var(--text-muted);font-style:italic}.post-meta{color:var(--text-muted);font-size:.9em}.post-list-item{margin:var(--space-md)0}.post-separator{margin:var(--space-md)0;padding-bottom:var(--space-sm);border-bottom:1px solid var(--border)}.post-title-inline{margin-top:0}.tag-spacing{margin-left:var(--space-sm)}.section-top{margin-top:var(--space-lg)}.sidebar-empty{color:var(--text-muted);padding:0 12px;font-size:.9em}.post-date{margin:var(--space-xs)0}hr{border:none;border-top:1px solid var(--hr-border);margin:2em 0}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0;transition:background-color .3s}::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px;transition:background-color .3s}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}footer{border-top:1px solid var(--border);text-align:center;color:var(--text-muted);background:var(--bg);padding:15px;font-size:.9em}footer a{color:var(--link)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
+695
theme/assets/css/tailwind.css
··· 1 + @import "tailwindcss"; 2 + 3 + /* ============================================ 4 + mdBook-Style Theme - Clean & Minimal 5 + ============================================ */ 6 + 7 + @theme { 8 + /* Typography */ 9 + --font-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif; 10 + --font-mono: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 11 + 12 + /* Simple spacing */ 13 + --spacing: 0.25rem; 14 + } 15 + 16 + /* ============================================ 17 + COLOR SCHEME - Catppuccin Macchiato 18 + ============================================ */ 19 + 20 + :root { 21 + /* Light Mode - Sweetie */ 22 + --bg: #dddde7; 23 + --bg-secondary: #ccccda; 24 + --text: #202023; 25 + --text-muted: #78789d; 26 + --border: #bbbbce; 27 + --link: #0c5090; 28 + --link-hover: #0b658e; 29 + --code-bg: #bbbbce; 30 + --sidebar-bg: #ccccda; 31 + --sidebar-active: #0c5090; 32 + --hr-border: #9999b8; 33 + --accent-red: #d20f39; 34 + 35 + /* Spacing scale for consistency */ 36 + --space-xs: 0.5em; 37 + --space-sm: 1em; 38 + --space-md: 1.5em; 39 + --space-lg: 3em; 40 + 41 + /* Shadows */ 42 + --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.1); 43 + } 44 + 45 + :root.dark { 46 + /* Dark Mode - Catppuccin Macchiato */ 47 + --bg: #24273a; 48 + --bg-secondary: #1e2030; 49 + --text: #cad3f5; 50 + --text-muted: #a5adcb; 51 + --border: #363a4f; 52 + --link: #8aadf4; 53 + --link-hover: #7dc4e4; 54 + --code-bg: #363a4f; 55 + --sidebar-bg: #1e2030; 56 + --sidebar-active: #8aadf4; 57 + --hr-border: #494d64; 58 + --accent-red: #ed8796; 59 + 60 + /* Dark mode shadow - more subtle */ 61 + --shadow-card: 0 4px 16px rgba(0, 0, 0, 0.3); 62 + } 63 + 64 + /* ============================================ 65 + VIEW TRANSITIONS - Radial Theme Expansion 66 + ============================================ */ 67 + 68 + ::view-transition-old(root), 69 + ::view-transition-new(root) { 70 + animation: none; 71 + mix-blend-mode: normal; 72 + } 73 + 74 + ::view-transition-old(root) { 75 + z-index: 1; 76 + } 77 + 78 + ::view-transition-new(root) { 79 + z-index: 999; 80 + animation: scale-up 0.5s ease-in-out; 81 + } 82 + 83 + @keyframes scale-up { 84 + from { 85 + clip-path: circle(0% at var(--x, 50%) var(--y, 50%)); 86 + } 87 + 88 + to { 89 + clip-path: circle(150% at var(--x, 50%) var(--y, 50%)); 90 + } 91 + } 92 + 93 + /* ============================================ 94 + BASE STYLES 95 + ============================================ */ 96 + 97 + html { 98 + scroll-behavior: smooth; 99 + font-family: var(--font-sans); 100 + } 101 + 102 + body { 103 + background: var(--bg); 104 + color: var(--text); 105 + font-size: 16px; 106 + line-height: 1.6; 107 + -webkit-font-smoothing: antialiased; 108 + -moz-osx-font-smoothing: grayscale; 109 + } 110 + 111 + /* ============================================ 112 + TYPOGRAPHY - mdBook Style 113 + ============================================ */ 114 + 115 + h1, 116 + h2, 117 + h3, 118 + h4, 119 + h5, 120 + h6 { 121 + font-weight: 600; 122 + line-height: 1.3; 123 + margin-top: 1.5em; 124 + margin-bottom: 0.5em; 125 + color: var(--text); 126 + } 127 + 128 + h1 { 129 + font-size: 2em; 130 + margin-top: 0; 131 + } 132 + 133 + h2 { 134 + font-size: 1.5em; 135 + } 136 + 137 + h3 { 138 + font-size: 1.25em; 139 + } 140 + 141 + h4 { 142 + font-size: 1.1em; 143 + } 144 + 145 + h5 { 146 + font-size: 1em; 147 + font-weight: 600; 148 + } 149 + 150 + h6 { 151 + font-size: 1em; 152 + font-weight: 500; 153 + } 154 + 155 + p { 156 + margin: 1em 0; 157 + } 158 + 159 + a { 160 + color: var(--link); 161 + text-decoration: none; 162 + transition: color 0.15s ease; 163 + } 164 + 165 + a:hover { 166 + color: var(--link-hover); 167 + text-decoration: underline; 168 + } 169 + 170 + strong, 171 + b { 172 + font-weight: 600; 173 + color: var(--accent-red); 174 + } 175 + 176 + /* ============================================ 177 + LAYOUT - mdBook inspired 178 + ============================================ */ 179 + 180 + .page-wrapper { 181 + display: flex; 182 + min-height: 100vh; 183 + } 184 + 185 + .sidebar { 186 + width: 300px; 187 + background: var(--sidebar-bg); 188 + border-right: 1px solid var(--border); 189 + overflow-y: auto; 190 + overflow-x: hidden; 191 + position: fixed; 192 + top: 50px; 193 + bottom: 0; 194 + left: 0; 195 + transition: width 0.3s ease, transform 0.3s ease; 196 + } 197 + 198 + .sidebar.collapsed { 199 + width: 50px; 200 + } 201 + 202 + .content-wrapper { 203 + margin-left: 300px; 204 + flex: 1; 205 + padding-top: 50px; 206 + transition: margin-left 0.3s ease; 207 + } 208 + 209 + .content-wrapper.sidebar-collapsed { 210 + margin-left: 50px; 211 + } 212 + 213 + /* Mobile */ 214 + @media (max-width: 768px) { 215 + .sidebar { 216 + transform: translateX(-100%); 217 + transition: transform 0.3s ease; 218 + width: 300px !important; 219 + } 220 + 221 + .sidebar.open { 222 + transform: translateX(0); 223 + } 224 + 225 + .sidebar.collapsed { 226 + width: 300px !important; 227 + } 228 + 229 + .content-wrapper { 230 + margin-left: 0 !important; 231 + } 232 + } 233 + 234 + /* ============================================ 235 + HEADER 236 + ============================================ */ 237 + 238 + header { 239 + position: fixed; 240 + top: 0; 241 + left: 0; 242 + right: 0; 243 + height: 50px; 244 + background: var(--bg); 245 + border-bottom: 1px solid var(--border); 246 + display: flex; 247 + align-items: center; 248 + padding: 0 15px; 249 + z-index: 100; 250 + } 251 + 252 + .header-title { 253 + font-size: 1.2em; 254 + font-weight: 600; 255 + color: var(--text); 256 + margin-left: 10px; 257 + } 258 + 259 + .header-title:hover { 260 + text-decoration: none; 261 + } 262 + 263 + /* Header Navigation Links */ 264 + .nav-link { 265 + color: var(--text); 266 + text-decoration: none; 267 + font-size: 0.95em; 268 + transition: color 0.15s ease; 269 + } 270 + 271 + .nav-link:hover { 272 + color: var(--link); 273 + text-decoration: none; 274 + } 275 + 276 + .nav-link.active-nav { 277 + color: var(--link); 278 + font-weight: 600; 279 + } 280 + 281 + /* Mobile Menu Dropdown */ 282 + .mobile-menu { 283 + position: absolute; 284 + top: 50px; 285 + left: 0; 286 + right: 0; 287 + background: var(--bg); 288 + border-bottom: 1px solid var(--border); 289 + padding: 10px 15px; 290 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 291 + z-index: 60; 292 + } 293 + 294 + .mobile-menu a { 295 + display: block; 296 + padding: 8px 10px; 297 + color: var(--text); 298 + text-decoration: none; 299 + border-radius: 3px; 300 + } 301 + 302 + .mobile-menu a:hover { 303 + background: var(--bg-secondary); 304 + } 305 + 306 + .mobile-menu a.active { 307 + color: var(--link); 308 + font-weight: 600; 309 + } 310 + 311 + .menu-btn { 312 + width: 30px; 313 + height: 30px; 314 + padding: 6px; 315 + cursor: pointer; 316 + color: var(--text); 317 + background: none; 318 + border: none; 319 + } 320 + 321 + .theme-toggle { 322 + margin-left: auto; 323 + width: 30px; 324 + height: 30px; 325 + padding: 6px; 326 + cursor: pointer; 327 + color: var(--text); 328 + background: none; 329 + border: none; 330 + } 331 + 332 + /* ============================================ 333 + CODE BLOCKS - Clean mdBook style 334 + ============================================ */ 335 + 336 + code { 337 + font-family: var(--font-mono); 338 + font-size: 0.9em; 339 + background: var(--code-bg); 340 + padding: 2px 6px; 341 + border-radius: 3px; 342 + } 343 + 344 + pre { 345 + background: var(--code-bg); 346 + padding: 1em; 347 + border-radius: 3px; 348 + overflow-x: auto; 349 + margin: 1.5em 0; 350 + border: 1px solid var(--border); 351 + } 352 + 353 + pre code { 354 + background: none; 355 + padding: 0; 356 + font-size: 0.875em; 357 + line-height: 1.5; 358 + } 359 + 360 + /* ============================================ 361 + BLOCKQUOTES 362 + ============================================ */ 363 + 364 + blockquote { 365 + margin: 1.5em 0; 366 + padding: 0.5em 1em; 367 + border-left: 4px solid var(--border); 368 + background: var(--bg-secondary); 369 + color: var(--text-muted); 370 + } 371 + 372 + blockquote p { 373 + margin: 0.5em 0; 374 + } 375 + 376 + /* ============================================ 377 + LISTS 378 + ============================================ */ 379 + 380 + ul { 381 + list-style-type: disc; 382 + } 383 + 384 + ol { 385 + list-style-type: decimal; 386 + } 387 + 388 + ul, 389 + ol { 390 + margin: 1em 0; 391 + padding-left: 2em; 392 + } 393 + 394 + li { 395 + margin: 0.5em 0; 396 + } 397 + 398 + /* ============================================ 399 + TABLES 400 + ============================================ */ 401 + 402 + table { 403 + border-collapse: collapse; 404 + width: 100%; 405 + margin: 1.5em 0; 406 + } 407 + 408 + th, 409 + td { 410 + border: 1px solid var(--border); 411 + padding: 8px 12px; 412 + text-align: left; 413 + } 414 + 415 + th { 416 + background: var(--bg-secondary); 417 + font-weight: 600; 418 + } 419 + 420 + /* ============================================ 421 + NAVIGATION (Sidebar) 422 + ============================================ */ 423 + 424 + .sidebar nav { 425 + padding: 15px; 426 + } 427 + 428 + .nav-section { 429 + margin-bottom: 1.5em; 430 + } 431 + 432 + .nav-section h3 { 433 + font-size: 0.75em; 434 + text-transform: uppercase; 435 + letter-spacing: 0.05em; 436 + color: var(--text-muted); 437 + margin: 0 0 0.5em 0; 438 + padding: 0 12px; 439 + font-weight: 600; 440 + } 441 + 442 + .sidebar.collapsed .nav-section h3 { 443 + opacity: 0; 444 + visibility: hidden; 445 + } 446 + 447 + .sidebar nav a { 448 + display: block; 449 + padding: 8px 12px; 450 + color: var(--text); 451 + text-decoration: none; 452 + border-radius: 3px; 453 + transition: background 0.15s ease; 454 + white-space: nowrap; 455 + overflow: hidden; 456 + text-overflow: ellipsis; 457 + } 458 + 459 + .sidebar.collapsed nav a { 460 + padding: 8px; 461 + text-indent: -9999px; 462 + } 463 + 464 + .sidebar nav a:hover { 465 + background: var(--bg); 466 + } 467 + 468 + .sidebar nav a.active { 469 + color: var(--sidebar-active); 470 + font-weight: 600; 471 + background: var(--bg); 472 + } 473 + 474 + /* Sidebar Toggle Button */ 475 + .sidebar-toggle { 476 + position: absolute; 477 + right: -15px; 478 + top: 50%; 479 + transform: translateY(-50%); 480 + width: 30px; 481 + height: 30px; 482 + background: var(--sidebar-bg); 483 + border: 1px solid var(--border); 484 + border-radius: 50%; 485 + display: flex; 486 + align-items: center; 487 + justify-content: center; 488 + cursor: pointer; 489 + color: var(--text); 490 + transition: background 0.15s ease; 491 + z-index: 10; 492 + } 493 + 494 + .sidebar-toggle:hover { 495 + background: var(--bg); 496 + } 497 + 498 + .sidebar-toggle svg { 499 + width: 16px; 500 + height: 16px; 501 + } 502 + 503 + /* Content Area - Responsive by default */ 504 + .content { 505 + max-width: min(750px, 100vw - 40px); 506 + margin: 0 auto; 507 + padding: 20px 15px; 508 + box-sizing: border-box; 509 + overflow-wrap: break-word; 510 + word-wrap: break-word; 511 + } 512 + 513 + /* Apply sensible defaults to all content children */ 514 + .content * { 515 + max-width: 100%; 516 + box-sizing: border-box; 517 + } 518 + 519 + /* Responsive media */ 520 + .content img, 521 + .content video, 522 + .content iframe { 523 + height: auto; 524 + } 525 + 526 + /* Horizontal scroll for wide elements */ 527 + .content table, 528 + .content pre { 529 + display: block; 530 + overflow-x: auto; 531 + } 532 + 533 + /* Aggressive word breaking on mobile only */ 534 + @media (max-width: 768px) { 535 + .content { 536 + padding: 15px 10px; 537 + word-break: break-word; 538 + hyphens: auto; 539 + } 540 + } 541 + 542 + /* Home page documentation cards */ 543 + .doc-grid { 544 + display: grid; 545 + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 546 + gap: 1.5em; 547 + margin-top: 1.5em; 548 + } 549 + 550 + .doc-card { 551 + display: block; 552 + padding: 1.5em; 553 + background: var(--bg-secondary); 554 + border: 1px solid var(--border); 555 + border-radius: 8px; 556 + text-decoration: none; 557 + transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; 558 + } 559 + 560 + .doc-card:hover { 561 + transform: translateY(-4px); 562 + box-shadow: var(--shadow-card); 563 + text-decoration: none; 564 + } 565 + 566 + .doc-card h3 { 567 + margin: 0 0 0.5em 0; 568 + color: var(--link); 569 + font-size: 1.1em; 570 + } 571 + 572 + .doc-card p { 573 + margin: 0; 574 + color: var(--text-muted); 575 + font-size: 0.9em; 576 + line-height: 1.5; 577 + } 578 + 579 + .doc-tags { 580 + margin-top: 1em; 581 + display: flex; 582 + flex-wrap: wrap; 583 + gap: 0.5em; 584 + } 585 + 586 + .doc-tag { 587 + font-size: 0.75em; 588 + padding: 0.25em 0.75em; 589 + background: var(--code-bg); 590 + border-radius: 12px; 591 + color: var(--text-muted); 592 + } 593 + 594 + /* Utility classes to replace inline styles */ 595 + .text-muted { 596 + color: var(--text-muted); 597 + } 598 + 599 + .text-muted-sm { 600 + color: var(--text-muted); 601 + font-size: 0.9em; 602 + } 603 + 604 + .page-description { 605 + font-style: italic; 606 + color: var(--text-muted); 607 + } 608 + 609 + .post-meta { 610 + color: var(--text-muted); 611 + font-size: 0.9em; 612 + } 613 + 614 + .post-list-item { 615 + margin: var(--space-md) 0; 616 + } 617 + 618 + .post-separator { 619 + margin: var(--space-md) 0; 620 + padding-bottom: var(--space-sm); 621 + border-bottom: 1px solid var(--border); 622 + } 623 + 624 + .post-title-inline { 625 + margin-top: 0; 626 + } 627 + 628 + .tag-spacing { 629 + margin-left: var(--space-sm); 630 + } 631 + 632 + .section-top { 633 + margin-top: var(--space-lg); 634 + } 635 + 636 + .sidebar-empty { 637 + padding: 0 12px; 638 + color: var(--text-muted); 639 + font-size: 0.9em; 640 + } 641 + 642 + .post-date { 643 + margin: var(--space-xs) 0; 644 + } 645 + 646 + /* ============================================ 647 + HORIZONTAL RULE 648 + ============================================ */ 649 + 650 + hr { 651 + border: none; 652 + border-top: 1px solid var(--hr-border); 653 + margin: 2em 0; 654 + } 655 + 656 + /* ============================================ 657 + SCROLLBAR 658 + ============================================ */ 659 + 660 + ::-webkit-scrollbar { 661 + width: 8px; 662 + height: 8px; 663 + } 664 + 665 + ::-webkit-scrollbar-track { 666 + background: transparent; 667 + transition: background-color 0.3s ease; 668 + } 669 + 670 + ::-webkit-scrollbar-thumb { 671 + background: var(--border); 672 + border-radius: 4px; 673 + transition: background-color 0.3s ease; 674 + } 675 + 676 + ::-webkit-scrollbar-thumb:hover { 677 + background: var(--text-muted); 678 + } 679 + 680 + /* ============================================ 681 + FOOTER 682 + ============================================ */ 683 + 684 + footer { 685 + border-top: 1px solid var(--border); 686 + padding: 15px; 687 + text-align: center; 688 + color: var(--text-muted); 689 + font-size: 0.9em; 690 + background: var(--bg); 691 + } 692 + 693 + footer a { 694 + color: var(--link); 695 + }
+208
theme/templates/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="{{ config.language }}"> 3 + 4 + <head> 5 + {% block head %} 6 + <meta charset="UTF-8" /> 7 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 8 + <meta name="generator" content="Norgolith" /> 9 + 10 + {# Meta tags #} 11 + {% if metadata.description and not metadata.description == "nil" %} 12 + <meta name="description" content="{{ metadata.description }}" /> 13 + {% endif %} 14 + {% if metadata.authors %} 15 + <meta name="author" content="{{ metadata.authors }}" /> 16 + {% endif %} 17 + {% if metadata.categories and metadata.categories | length > 0 %} 18 + <meta name="keywords" content="{{ metadata.categories | join(sep=" , ") }}" /> 19 + {% endif %} 20 + 21 + {% block seo_tags %}{% endblock seo_tags %} 22 + 23 + {# Google Fonts - Monospace #} 24 + <link rel="preconnect" href="https://fonts.googleapis.com"> 25 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 26 + <link 27 + href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" 28 + rel="stylesheet"> 29 + 30 + {# Syntax highlighter #} 31 + {% if config.highlighter is defined and config.highlighter.enable %} 32 + {% if config.highlighter.engine is not string or config.highlighter.engine == "prism" %} 33 + <link rel="stylesheet" href="https://prismjs.catppuccin.com/mocha.css" /> 34 + <link rel="stylesheet" href="/assets/css/prism-custom.css" /> 35 + <script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> 36 + <script defer 37 + src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script> 38 + <script defer 39 + src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script> 40 + {% elif config.highlighter.engine is defined and config.highlighter.engine == "hljs" %} 41 + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> 42 + <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> 43 + <script>hljs.highlightAll();</script> 44 + {% endif %} 45 + {% endif %} 46 + 47 + {# AlpineJS #} 48 + <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script> 49 + <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 50 + <script> 51 + document.addEventListener("alpine:init", () => { 52 + Alpine.data("menu", () => ({ 53 + currentPage: window.location.pathname, 54 + openMobile: false, 55 + sidebarCollapsed: localStorage.getItem('sidebarCollapsed') === 'true', 56 + 57 + toggleMobile() { 58 + this.openMobile = !this.openMobile; 59 + }, 60 + 61 + toggleSidebar() { 62 + this.sidebarCollapsed = !this.sidebarCollapsed; 63 + localStorage.setItem('sidebarCollapsed', this.sidebarCollapsed); 64 + } 65 + })); 66 + 67 + Alpine.data("theme", () => ({ 68 + current: "light", 69 + init() { 70 + const storedTheme = localStorage.getItem("theme"); 71 + if (storedTheme) { 72 + this.current = storedTheme; 73 + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 74 + this.current = "dark"; 75 + } 76 + localStorage.setItem("theme", this.current); 77 + if (this.current === "dark") { 78 + document.documentElement.classList.add("dark"); 79 + } 80 + }, 81 + toggle(event) { 82 + // Get button position for radial expansion 83 + const x = event.clientX; 84 + const y = event.clientY; 85 + 86 + // Set CSS custom properties for animation origin 87 + document.documentElement.style.setProperty('--x', `${x}px`); 88 + document.documentElement.style.setProperty('--y', `${y}px`); 89 + 90 + // Check if View Transitions API is supported 91 + if (!document.startViewTransition) { 92 + // Fallback for browsers without View Transitions API 93 + this.current = this.current === "dark" ? "light" : "dark"; 94 + document.documentElement.classList.toggle("dark", this.current === "dark"); 95 + localStorage.setItem("theme", this.current); 96 + return; 97 + } 98 + 99 + // Use View Transitions API for smooth radial expansion 100 + const transition = document.startViewTransition(() => { 101 + this.current = this.current === "dark" ? "light" : "dark"; 102 + document.documentElement.classList.toggle("dark", this.current === "dark"); 103 + localStorage.setItem("theme", this.current); 104 + }); 105 + } 106 + })); 107 + }); 108 + </script> 109 + 110 + {# Clickable Headings #} 111 + <script> 112 + document.addEventListener('DOMContentLoaded', () => { 113 + const content = document.querySelector('.content'); 114 + if (!content) return; 115 + 116 + const headings = content.querySelectorAll('h1, h2, h3, h4, h5, h6'); 117 + 118 + headings.forEach(heading => { 119 + // Skip headings inside doc-cards 120 + if (heading.closest('.doc-card')) return; 121 + 122 + // Generate ID if needed 123 + if (!heading.id) { 124 + heading.id = heading.textContent 125 + .toLowerCase() 126 + .trim() 127 + .replace(/[^\w\s-]/g, '') 128 + .replace(/\s+/g, '-') 129 + .replace(/-+/g, '-'); 130 + } 131 + 132 + // Make heading clickable 133 + heading.style.cursor = 'pointer'; 134 + heading.addEventListener('click', () => { 135 + window.location.hash = heading.id; 136 + }); 137 + }); 138 + }); 139 + </script> 140 + 141 + {# MermaidJS #} 142 + {% if config.extra.enable_mermaid %} 143 + <script defer src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.9.0/mermaid.min.js"></script> 144 + {% endif %} 145 + 146 + {# Styling and favicon #} 147 + <link rel="stylesheet" href="/assets/css/styles.min.css" /> 148 + {% if config.extra.favicon_path is defined and config.extra.favicon_path is string %} 149 + <link rel="icon" href={{ config.extra.favicon_path }} /> 150 + {% else %} 151 + <link rel="icon" href="/assets/norgolith.svg" /> 152 + {% endif %} 153 + 154 + <title>{% block title %}{% endblock title %} - {{ config.title | title }}</title> 155 + {% endblock head %} 156 + </head> 157 + 158 + <body x-data="menu"> 159 + <header> 160 + {% include "partials/nav.html" %} 161 + </header> 162 + 163 + <div class="page-wrapper"> 164 + <!-- Collapsible Sidebar - Only Posts --> 165 + <aside class="sidebar" :class="{ 'open': openMobile, 'collapsed': sidebarCollapsed }"> 166 + <!-- Collapse Toggle (Desktop) --> 167 + <button @click="toggleSidebar" class="sidebar-toggle hidden md:flex" 168 + :aria-label="sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"> 169 + <svg x-show="!sidebarCollapsed" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 170 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /> 171 + </svg> 172 + <svg x-show="sidebarCollapsed" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 173 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" /> 174 + </svg> 175 + </button> 176 + 177 + <nav> 178 + <!-- Documentation Posts Only --> 179 + {% if posts | length > 0 %} 180 + <div class="nav-section"> 181 + <h3>Documentation</h3> 182 + {% for post in posts | sort(attribute="created") %} 183 + <a :class="{ 'active': currentPage === '{{ post.permalink }}' }" href="{{ post.permalink }}" 184 + title="{{ post.description }}">{{ post.title }}</a> 185 + {% endfor %} 186 + </div> 187 + {% else %} 188 + <div class="nav-section"> 189 + <p class="sidebar-empty">No documentation posts yet.</p> 190 + </div> 191 + {% endif %} 192 + </nav> 193 + </aside> 194 + 195 + <!-- Main Content --> 196 + <div class="content-wrapper" :class="{ 'sidebar-collapsed': sidebarCollapsed }"> 197 + <main class="content"> 198 + {% block content %}{% endblock content %} 199 + </main> 200 + 201 + <footer> 202 + {% include "partials/footer.html" %} 203 + </footer> 204 + </div> 205 + </div> 206 + </body> 207 + 208 + </html>
+33
theme/templates/categories.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + <meta property="og:title" content="Categories - {{ config.title | title }}" /> 4 + <meta property="og:type" content="website" /> 5 + <meta property="og:url" content="{{ config.rootUrl }}/categories" /> 6 + <meta property="og:description" content="Posts categories" /> 7 + <meta property="og:site_name" content="{{ config.title }}" /> 8 + <meta property="og:locale" content="{{ config.language }}" /> 9 + <link rel="canonical" href="{{ config.rootUrl }}/categories" /> 10 + <meta name="robots" content="index, follow" /> 11 + {% endblock seo_tags %} 12 + 13 + {% block title %}Categories{% endblock title %} 14 + 15 + {% block content %} 16 + <h1>Categories</h1> 17 + <p class="page-description">All the categories used in posts.</p> 18 + 19 + <ul> 20 + {% for category in categories | sort %} 21 + {%- set_global cat_posts = 0 -%} 22 + {% for post in posts %} 23 + {% if category in post.categories %} 24 + {%- set_global cat_posts = cat_posts + 1 -%} 25 + {% endif %} 26 + {% endfor %} 27 + <li> 28 + <a href="/categories/{{ category }}">{{ category }}</a> 29 + <span class="text-muted">({{ cat_posts }} post{{ cat_posts | pluralize }})</span> 30 + </li> 31 + {% endfor %} 32 + </ul> 33 + {% endblock content %}
+31
theme/templates/category.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + <meta property="og:title" content="Category: {{ category }} - {{ config.title | title }}" /> 4 + <meta property="og:type" content="website" /> 5 + <meta property="og:url" content="{{ config.rootUrl }}/categories/{{ category }}" /> 6 + <meta property="og:description" content="Posts on category {{ category }}" /> 7 + <meta property="og:site_name" content="{{ config.title }}" /> 8 + <meta property="og:locale" content="{{ config.language }}" /> 9 + <link rel="canonical" href="{{ config.rootUrl }}/categories/{{ category }}" /> 10 + <meta name="robots" content="index, follow" /> 11 + {% endblock seo_tags %} 12 + 13 + {% block title %}Category: {{ category }}{% endblock title %} 14 + 15 + {% block content %} 16 + <h1>Posts in {{ category }}</h1> 17 + <p class="page-description">All the posts with the category "{{ category }}"</p> 18 + 19 + <ul> 20 + {% for post in posts | sort(attribute="created") | reverse %} 21 + {% if category in post.categories %} 22 + <li class="post-list-item"> 23 + <a href="{{ post.permalink }}">{{ post.title }}</a> 24 + <span class="text-muted-sm"> 25 + — {{ post.created | date(format="%B %e, %Y") }} 26 + </span> 27 + </li> 28 + {% endif %} 29 + {% endfor %} 30 + </ul> 31 + {% endblock content %}
+26
theme/templates/default.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + {% if metadata.title is defined %} 4 + <meta property="og:title" content="{{ metadata.title | title }} - {{ config.title | title }}" /> 5 + {% else %} 6 + <meta property="og:title" content="{{ config.title | title }}" /> 7 + {% endif %} 8 + <meta property="og:type" content="website" /> 9 + {% if metadata.permalink is defined %} 10 + <meta property="og:url" content="{{ metadata.permalink }}" /> 11 + <link rel="canonical" href="{{ metadata.permalink }}" /> 12 + {% endif %} 13 + {% if metadata.description is defined %} 14 + <meta property="og:description" content="{{ metadata.description }}" /> 15 + {% endif %} 16 + <meta property="og:site_name" content="{{ config.title }}" /> 17 + <meta property="og:locale" content="{{ config.language }}" /> 18 + <meta name="robots" content="index, follow" /> 19 + {% endblock seo_tags %} 20 + 21 + {% block title %}{% if metadata.title is defined %}{{ metadata.title | title }}{% else %}{{ config.title | title }}{% 22 + endif %}{% endblock title %} 23 + 24 + {% block content %} 25 + {{ content | safe }} 26 + {% endblock content %}
+41
theme/templates/home.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + <meta property="og:title" content="{{ config.title | title }}" /> 4 + <meta property="og:type" content="website" /> 5 + <meta property="og:url" content="{{ metadata.permalink }}" /> 6 + <meta property="og:description" content="{{ metadata.description }}" /> 7 + <meta property="og:site_name" content="{{ config.title }}" /> 8 + <meta property="og:locale" content="{{ config.language }}" /> 9 + <link rel="canonical" href="{{ config.rootUrl }}" /> 10 + <meta name="robots" content="index, follow" /> 11 + {% endblock seo_tags %} 12 + 13 + {% block title %}{{ metadata.title | title }}{% endblock title %} 14 + 15 + {% block content %} 16 + {{ content | safe }} 17 + 18 + {# Documentation Cards #} 19 + {% if posts | length > 0 %} 20 + <div class="section-top"> 21 + <h2>Documentation</h2> 22 + <div class="doc-grid"> 23 + {% for post in posts | sort(attribute="created") %} 24 + <a href="{{ post.permalink }}" class="doc-card"> 25 + <h3>{{ post.title }}</h3> 26 + {% if post.description %} 27 + <p>{{ post.description }}</p> 28 + {% endif %} 29 + {% if post.categories and post.categories | length > 0 %} 30 + <div class="doc-tags"> 31 + {% for category in post.categories %} 32 + <span class="doc-tag">{{ category }}</span> 33 + {% endfor %} 34 + </div> 35 + {% endif %} 36 + </a> 37 + {% endfor %} 38 + </div> 39 + </div> 40 + {% endif %} 41 + {% endblock content %}
+14
theme/templates/partials/footer.html
··· 1 + <div> 2 + <p> 3 + {{ config.author }} © {{ now(format="%Y") }} 4 + {% if config.extra.license %}| Licensed under {{ config.extra.license }}{% endif %} 5 + </p> 6 + {% if config.extra.footer %} 7 + <p> 8 + {% for name, link in config.extra.footer %} 9 + <a href="{{ link }}" target="_blank" rel="noopener">{{ name }}</a> 10 + {% if not loop.last %} | {% endif %} 11 + {% endfor %} 12 + </p> 13 + {% endif %} 14 + </div>
+34
theme/templates/partials/nav.html
··· 1 + <nav class="flex items-center h-full px-4"> 2 + <!-- Menu Button (Mobile) --> 3 + <button @click="toggleMobile" class="menu-btn md:hidden" aria-label="Toggle Menu"> 4 + <svg x-show="!openMobile" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 5 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 6 + </svg> 7 + <svg x-show="openMobile" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 8 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 9 + </svg> 10 + </button> 11 + 12 + <!-- Title --> 13 + <a href="/" class="header-title">{{ config.title | title }}</a> 14 + 15 + <!-- Desktop Navigation Links --> 16 + <div class="hidden md:flex md:items-center md:gap-6 md:ml-8"> 17 + <a :class="{ 'active-nav': currentPage.startsWith('/categories') }" class="nav-link" href="/categories">Tags</a> 18 + </div> 19 + 20 + <!-- Theme Toggle --> 21 + <div x-data="theme"> 22 + <button @click="toggle($event)" class="theme-toggle" 23 + :aria-label="current === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"> 24 + <svg x-show="current === 'dark'" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 25 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 26 + d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> 27 + </svg> 28 + <svg x-show="current === 'light'" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 29 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 30 + d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> 31 + </svg> 32 + </button> 33 + </div> 34 + </nav>
+33
theme/templates/post.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + <meta property="og:title" content="{{ metadata.title | title }} - {{ config.title | title }}" /> 4 + <meta property="og:type" content="article" /> 5 + <meta property="og:url" content="{{ metadata.permalink }}" /> 6 + <meta property="og:description" content="{{ metadata.description }}" /> 7 + <meta property="og:site_name" content="{{ config.title }}" /> 8 + <meta property="og:locale" content="{{ config.language }}" /> 9 + <link rel="canonical" href="{{ metadata.permalink }}" /> 10 + <meta name="robots" content="index, follow" /> 11 + {% endblock seo_tags %} 12 + 13 + {% block title %}{{ metadata.title | title }}{% endblock title %} 14 + 15 + {% block content %} 16 + <h1>{{ metadata.title }}</h1> 17 + 18 + <p class="post-meta"> 19 + {% set created = metadata.created | date(format="%B %e, %Y") %} 20 + <time datetime="{{ metadata.created }}">{{ created }}</time> 21 + 22 + {% if metadata.categories | length > 0 %} 23 + <span class="tag-spacing"> 24 + Tags: 25 + {% for category in metadata.categories %} 26 + <a href="/categories/{{ category }}">{{ category }}</a>{% if not loop.last %}, {% endif %} 27 + {% endfor %} 28 + </span> 29 + {% endif %} 30 + </p> 31 + 32 + {{ content | safe }} 33 + {% endblock content %}
+39
theme/templates/posts.html
··· 1 + {% extends "base.html" %} 2 + {% block seo_tags %} 3 + <meta property="og:title" content="Posts - {{ config.title | title }}" /> 4 + <meta property="og:type" content="website" /> 5 + <meta property="og:url" content="{{ metadata.permalink }}" /> 6 + <meta property="og:description" content="Posts on {{ config.title | title }}" /> 7 + <meta property="og:site_name" content="{{ config.title }}" /> 8 + <meta property="og:locale" content="{{ config.language }}" /> 9 + <link rel="canonical" href="{{ metadata.permalink }}" /> 10 + <meta name="robots" content="index, follow" /> 11 + {% endblock seo_tags %} 12 + 13 + {% block title %}Posts{% endblock title %} 14 + 15 + {% block content %} 16 + <h1>Posts</h1> 17 + 18 + {% for post in posts | sort(attribute="created") | reverse %} 19 + <div class="post-separator"> 20 + <h2 class="post-title-inline"> 21 + <a href="{{ post.permalink }}">{{ post.title }}</a> 22 + </h2> 23 + <p class="text-muted post-date"> 24 + <time datetime="{{ post.created }}">{{ post.created | date(format="%B %e, %Y") }}</time> 25 + </p> 26 + {% if post.description %} 27 + <p class="page-description">{{ post.description }}</p> 28 + {% endif %} 29 + <p> 30 + {% if post.truncate_char is defined and post.truncate_char is matching("^nil$") %} 31 + {% set truncate_char = "" %} 32 + {% else %} 33 + {% set truncate_char = "…" %} 34 + {% endif %} 35 + {{ post.raw | striptags | truncate(length=200, end=truncate_char) | safe }} 36 + </p> 37 + </div> 38 + {% endfor %} 39 + {% endblock content %}
+5
theme/theme.toml
··· 1 + name = "Norgowind" 2 + author = "NTBBloodbath" 3 + description = "Norgolith <3 TailwindCSS" 4 + version = "0.3.0" 5 + license = "MIT"