engineering blog at https://blog.tangled.sh

Compare changes

Choose any two refs to compare.

Changed files
+1150 -76
pages
blog
static
templates
+6 -6
flake.lock
··· 69 69 "nixpkgs": "nixpkgs_2" 70 70 }, 71 71 "locked": { 72 - "lastModified": 1758224598, 73 - "narHash": "sha256-Ai+kyEpZVPTuk0IP34kE8ZaXxhI+Z97msUFPe82k0Ic=", 72 + "lastModified": 1768109018, 73 + "narHash": "sha256-ePHsZ62UURGy44rkLva16RILZKI7PWcnGzyrP5Qmqt8=", 74 74 "ref": "refs/heads/master", 75 - "rev": "e67e553dc237e41adc9ceae4d834fc02d44e4eb4", 76 - "revCount": 96, 75 + "rev": "0368173f7a3672916d26ac7c3183dd9998d1a514", 76 + "revCount": 98, 77 77 "type": "git", 78 - "url": "https://tangled.sh/@anirudh.fi/vite" 78 + "url": "https://tangled.org/oppi.li/vite" 79 79 }, 80 80 "original": { 81 81 "type": "git", 82 - "url": "https://tangled.sh/@anirudh.fi/vite" 82 + "url": "https://tangled.org/oppi.li/vite" 83 83 } 84 84 } 85 85 },
+1 -1
flake.nix
··· 4 4 inputs = { 5 5 nixpkgs.url = "github:nixos/nixpkgs"; 6 6 vite = { 7 - url = "git+https://tangled.sh/@anirudh.fi/vite"; 7 + url = "git+https://tangled.org/oppi.li/vite"; 8 8 flake = true; 9 9 }; 10 10 inter-fonts-src = {
+856 -27
input.css
··· 66 66 font-display: swap; 67 67 } 68 68 69 - h1 { 70 - @apply text-2xl; 71 - @apply text-black; 72 - @apply font-bold; 73 - } 74 - 75 69 ::selection { 76 70 @apply bg-yellow-400 text-black bg-opacity-30 dark:bg-yellow-600 dark:bg-opacity-50 dark:text-white; 77 71 } ··· 84 78 @supports (font-variation-settings: normal) { 85 79 html { 86 80 font-feature-settings: 87 - "ss01" 1, 88 81 "kern" 1, 89 82 "liga" 1, 90 83 "cv05" 1, ··· 92 85 } 93 86 } 94 87 95 - a { 96 - @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 97 - } 98 - 99 88 img { 100 89 @apply border border-gray-200 rounded dark:border-gray-700; 101 90 } ··· 104 93 @apply border-0 dark:brightness-100 dark:opacity-100; 105 94 } 106 95 96 + a { 97 + @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 98 + } 99 + 107 100 label { 108 - @apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 101 + @apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 109 102 } 110 103 input { 111 - @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 104 + @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 112 105 } 113 106 textarea { 114 - @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 107 + @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 115 108 } 116 109 details summary::-webkit-details-marker { 117 110 display: none; 118 111 } 112 + 113 + code { 114 + @apply font-mono rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white; 115 + } 119 116 } 120 117 121 118 @layer components { 122 119 .btn { 123 - @apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center 124 - justify-center bg-transparent px-2 pb-[0.2rem] text-base 125 - text-gray-900 before:absolute before:inset-0 before:-z-10 126 - before:block before:rounded-sm before:border before:border-gray-200 127 - before:bg-white before:drop-shadow-sm 128 - before:content-[''] hover:before:border-gray-300 129 - hover:before:bg-gray-50 130 - hover:before:shadow-[0_2px_2px_0_rgba(20,20,96,0.1),inset_0_-2px_0_0_#f5f5f5] 131 - focus:outline-none focus-visible:before:outline 132 - focus-visible:before:outline-4 focus-visible:before:outline-gray-500 133 - active:before:shadow-[inset_0_2px_2px_0_rgba(20,20,96,0.1)]; 120 + @apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center 121 + bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900 122 + before:absolute before:inset-0 before:-z-10 before:block before:rounded 123 + before:border before:border-gray-200 before:bg-white 124 + before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.1),0_1px_0_0_rgba(0,0,0,0.04)] 125 + before:content-[''] before:transition-all before:duration-150 before:ease-in-out 126 + hover:before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.15),0_2px_1px_0_rgba(0,0,0,0.06)] 127 + hover:before:bg-gray-50 128 + dark:hover:before:bg-gray-700 129 + active:before:shadow-[inset_0_2px_2px_0_rgba(0,0,0,0.1)] 130 + focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400 131 + disabled:cursor-not-allowed disabled:opacity-50 132 + dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700; 133 + } 134 + 135 + .btn-create { 136 + @apply btn text-white 137 + before:bg-green-600 hover:before:bg-green-700 138 + dark:before:bg-green-700 dark:hover:before:bg-green-800 139 + before:border before:border-green-700 hover:before:border-green-800 140 + focus-visible:before:outline-green-500 141 + disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 142 + } 143 + 144 + .prose hr { 145 + @apply my-2; 146 + } 147 + 148 + .prose li:has(input) { 149 + @apply list-none; 150 + } 151 + 152 + .prose ul:has(input) { 153 + @apply pl-2; 154 + } 155 + 156 + .prose .heading .anchor { 157 + @apply no-underline mx-2 opacity-0; 158 + } 159 + 160 + .prose .heading:hover .anchor { 161 + @apply opacity-70; 162 + } 163 + 164 + .prose .heading .anchor:hover { 165 + @apply opacity-70; 166 + } 167 + 168 + .prose a.footnote-backref { 169 + @apply no-underline; 170 + } 171 + 172 + .prose a.mention { 173 + @apply no-underline hover:underline font-bold; 174 + } 175 + 176 + .prose li { 177 + @apply my-0 py-0; 178 + } 179 + 180 + .prose ul, 181 + .prose ol { 182 + @apply my-1 py-0; 183 + } 184 + 185 + .prose img { 186 + display: inline; 187 + margin: 0; 188 + vertical-align: middle; 189 + } 190 + 191 + .prose input { 192 + @apply inline-block my-0 mb-1 mx-1; 193 + } 194 + 195 + .prose input[type="checkbox"] { 196 + @apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500; 197 + } 198 + 199 + /* Base callout */ 200 + details[data-callout] { 201 + @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4; 202 + } 203 + 204 + details[data-callout] > summary { 205 + @apply font-bold cursor-pointer mb-1; 206 + } 207 + 208 + details[data-callout] > .callout-content { 209 + @apply text-sm leading-snug; 210 + } 211 + 212 + /* Note (blue) */ 213 + details[data-callout="note" i] { 214 + @apply border-blue-400 dark:border-blue-500; 215 + } 216 + details[data-callout="note" i] > summary { 217 + @apply text-blue-700 dark:text-blue-400; 218 + } 219 + 220 + /* Important (purple) */ 221 + details[data-callout="important" i] { 222 + @apply border-purple-400 dark:border-purple-500; 223 + } 224 + details[data-callout="important" i] > summary { 225 + @apply text-purple-700 dark:text-purple-400; 226 + } 227 + 228 + /* Warning (yellow) */ 229 + details[data-callout="warning" i] { 230 + @apply border-yellow-400 dark:border-yellow-500; 231 + } 232 + details[data-callout="warning" i] > summary { 233 + @apply text-yellow-700 dark:text-yellow-400; 234 + } 235 + 236 + /* Caution (red) */ 237 + details[data-callout="caution" i] { 238 + @apply border-red-400 dark:border-red-500; 239 + } 240 + details[data-callout="caution" i] > summary { 241 + @apply text-red-700 dark:text-red-400; 242 + } 243 + 244 + /* Tip (green) */ 245 + details[data-callout="tip" i] { 246 + @apply border-green-400 dark:border-green-500; 247 + } 248 + details[data-callout="tip" i] > summary { 249 + @apply text-green-700 dark:text-green-400; 134 250 } 251 + 252 + /* Optional: hide the disclosure arrow like GitHub */ 253 + details[data-callout] > summary::-webkit-details-marker { 254 + display: none; 255 + } 256 + 135 257 } 136 258 @layer utilities { 137 259 .error { 138 - @apply py-1 text-red-400; 260 + @apply py-1 text-red-400 dark:text-red-300; 139 261 } 140 262 .success { 141 - @apply py-1 text-black; 263 + @apply py-1 text-gray-900 dark:text-gray-100; 142 264 } 143 265 } 266 + 267 + } 268 + 269 + /* Background */ 270 + .bg { 271 + color: #4c4f69; 272 + background-color: #eff1f5; 273 + } 274 + /* PreWrapper */ 275 + .chroma { 276 + color: #4c4f69; 277 + } 278 + /* Error */ 279 + .chroma .err { 280 + color: #d20f39; 281 + } 282 + /* LineLink */ 283 + .chroma .lnlinks { 284 + outline: none; 285 + text-decoration: none; 286 + color: inherit; 287 + } 288 + /* LineTableTD */ 289 + .chroma .lntd { 290 + vertical-align: top; 291 + padding: 0; 292 + margin: 0; 293 + border: 0; 294 + } 295 + /* LineTable */ 296 + .chroma .lntable { 297 + border-spacing: 0; 298 + padding: 0; 299 + margin: 0; 300 + border: 0; 301 + } 302 + /* LineHighlight */ 303 + .chroma .hl { 304 + @apply bg-amber-400/30 dark:bg-amber-500/20; 305 + } 306 + 307 + /* LineNumbersTable */ 308 + .chroma .lnt { 309 + white-space: pre; 310 + -webkit-user-select: none; 311 + user-select: none; 312 + margin-right: 0.4em; 313 + padding: 0 0.4em 0 0.4em; 314 + color: #8c8fa1; 315 + } 316 + /* LineNumbers */ 317 + .chroma .ln { 318 + white-space: pre; 319 + -webkit-user-select: none; 320 + user-select: none; 321 + margin-right: 0.4em; 322 + padding: 0 0.4em 0 0.4em; 323 + color: #8c8fa1; 324 + } 325 + /* Line */ 326 + .chroma .line { 327 + display: flex; 328 + } 329 + /* Keyword */ 330 + .chroma .k { 331 + color: #8839ef; 332 + } 333 + /* KeywordConstant */ 334 + .chroma .kc { 335 + color: #fe640b; 336 + } 337 + /* KeywordDeclaration */ 338 + .chroma .kd { 339 + color: #d20f39; 340 + } 341 + /* KeywordNamespace */ 342 + .chroma .kn { 343 + color: #179299; 344 + } 345 + /* KeywordPseudo */ 346 + .chroma .kp { 347 + color: #8839ef; 348 + } 349 + /* KeywordReserved */ 350 + .chroma .kr { 351 + color: #8839ef; 352 + } 353 + /* KeywordType */ 354 + .chroma .kt { 355 + color: #d20f39; 356 + } 357 + /* NameAttribute */ 358 + .chroma .na { 359 + color: #1e66f5; 360 + } 361 + /* NameBuiltin */ 362 + .chroma .nb { 363 + color: #04a5e5; 364 + } 365 + /* NameBuiltinPseudo */ 366 + .chroma .bp { 367 + color: #04a5e5; 368 + } 369 + /* NameClass */ 370 + .chroma .nc { 371 + color: #df8e1d; 372 + } 373 + /* NameConstant */ 374 + .chroma .no { 375 + color: #df8e1d; 376 + } 377 + /* NameDecorator */ 378 + .chroma .nd { 379 + color: #1e66f5; 380 + font-weight: bold; 381 + } 382 + /* NameEntity */ 383 + .chroma .ni { 384 + color: #179299; 385 + } 386 + /* NameException */ 387 + .chroma .ne { 388 + color: #fe640b; 389 + } 390 + /* NameFunction */ 391 + .chroma .nf { 392 + color: #1e66f5; 393 + } 394 + /* NameFunctionMagic */ 395 + .chroma .fm { 396 + color: #1e66f5; 397 + } 398 + /* NameLabel */ 399 + .chroma .nl { 400 + color: #04a5e5; 401 + } 402 + /* NameNamespace */ 403 + .chroma .nn { 404 + color: #fe640b; 405 + } 406 + /* NameProperty */ 407 + .chroma .py { 408 + color: #fe640b; 409 + } 410 + /* NameTag */ 411 + .chroma .nt { 412 + color: #8839ef; 413 + } 414 + /* NameVariable */ 415 + .chroma .nv { 416 + color: #dc8a78; 417 + } 418 + /* NameVariableClass */ 419 + .chroma .vc { 420 + color: #dc8a78; 421 + } 422 + /* NameVariableGlobal */ 423 + .chroma .vg { 424 + color: #dc8a78; 425 + } 426 + /* NameVariableInstance */ 427 + .chroma .vi { 428 + color: #dc8a78; 429 + } 430 + /* NameVariableMagic */ 431 + .chroma .vm { 432 + color: #dc8a78; 433 + } 434 + /* LiteralString */ 435 + .chroma .s { 436 + color: #40a02b; 437 + } 438 + /* LiteralStringAffix */ 439 + .chroma .sa { 440 + color: #d20f39; 441 + } 442 + /* LiteralStringBacktick */ 443 + .chroma .sb { 444 + color: #40a02b; 445 + } 446 + /* LiteralStringChar */ 447 + .chroma .sc { 448 + color: #40a02b; 449 + } 450 + /* LiteralStringDelimiter */ 451 + .chroma .dl { 452 + color: #1e66f5; 453 + } 454 + /* LiteralStringDoc */ 455 + .chroma .sd { 456 + color: #9ca0b0; 457 + } 458 + /* LiteralStringDouble */ 459 + .chroma .s2 { 460 + color: #40a02b; 461 + } 462 + /* LiteralStringEscape */ 463 + .chroma .se { 464 + color: #1e66f5; 465 + } 466 + /* LiteralStringHeredoc */ 467 + .chroma .sh { 468 + color: #9ca0b0; 469 + } 470 + /* LiteralStringInterpol */ 471 + .chroma .si { 472 + color: #40a02b; 473 + } 474 + /* LiteralStringOther */ 475 + .chroma .sx { 476 + color: #40a02b; 477 + } 478 + /* LiteralStringRegex */ 479 + .chroma .sr { 480 + color: #179299; 481 + } 482 + /* LiteralStringSingle */ 483 + .chroma .s1 { 484 + color: #40a02b; 485 + } 486 + /* LiteralStringSymbol */ 487 + .chroma .ss { 488 + color: #40a02b; 489 + } 490 + /* LiteralNumber */ 491 + .chroma .m { 492 + color: #fe640b; 493 + } 494 + /* LiteralNumberBin */ 495 + .chroma .mb { 496 + color: #fe640b; 497 + } 498 + /* LiteralNumberFloat */ 499 + .chroma .mf { 500 + color: #fe640b; 501 + } 502 + /* LiteralNumberHex */ 503 + .chroma .mh { 504 + color: #fe640b; 505 + } 506 + /* LiteralNumberInteger */ 507 + .chroma .mi { 508 + color: #fe640b; 509 + } 510 + /* LiteralNumberIntegerLong */ 511 + .chroma .il { 512 + color: #fe640b; 513 + } 514 + /* LiteralNumberOct */ 515 + .chroma .mo { 516 + color: #fe640b; 517 + } 518 + /* Operator */ 519 + .chroma .o { 520 + color: #04a5e5; 521 + font-weight: bold; 522 + } 523 + /* OperatorWord */ 524 + .chroma .ow { 525 + color: #04a5e5; 526 + font-weight: bold; 527 + } 528 + /* Comment */ 529 + .chroma .c { 530 + color: #9ca0b0; 531 + font-style: italic; 532 + } 533 + /* CommentHashbang */ 534 + .chroma .ch { 535 + color: #9ca0b0; 536 + font-style: italic; 537 + } 538 + /* CommentMultiline */ 539 + .chroma .cm { 540 + color: #9ca0b0; 541 + font-style: italic; 542 + } 543 + /* CommentSingle */ 544 + .chroma .c1 { 545 + color: #9ca0b0; 546 + font-style: italic; 547 + } 548 + /* CommentSpecial */ 549 + .chroma .cs { 550 + color: #9ca0b0; 551 + font-style: italic; 552 + } 553 + /* CommentPreproc */ 554 + .chroma .cp { 555 + color: #9ca0b0; 556 + font-style: italic; 557 + } 558 + /* CommentPreprocFile */ 559 + .chroma .cpf { 560 + color: #9ca0b0; 561 + font-weight: bold; 562 + font-style: italic; 563 + } 564 + /* GenericDeleted */ 565 + .chroma .gd { 566 + color: #d20f39; 567 + background-color: oklch(93.6% 0.032 17.717); 568 + } 569 + /* GenericEmph */ 570 + .chroma .ge { 571 + font-style: italic; 572 + } 573 + /* GenericError */ 574 + .chroma .gr { 575 + color: #d20f39; 576 + } 577 + /* GenericHeading */ 578 + .chroma .gh { 579 + color: #fe640b; 580 + font-weight: bold; 581 + } 582 + /* GenericInserted */ 583 + .chroma .gi { 584 + color: #40a02b; 585 + background-color: oklch(96.2% 0.044 156.743); 586 + } 587 + /* GenericStrong */ 588 + .chroma .gs { 589 + font-weight: bold; 590 + } 591 + /* GenericSubheading */ 592 + .chroma .gu { 593 + color: #fe640b; 594 + font-weight: bold; 595 + } 596 + /* GenericTraceback */ 597 + .chroma .gt { 598 + color: #d20f39; 599 + } 600 + /* GenericUnderline */ 601 + .chroma .gl { 602 + text-decoration: underline; 603 + } 604 + 605 + @media (prefers-color-scheme: dark) { 606 + /* Background */ 607 + .bg { 608 + color: #cad3f5; 609 + background-color: #24273a; 610 + } 611 + /* PreWrapper */ 612 + .chroma { 613 + color: #cad3f5; 614 + } 615 + /* Error */ 616 + .chroma .err { 617 + color: #ed8796; 618 + } 619 + /* LineLink */ 620 + .chroma .lnlinks { 621 + outline: none; 622 + text-decoration: none; 623 + color: inherit; 624 + } 625 + /* LineTableTD */ 626 + .chroma .lntd { 627 + vertical-align: top; 628 + padding: 0; 629 + margin: 0; 630 + border: 0; 631 + } 632 + /* LineTable */ 633 + .chroma .lntable { 634 + border-spacing: 0; 635 + padding: 0; 636 + margin: 0; 637 + border: 0; 638 + } 639 + /* LineHighlight */ 640 + .chroma .hl { 641 + background-color: #494d64; 642 + } 643 + /* LineNumbersTable */ 644 + .chroma .lnt { 645 + white-space: pre; 646 + -webkit-user-select: none; 647 + user-select: none; 648 + margin-right: 0.4em; 649 + padding: 0 0.4em 0 0.4em; 650 + color: #8087a2; 651 + } 652 + /* LineNumbers */ 653 + .chroma .ln { 654 + white-space: pre; 655 + -webkit-user-select: none; 656 + user-select: none; 657 + margin-right: 0.4em; 658 + padding: 0 0.4em 0 0.4em; 659 + color: #8087a2; 660 + } 661 + /* Line */ 662 + .chroma .line { 663 + display: flex; 664 + } 665 + /* Keyword */ 666 + .chroma .k { 667 + color: #c6a0f6; 668 + } 669 + /* KeywordConstant */ 670 + .chroma .kc { 671 + color: #f5a97f; 672 + } 673 + /* KeywordDeclaration */ 674 + .chroma .kd { 675 + color: #ed8796; 676 + } 677 + /* KeywordNamespace */ 678 + .chroma .kn { 679 + color: #8bd5ca; 680 + } 681 + /* KeywordPseudo */ 682 + .chroma .kp { 683 + color: #c6a0f6; 684 + } 685 + /* KeywordReserved */ 686 + .chroma .kr { 687 + color: #c6a0f6; 688 + } 689 + /* KeywordType */ 690 + .chroma .kt { 691 + color: #ed8796; 692 + } 693 + /* NameAttribute */ 694 + .chroma .na { 695 + color: #8aadf4; 696 + } 697 + /* NameBuiltin */ 698 + .chroma .nb { 699 + color: #91d7e3; 700 + } 701 + /* NameBuiltinPseudo */ 702 + .chroma .bp { 703 + color: #91d7e3; 704 + } 705 + /* NameClass */ 706 + .chroma .nc { 707 + color: #eed49f; 708 + } 709 + /* NameConstant */ 710 + .chroma .no { 711 + color: #eed49f; 712 + } 713 + /* NameDecorator */ 714 + .chroma .nd { 715 + color: #8aadf4; 716 + font-weight: bold; 717 + } 718 + /* NameEntity */ 719 + .chroma .ni { 720 + color: #8bd5ca; 721 + } 722 + /* NameException */ 723 + .chroma .ne { 724 + color: #f5a97f; 725 + } 726 + /* NameFunction */ 727 + .chroma .nf { 728 + color: #8aadf4; 729 + } 730 + /* NameFunctionMagic */ 731 + .chroma .fm { 732 + color: #8aadf4; 733 + } 734 + /* NameLabel */ 735 + .chroma .nl { 736 + color: #91d7e3; 737 + } 738 + /* NameNamespace */ 739 + .chroma .nn { 740 + color: #f5a97f; 741 + } 742 + /* NameProperty */ 743 + .chroma .py { 744 + color: #f5a97f; 745 + } 746 + /* NameTag */ 747 + .chroma .nt { 748 + color: #c6a0f6; 749 + } 750 + /* NameVariable */ 751 + .chroma .nv { 752 + color: #f4dbd6; 753 + } 754 + /* NameVariableClass */ 755 + .chroma .vc { 756 + color: #f4dbd6; 757 + } 758 + /* NameVariableGlobal */ 759 + .chroma .vg { 760 + color: #f4dbd6; 761 + } 762 + /* NameVariableInstance */ 763 + .chroma .vi { 764 + color: #f4dbd6; 765 + } 766 + /* NameVariableMagic */ 767 + .chroma .vm { 768 + color: #f4dbd6; 769 + } 770 + /* LiteralString */ 771 + .chroma .s { 772 + color: #a6da95; 773 + } 774 + /* LiteralStringAffix */ 775 + .chroma .sa { 776 + color: #ed8796; 777 + } 778 + /* LiteralStringBacktick */ 779 + .chroma .sb { 780 + color: #a6da95; 781 + } 782 + /* LiteralStringChar */ 783 + .chroma .sc { 784 + color: #a6da95; 785 + } 786 + /* LiteralStringDelimiter */ 787 + .chroma .dl { 788 + color: #8aadf4; 789 + } 790 + /* LiteralStringDoc */ 791 + .chroma .sd { 792 + color: #6e738d; 793 + } 794 + /* LiteralStringDouble */ 795 + .chroma .s2 { 796 + color: #a6da95; 797 + } 798 + /* LiteralStringEscape */ 799 + .chroma .se { 800 + color: #8aadf4; 801 + } 802 + /* LiteralStringHeredoc */ 803 + .chroma .sh { 804 + color: #6e738d; 805 + } 806 + /* LiteralStringInterpol */ 807 + .chroma .si { 808 + color: #a6da95; 809 + } 810 + /* LiteralStringOther */ 811 + .chroma .sx { 812 + color: #a6da95; 813 + } 814 + /* LiteralStringRegex */ 815 + .chroma .sr { 816 + color: #8bd5ca; 817 + } 818 + /* LiteralStringSingle */ 819 + .chroma .s1 { 820 + color: #a6da95; 821 + } 822 + /* LiteralStringSymbol */ 823 + .chroma .ss { 824 + color: #a6da95; 825 + } 826 + /* LiteralNumber */ 827 + .chroma .m { 828 + color: #f5a97f; 829 + } 830 + /* LiteralNumberBin */ 831 + .chroma .mb { 832 + color: #f5a97f; 833 + } 834 + /* LiteralNumberFloat */ 835 + .chroma .mf { 836 + color: #f5a97f; 837 + } 838 + /* LiteralNumberHex */ 839 + .chroma .mh { 840 + color: #f5a97f; 841 + } 842 + /* LiteralNumberInteger */ 843 + .chroma .mi { 844 + color: #f5a97f; 845 + } 846 + /* LiteralNumberIntegerLong */ 847 + .chroma .il { 848 + color: #f5a97f; 849 + } 850 + /* LiteralNumberOct */ 851 + .chroma .mo { 852 + color: #f5a97f; 853 + } 854 + /* Operator */ 855 + .chroma .o { 856 + color: #91d7e3; 857 + font-weight: bold; 858 + } 859 + /* OperatorWord */ 860 + .chroma .ow { 861 + color: #91d7e3; 862 + font-weight: bold; 863 + } 864 + /* Comment */ 865 + .chroma .c { 866 + color: #6e738d; 867 + font-style: italic; 868 + } 869 + /* CommentHashbang */ 870 + .chroma .ch { 871 + color: #6e738d; 872 + font-style: italic; 873 + } 874 + /* CommentMultiline */ 875 + .chroma .cm { 876 + color: #6e738d; 877 + font-style: italic; 878 + } 879 + /* CommentSingle */ 880 + .chroma .c1 { 881 + color: #6e738d; 882 + font-style: italic; 883 + } 884 + /* CommentSpecial */ 885 + .chroma .cs { 886 + color: #6e738d; 887 + font-style: italic; 888 + } 889 + /* CommentPreproc */ 890 + .chroma .cp { 891 + color: #6e738d; 892 + font-style: italic; 893 + } 894 + /* CommentPreprocFile */ 895 + .chroma .cpf { 896 + color: #6e738d; 897 + font-weight: bold; 898 + font-style: italic; 899 + } 900 + /* GenericDeleted */ 901 + .chroma .gd { 902 + color: #ed8796; 903 + background-color: oklch(44.4% 0.177 26.899 / 0.5); 904 + } 905 + /* GenericEmph */ 906 + .chroma .ge { 907 + font-style: italic; 908 + } 909 + /* GenericError */ 910 + .chroma .gr { 911 + color: #ed8796; 912 + } 913 + /* GenericHeading */ 914 + .chroma .gh { 915 + color: #f5a97f; 916 + font-weight: bold; 917 + } 918 + /* GenericInserted */ 919 + .chroma .gi { 920 + color: #a6da95; 921 + background-color: oklch(44.8% 0.119 151.328 / 0.5); 922 + } 923 + /* GenericStrong */ 924 + .chroma .gs { 925 + font-weight: bold; 926 + } 927 + /* GenericSubheading */ 928 + .chroma .gu { 929 + color: #f5a97f; 930 + font-weight: bold; 931 + } 932 + /* GenericTraceback */ 933 + .chroma .gt { 934 + color: #ed8796; 935 + } 936 + /* GenericUnderline */ 937 + .chroma .gl { 938 + text-decoration: underline; 939 + } 940 + } 941 + 942 + actor-typeahead { 943 + --color-background: #ffffff; 944 + --color-border: #d1d5db; 945 + --color-shadow: #000000; 946 + --color-hover: #f9fafb; 947 + --color-avatar-fallback: #e5e7eb; 948 + --radius: 0.0; 949 + --padding-menu: 0.0rem; 950 + z-index: 1000; 951 + } 952 + 953 + actor-typeahead::part(handle) { 954 + color: #111827; 955 + } 956 + 957 + actor-typeahead::part(menu) { 958 + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 959 + } 960 + 961 + @media (prefers-color-scheme: dark) { 962 + actor-typeahead { 963 + --color-background: #1f2937; 964 + --color-border: #4b5563; 965 + --color-shadow: #000000; 966 + --color-hover: #374151; 967 + --color-avatar-fallback: #4b5563; 968 + } 969 + 970 + actor-typeahead::part(handle) { 971 + color: #f9fafb; 972 + } 144 973 }
+1 -1
pages/blog/ci.md
··· 119 119 in your terminal if you've got Docker installed: 120 120 121 121 ``` 122 - docker run nixery.dev/bash/hello-go hello 122 + docker run nixery.dev/bash/hello-go hello-go 123 123 ``` 124 124 125 125 This should output `Hello, world!`. This is running the
+241
pages/blog/docs.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: docs 5 + title: why we rolled our own documentation site 6 + subtitle: you don't need mintlify 7 + date: 2026-01-06 8 + authors: 9 + - name: Akshay 10 + email: akshay@tangled.org 11 + handle: oppi.li 12 + draft: true 13 + --- 14 + 15 + We recently organized our documentation and put it up on 16 + https://docs.tangled.org, using just pandoc. For several 17 + reasons, using pandoc to roll your own static sites is more 18 + than sufficient for small projects. 19 + 20 + ![docs.tangled.org](/static/img/docs_homepage.png) 21 + 22 + ## requirements 23 + 24 + - Lives in [our 25 + monorepo](https://tangled.org/tangled.org/core). 26 + - No JS: a collection of pages containing just text 27 + should not require JS to view! 28 + - Searchability: in practice, documentation engines that 29 + come bundled with a search-engine have always been lack 30 + lustre. I tend to Ctrl+F or use an actual search engine in 31 + most scenarios. 32 + - Low complexity: building, testing, deploying should be 33 + easy. 34 + - Easy to style 35 + 36 + ## evaluating the ecosystem 37 + 38 + I took the time to evaluate several documentation engine 39 + solutions: 40 + 41 + - [Mintlify](https://www.mintlify.com/): It is quite obvious 42 + from their homepage that mintlify is performing an AI 43 + pivot for the sake of doing so. 44 + - [Docusaurus](https://docusaurus.io/): The generated 45 + documentation site is quite nice, but the value of pages 46 + being served as a full-blown React SPA is questionable. 47 + - [MkDocs](https://www.mkdocs.org/): Works great with JS 48 + disabled, however the table of contents needs to be 49 + maintained via `mkdocs.yml`, which can be quite tedious. 50 + - [MdBook](https://rust-lang.github.io/mdBook/index.html): 51 + As above, you need a `SUMMARY.md` file to control the 52 + table-of-contents. 53 + 54 + MkDocs and MdBook are still on my radar however, in case we 55 + need a bigger feature set. 56 + 57 + ## using pandoc 58 + 59 + [pandoc](https://pandoc.org/) is a wonderfully customizable 60 + markup converter. It provides a "chunkedhtml" output format, 61 + which is perfect for generating documentation sites. Without 62 + any customization, 63 + [this](https://pandoc.org/demo/example33/) is the generated 64 + output, for this [markdown file 65 + input](https://pandoc.org/demo/MANUAL.txt). 66 + 67 + - You get an autogenerated TOC based on the document layout 68 + - Each section is turned into a page of its own 69 + 70 + Massaging pandoc to work for us was quite straightforward: 71 + 72 + - I first combined all our individual markdown files into 73 + [one big 74 + `DOCS.md`](https://tangled.org/tangled.org/core/blob/master/docs/DOCS.md) 75 + file. 76 + - Modified the [default 77 + template](https://github.com/jgm/pandoc-templates/blob/master/default.chunkedhtml) 78 + to put the TOC on every page, to form a "sidebar", see 79 + [`docs/template.html`](https://tangled.org/tangled.org/core/blob/master/docs/template.html) 80 + - Inserted tailwind `prose` classes where necessary, such 81 + that markdown content is rendered the same way between 82 + `tangled.org` and `docs.tangled.org` 83 + 84 + Generating the docs is done with one pandoc command: 85 + 86 + ```bash 87 + pandoc docs/DOCS.md \ 88 + -o out/ \ 89 + -t chunkedhtml \ 90 + --variable toc \ 91 + --toc-depth=2 \ 92 + --css=docs/stylesheet.css \ 93 + --chunk-template="%i.html" \ 94 + --highlight-style=docs/highlight.theme \ 95 + --template=docs/template.html 96 + ``` 97 + 98 + ## avoiding javascript 99 + 100 + The "sidebar" style table-of-contents needs to be collapsed 101 + on mobile displays. Most of the engines I evaluated seem to 102 + require JS to collapse and expand the sidebar, with MkDocs 103 + being the outlier, it uses a checkbox with the 104 + [`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:checked) 105 + pseudo-class trick to avoid JS. 106 + 107 + The other ways to do this are: 108 + 109 + - Use `<details` and `<summary>`: this is definitely a 110 + "hack", clicking outside the sidebar does not collapse it. 111 + Using Ctrl+F or "Find in page" still works through the 112 + details tag though. 113 + - Use the new `popover` API: this seems like the perfect fit 114 + for a "sidebar" component. 115 + 116 + The bar at the top includes a button to trigger the popover: 117 + 118 + ```html 119 + <button popovertarget="toc-popover">Table of Contents</button> 120 + ``` 121 + 122 + And a `fixed` position div includes the TOC itself: 123 + 124 + ```html 125 + <div id="toc-popover" popover class="fixed top-0"> 126 + <ul> 127 + Quick Start 128 + <li>...</li> 129 + <li>...</li> 130 + <li>...</li> 131 + </ul> 132 + </div> 133 + ``` 134 + 135 + The TOC is scrollable independently and can be collapsed by 136 + clicking anywhere on the screen outside the sidebar. 137 + Searching for content in the page via "Find in page" does 138 + not show any results that are present in the popover 139 + however. The collapsible TOC is only available on smaller 140 + viewports, the TOC is not hidden on larger viewports. 141 + 142 + ## search 143 + 144 + There is native search on the site for now. Taking 145 + inspiration from https://htmx.org's search bar, our search 146 + bar also simply redirects to Google: 147 + 148 + ``` 149 + <form action="https://google.com/search"> 150 + <input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]"> 151 + ... 152 + </form> 153 + ``` 154 + 155 + I mentioned earlier that Ctrl+F has typically worked better 156 + for me than, say, the search engine provided by Docusaurus. 157 + To that end, the same docs have been exported to a ["single 158 + page" format](https://docs.tangled.org/single-page.html), by 159 + just removing the `chunkedhtml` related options: 160 + 161 + ```diff 162 + pandoc docs/DOCS.md \ 163 + -o out/ \ 164 + - -t chunkedhtml \ 165 + --variable toc \ 166 + --toc-depth=2 \ 167 + --css=docs/stylesheet.css \ 168 + - --chunk-template="%i.html" \ 169 + --highlight-style=docs/highlight.theme \ 170 + --template=docs/template.html 171 + ``` 172 + 173 + With all the content on a single page, it is trivial to 174 + search through the entire site with the browser. If the docs 175 + do outgrow this, I will consider other options! 176 + 177 + ## building and deploying 178 + 179 + We use [nix](https://nixos.org) and 180 + [colmena](https://colmena.cli.rs/) to build and deploy all 181 + Tangled services. A nix derivation to [build the 182 + documentation](https://tangled.org/tangled.org/core/blob/master/nix/pkgs/docs.nix) 183 + site is written very easily with the `runCommandLocal` 184 + helper: 185 + 186 + ```nix 187 + runCommandLocal "docs" {} '' 188 + . 189 + . 190 + . 191 + ${pandoc}/bin/pandoc ${src}/docs/DOCS.md ... 192 + . 193 + . 194 + . 195 + '' 196 + ``` 197 + 198 + The nixos machine is configured to serve the site [via 199 + nginx](https://tangled.org/tangled.org/infra/blob/master/hosts/nixery/services/nginx.nix#L7): 200 + 201 + ```nix 202 + services.nginx = { 203 + enable = true; 204 + virtualHosts = { 205 + "docs.tangled.org" = { 206 + root = "${tangled-pkgs.docs}"; 207 + locations."/" = { 208 + tryFiles = "$uri $uri/ =404"; 209 + index = "index.html"; 210 + }; 211 + }; 212 + }; 213 + }; 214 + ``` 215 + 216 + And deployed using `colmena`: 217 + 218 + ```bash 219 + nix run nixpkgs#colmena -- apply 220 + ``` 221 + 222 + To update the site, I first run: 223 + 224 + ```bash 225 + nix flake update tangled 226 + ``` 227 + 228 + Which bumps the `tangled` flake input, and thus 229 + `tangled-pkgs.docs`. The above `colmena` invocation applies 230 + the changes to the machine serving the site. 231 + 232 + ## notes 233 + 234 + Going homegrown has made it a lot easier to style the 235 + documentation site to match the main site. Unfortunately 236 + there are still a few discrepancies between pandoc's 237 + markdown rendering and 238 + [goldmark's](https://pkg.go.dev/github.com/yuin/goldmark/) 239 + markdown rendering (which is what we use in Tangled). We may 240 + yet roll our own SSG, 241 + [TigerStyle](https://tigerbeetle.com/blog/2025-02-27-why-we-designed-tigerbeetles-docs-from-scratch/)!
static/img/docs_homepage.png

This is a binary file and will not be displayed.

+30 -30
templates/index.html
··· 10 10 {{ .Meta.title }} 11 11 </title> 12 12 13 - <body class="bg-slate-100 dark:bg-gray-900 flex flex-col min-h-screen"> 14 - {{ template "partials/nav.html" }} 15 - <div class="prose dark:prose-invert mx-auto px-1 pt-4 flex-grow flex flex-col container"> 16 - <main> 17 - <header class="px-6"> 18 - <h1 class="mb-0">{{ index .Meta "title" }}</h1> 19 - <h2 class="font-light mt-1 mb-0 text-lg">{{ index .Meta "subtitle" }}</h2> 20 - </header> 13 + <body class="bg-slate-100 dark:bg-gray-900 flex flex-col items-center min-h-screen"> 14 + {{ template "partials/nav.html" }} 15 + <div class="px-1 pt-4 flex-grow flex flex-col w-full max-w-[75ch]"> 16 + <main> 17 + <header class="px-6"> 18 + <h1 class="mb-0 text-2xl font-bold text-black dark:text-white">{{ index .Meta "title" }}</h1> 19 + <h2 class="font-light text-gray-600 dark:text-gray-400 mt-1 mb-0 text-lg">{{ index .Meta "subtitle" }}</h2> 20 + </header> 21 21 22 - {{ .Body }} 22 + {{ .Body }} 23 23 24 - <section class="py-4"> 25 - <ul class="px-0"> 24 + <section class="py-4"> 25 + <ul class="px-0 space-y-4"> 26 26 {{ $posts := .Extra.blog }} 27 27 {{ range $posts }} 28 - <li class="mt-5 bg-white dark:bg-gray-800 py-4 px-6 rounded drop-shadow-sm list-none"> 29 - {{ $dateStr := .Meta.date }} 30 - {{ $date := parsedate $dateStr }} 31 - <div class="post-date py-1 mb-0 text-sm">{{ $date.Format "02 Jan, 2006" }}</div> 32 - <div> 33 - <a class="title mb-0 text-xl no-underline font-bold" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 34 - {{ if .Meta.draft }} 35 - <span class="text-red-500">[draft]</span> 36 - {{ end }} 37 - <p class="italic mt-1 mb-0">{{ .Meta.subtitle }}</p> 38 - </div> 39 - </li> 28 + <li class="bg-white dark:bg-gray-800 py-4 px-6 rounded drop-shadow-sm list-none"> 29 + {{ $dateStr := .Meta.date }} 30 + {{ $date := parsedate $dateStr }} 31 + <div class="post-date py-1 mb-0 text-sm text-gray-600 dark:text-gray-400">{{ $date.Format "02 Jan, 2006" }}</div> 32 + <div> 33 + <a class="title mb-0 text-xl no-underline font-bold" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 34 + {{ if .Meta.draft }} 35 + <span class="text-red-500">[draft]</span> 36 + {{ end }} 37 + <p class="italic mt-1 mb-0 text-gray-600 dark:text-gray-400">{{ .Meta.subtitle }}</p> 38 + </div> 39 + </li> 40 40 {{ end }} 41 41 </ul> 42 - </section> 43 - </main> 44 - </div> 45 - <footer class="w-full"> 46 - {{ template "partials/footer.html" }} 47 - </footer> 48 - </body> 42 + </section> 43 + </main> 44 + </div> 45 + <footer class="w-full"> 46 + {{ template "partials/footer.html" }} 47 + </footer> 48 + </body> 49 49 50 50 </html>
+5 -9
templates/partials/nav.html
··· 1 - <div class="w-full"> 2 - <div class="container mx-auto max-w-7xl px-4"> 3 - <div class="flex justify-start py-4 mb-8"> 4 - <a href="/" class="text-2xl no-underline hover:no-underline"> 5 - {{ template "fragments/logotypeSmall" }} 6 - </a> 7 - </div> 8 - </div> 9 - </div> 1 + <nav class="w-full flex items-center dark:text-white drop-shadow-sm bg-white dark:bg-gray-800 px-6 py-2 h-[44px]"> 2 + <a href="/" class="text-2xl no-underline hover:no-underline"> 3 + {{ template "fragments/logotypeSmall" }} 4 + </a> 5 + </nav>
+10 -2
templates/text.html
··· 5 5 <meta name="description" content="{{ index .Meta "subtitle" }}"> 6 6 <meta property="og:title" content="{{ .Meta.title }}" /> 7 7 <meta property="og:description" content="{{ .Meta.subtitle }}" /> 8 - <meta property="og:url" content="https://blog.tangled.sh/{{ .Meta.slug }}" /> 9 - <meta property="og:image" content="{{ .Meta.image }}" /> 8 + <meta property="og:url" content="https://blog.tangled.org/{{ .Meta.slug }}" /> 9 + <meta property="og:image" content="https://blog.tangled.org{{ .Meta.image }}" /> 10 10 <meta property="og:type" content="website" /> 11 + <meta property="og:image:width" content="1200" /> 12 + <meta property="og:image:height" content="630" /> 13 + 14 + <meta name="twitter:card" content="summary_large_image" /> 15 + <meta name="twitter:title" content="{{ .Meta.title }}" /> 16 + <meta name="twitter:description" content="{{ .Meta.subtitle }}" /> 17 + <meta name="twitter:image" content="https://blog.tangled.org{{ .Meta.image }}" /> 18 + 11 19 <title> 12 20 {{ index .Meta "title" }} 13 21 </title>