From 72a901028e0c5bc4ad1961c26de4febcf993f89f Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Wed, 25 Feb 2026 23:33:02 -0700 Subject: [PATCH 1/7] feat: implement basic blog --- blog/index.html | 57 +++ bun.lock | 118 +++++ package.json | 4 + src/components/blog/obscure-blog-app.ts | 91 ++++ src/components/blog/obscure-blog-list.ts | 93 ++++ src/components/blog/obscure-blog-post.ts | 96 ++++ src/components/obscure-app.ts | 57 +-- src/content/posts/hello-world.md | 57 +++ src/index.css | 1 + src/plugins/extensions/highlight.ts | 44 ++ src/plugins/extensions/infobox.ts | 39 ++ src/plugins/vite-plugin-blog.ts | 122 ++++++ src/scripts/effects.ts | 69 +++ src/styles/blog.css | 534 +++++++++++++++++++++++ src/types/blog.d.ts | 31 ++ vite.config.ts | 29 ++ 16 files changed, 1388 insertions(+), 54 deletions(-) create mode 100644 blog/index.html create mode 100644 src/components/blog/obscure-blog-app.ts create mode 100644 src/components/blog/obscure-blog-list.ts create mode 100644 src/components/blog/obscure-blog-post.ts create mode 100644 src/content/posts/hello-world.md create mode 100644 src/plugins/extensions/highlight.ts create mode 100644 src/plugins/extensions/infobox.ts create mode 100644 src/plugins/vite-plugin-blog.ts create mode 100644 src/scripts/effects.ts create mode 100644 src/styles/blog.css create mode 100644 src/types/blog.d.ts create mode 100644 vite.config.ts diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 0000000..e718c38 --- /dev/null +++ b/blog/index.html @@ -0,0 +1,57 @@ + + + + + + + + + blog — obscure computer + + + + + + + + + + + + + + + + + + + + + diff --git a/bun.lock b/bun.lock index 990367f..5a2dc02 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,16 @@ "name": "site", "dependencies": { "@types/three": "^0.182.0", + "gray-matter": "^4.0.3", "gsap": "^3.14.2", "lenis": "^1.3.17", "lit": "^3.3.1", + "marked": "^17.0.3", + "shiki": "^3.23.0", "three": "^0.182.0", }, "devDependencies": { + "@types/node": "^25.3.1", "typescript": "~5.9.3", "vite": "^7.2.4", }, @@ -120,30 +124,86 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], "@types/three": ["@types/three@0.182.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@webgpu/types": ["@webgpu/types@0.1.68", "", {}, "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + "gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "lenis": ["lenis@1.3.17", "", { "peerDependencies": { "@nuxt/kit": ">=3.0.0", "react": ">=17.0.0", "vue": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "react", "vue"] }, "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw=="], "lit": ["lit@3.3.2", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ=="], @@ -152,26 +212,84 @@ "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], + "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "meshoptimizer": ["meshoptimizer@0.22.0", "", {}, "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], } } diff --git a/package.json b/package.json index ba83fb9..3fa35e0 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,16 @@ }, "dependencies": { "@types/three": "^0.182.0", + "gray-matter": "^4.0.3", "gsap": "^3.14.2", "lenis": "^1.3.17", "lit": "^3.3.1", + "marked": "^17.0.3", + "shiki": "^3.23.0", "three": "^0.182.0" }, "devDependencies": { + "@types/node": "^25.3.1", "typescript": "~5.9.3", "vite": "^7.2.4" } diff --git a/src/components/blog/obscure-blog-app.ts b/src/components/blog/obscure-blog-app.ts new file mode 100644 index 0000000..26350f7 --- /dev/null +++ b/src/components/blog/obscure-blog-app.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { initParticles, initCursor } from "../../scripts/effects"; + +import "./obscure-blog-list"; +import "./obscure-blog-post"; + +@customElement("obscure-blog-app") +export class ObscureBlogApp extends LitElement { + @state() + private _slug = ""; + + connectedCallback() { + super.connectedCallback(); + this._slug = location.hash.slice(1); + window.addEventListener("hashchange", this._onHashChange); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("hashchange", this._onHashChange); + } + + private _onHashChange = () => { + this._slug = location.hash.slice(1); + window.scrollTo(0, 0); + }; + + render() { + return html` +
+
+
+
+
+ +
+ ${this._slug + ? html`` + : html``} +
+ `; + } + + firstUpdated() { + initParticles(this.renderRoot); + initCursor(this.renderRoot, "a, button, .blog-post-card"); + this._initCopyButtons(); + } + + updated() { + this._initCopyButtons(); + } + + private _initCopyButtons() { + this.renderRoot.querySelectorAll(".code-copy").forEach((btn) => { + if (btn.dataset.bound) return; + btn.dataset.bound = "1"; + btn.addEventListener("click", () => { + const code = btn.closest(".code-block")?.querySelector("code"); + if (!code) return; + const original = btn.innerHTML; + navigator.clipboard.writeText(code.textContent ?? "").then(() => { + btn.innerHTML = ''; + btn.style.color = "var(--c-cyan)"; + setTimeout(() => { btn.innerHTML = original; btn.style.color = ""; }, 1500); + }); + }); + }); + } + + createRenderRoot() { + return this; + } +} diff --git a/src/components/blog/obscure-blog-list.ts b/src/components/blog/obscure-blog-list.ts new file mode 100644 index 0000000..a031127 --- /dev/null +++ b/src/components/blog/obscure-blog-list.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LitElement, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import posts from "virtual:blog-posts"; + +@customElement("obscure-blog-list") +export class ObscureBlogList extends LitElement { + render() { + return html` + + `; + } + + createRenderRoot() { + return this; + } +} diff --git a/src/components/blog/obscure-blog-post.ts b/src/components/blog/obscure-blog-post.ts new file mode 100644 index 0000000..76840cb --- /dev/null +++ b/src/components/blog/obscure-blog-post.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import posts from "virtual:blog-posts"; + +@customElement("obscure-blog-post") +export class ObscureBlogPost extends LitElement { + @property() + slug = ""; + + private get _post(): BlogPostFull | undefined { + return posts.find((p) => p.slug === this.slug); + } + + private get _readTime(): number { + if (!this._post) return 0; + const text = this._post.html.replace(/<[^>]*>/g, ""); + const words = text.trim().split(/\s+/).length; + return Math.max(1, Math.round(words / 200)); + } + + render() { + if (!this._post) { + return html` +
+ +
+

POST_NOT_FOUND

+

No post matching slug "${this.slug}"

+
+
+ `; + } + + return html` +
+ + +
+
+

${this._post.title}

+ +
+ ${this._post.tags.map( + (tag) => + html`${tag}` + )} +
+
+ +
+ ${unsafeHTML(this._post.html)} +
+
+
+ `; + } + + createRenderRoot() { + return this; + } +} diff --git a/src/components/obscure-app.ts b/src/components/obscure-app.ts index a81edc9..f965137 100644 --- a/src/components/obscure-app.ts +++ b/src/components/obscure-app.ts @@ -17,6 +17,7 @@ import { customElement, state } from "lit/decorators.js"; import Lenis from "lenis"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { initParticles, initCursor } from "../scripts/effects"; gsap.registerPlugin(ScrollTrigger); @@ -115,31 +116,13 @@ export class ObscureApp extends LitElement { private _onLoaded() { this.loaded = true; this.updateComplete.then(() => { - this._initParticles(); + initParticles(this.renderRoot); this._initAnimations(); setTimeout(() => ScrollTrigger.refresh(), 100); }); } - private _initParticles() { - const container = this.renderRoot.querySelector("#particles"); - if (!container) return; - - const colors = ["var(--c-cyan)", "var(--c-pink)", "var(--c-yellow)"]; - - for (let i = 0; i < 15; i++) { - const p = document.createElement("div"); - p.className = "particle"; - p.style.left = Math.random() * 100 + "vw"; - p.style.animationDelay = Math.random() * 15 + "s"; - p.style.setProperty("--drift", (Math.random() - 0.5) * 100 + "px"); - p.style.background = - colors[Math.floor(Math.random() * colors.length)]; - container.appendChild(p); - } - } - private _initAnimations() { gsap.to(".hero-text-animation", { y: 0, @@ -228,7 +211,7 @@ export class ObscureApp extends LitElement { ScrollTrigger.refresh(); - this._initCursor(); + initCursor(this.renderRoot, "a, button, .project-item, .member-row"); this._initMembers(); this._initModal(); } @@ -362,40 +345,6 @@ export class ObscureApp extends LitElement { }); } - private _initCursor() { - const dot = this.renderRoot.querySelector(".cursor-dot") as HTMLElement; - const outline = this.renderRoot.querySelector( - ".cursor-outline" - ) as HTMLElement; - - if (!dot || !outline) return; - - dot.style.opacity = "0"; - outline.style.opacity = "0"; - document.body.style.cursor = "none"; - - window.addEventListener("mousemove", (e) => { - if (this.loaded && dot.style.opacity === "0") { - dot.style.opacity = "1"; - outline.style.opacity = "1"; - } - gsap.to(dot, { x: e.clientX, y: e.clientY, duration: 0 }); - gsap.to(outline, { x: e.clientX, y: e.clientY, duration: 0.15 }); - }); - - const hoverables = this.renderRoot.querySelectorAll( - "a, button, .project-item, .member-row" - ); - hoverables.forEach((el) => { - el.addEventListener("mouseenter", () => - outline.classList.add("hovered") - ); - el.addEventListener("mouseleave", () => - outline.classList.remove("hovered") - ); - }); - } - createRenderRoot() { return this; } diff --git a/src/content/posts/hello-world.md b/src/content/posts/hello-world.md new file mode 100644 index 0000000..8ce0c57 --- /dev/null +++ b/src/content/posts/hello-world.md @@ -0,0 +1,57 @@ +--- +title: "test" +date: 2026-02-25 +description: "abc test post" +tags: ["meta", "announcements"] +author: "the obscure computer club" +--- + +This is text + +This is _underlined_ text + +This is **bold** text + +This is ~~striked~~ text + +This is a quote +> The best way to predict the future is to invent it
+> \- Alan Kay + +## Heading 2 + +### Heading 3 + +#### Heading 4 + +- List item + - List item 2 + +1. Numbered item +2. Numbered item 2 + + +| Header 1 | Header 2 | Header 3 | +|--------------|--------------|--------------| +| Row 1, Col 1 | Row 1, Col 2 | Row 1, Col 3 | +| Row 2, Col 1 | Row 2, Col 2 | Row 2, Col 3 | + +:::important +do NOT turn on the blender after 4pm +::: + +```kt +class MyNative : TwineNative("mylib") { + @TwineNativeFunction + @TwineOverload // <- Mark both with this + fun doThing(text: String): String { + return "Got string: $text" + } + + @TwineNativeFunction + @TwineOverload // <- Same annotation + fun doThing(number: Int): Int { + return number * 2 + } +} +``` \ No newline at end of file diff --git a/src/index.css b/src/index.css index 22830fd..c51d42f 100644 --- a/src/index.css +++ b/src/index.css @@ -18,3 +18,4 @@ @import "./styles/animations.css"; @import "./styles/components.css"; @import "./styles/layout.css"; +@import "./styles/blog.css"; diff --git a/src/plugins/extensions/highlight.ts b/src/plugins/extensions/highlight.ts new file mode 100644 index 0000000..663f030 --- /dev/null +++ b/src/plugins/extensions/highlight.ts @@ -0,0 +1,44 @@ +import { createHighlighter } from "shiki"; +import type { MarkedExtension } from "marked"; + +const highlighter = await createHighlighter({ + themes: ["vitesse-dark"], + langs: [ + "kotlin", + "yaml", + "typescript", + "javascript", + "bash", + "html", + "css", + "java", + "json", + "lua", + "markdown", + ], +}); + +export const highlightExtension: MarkedExtension = { + renderer: { + code({ text, lang }) { + const label = lang || "text"; + let html: string; + try { + html = highlighter.codeToHtml(text, { + lang: label, + theme: "vitesse-dark", + }); + } catch { + html = highlighter.codeToHtml(text, { + lang: "text", + theme: "vitesse-dark", + }); + } + html = html.replace( + /^
${html}`;
+        },
+    },
+};
diff --git a/src/plugins/extensions/infobox.ts b/src/plugins/extensions/infobox.ts
new file mode 100644
index 0000000..f122890
--- /dev/null
+++ b/src/plugins/extensions/infobox.ts
@@ -0,0 +1,39 @@
+import type { TokenizerAndRendererExtension, Tokens } from 'marked';
+
+interface AlertToken extends Tokens.Generic {
+  type: 'alert';
+  alertType: string;
+  text: string;
+}
+
+export const infoboxExtension: TokenizerAndRendererExtension = {
+  name: 'alert',
+  level: 'block',
+  start(src) { return src.match(/:::(info|note|tip|important|warning|caution)\n/i)?.index; },
+  tokenizer(src) {
+    const rule = /^:::(info|note|tip|important|warning|caution)\n([\s\S]*?)\n:::\s*(?:\n|$)/i;
+    const match = rule.exec(src);
+
+    if (match) {
+      const alertType = match[1].toUpperCase();
+      const text = match[2].trim();
+
+      return {
+        type: 'alert',
+        raw: match[0],
+        alertType,
+        text,
+        tokens: this.lexer.blockTokens(text, [])
+      };
+    }
+  },
+  renderer(token) {
+    const t = token as unknown as AlertToken;
+    const body = this.parser.parse(t.tokens ?? []);
+    return `
+      
+

${t.alertType}

+ ${body} +
`; + } +}; diff --git a/src/plugins/vite-plugin-blog.ts b/src/plugins/vite-plugin-blog.ts new file mode 100644 index 0000000..f67bf56 --- /dev/null +++ b/src/plugins/vite-plugin-blog.ts @@ -0,0 +1,122 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from "node:fs"; +import path from "node:path"; +import matter from "gray-matter"; +import { marked } from "marked"; +import type { Plugin } from "vite"; +import { infoboxExtension } from "./extensions/infobox.ts"; +import { highlightExtension } from "./extensions/highlight.ts"; + +interface PostMeta { + slug: string; + title: string; + date: string; + description: string; + tags: string[]; + author: string; +} + +interface PostFull extends PostMeta { + html: string; +} + +const POSTS_DIR = path.resolve("src/content/posts"); +const VIRTUAL_LIST = "virtual:blog-posts"; +const RESOLVED_LIST = "\0" + VIRTUAL_LIST; + +function getSlug(filename: string): string { + return filename.replace(/\.md$/, ""); +} + +marked.use({ extensions: [infoboxExtension] }); +marked.use(highlightExtension); + +function parsePost(filePath: string): PostFull { + const raw = fs.readFileSync(filePath, "utf-8"); + const { data, content } = matter(raw); + const html = marked.parse(content) as string; + const slug = getSlug(path.basename(filePath)); + + return { + slug, + title: data.title ?? slug, + date: data.date ? new Date(data.date).toISOString().split("T")[0] : "", + description: data.description ?? "", + tags: data.tags ?? [], + author: data.author ?? "", + html, + }; +} + +function getAllPosts(): PostFull[] { + if (!fs.existsSync(POSTS_DIR)) return []; + + return fs + .readdirSync(POSTS_DIR) + .filter((f: string) => f.endsWith(".md")) + .map((f: string) => parsePost(path.join(POSTS_DIR, f))) + .sort((a: PostFull, b: PostFull) => (a.date > b.date ? -1 : 1)); +} + +export default function blogPlugin(): Plugin { + return { + name: "vite-plugin-blog", + + resolveId(id) { + if (id === VIRTUAL_LIST) return RESOLVED_LIST; + return null; + }, + + load(id) { + if (id === RESOLVED_LIST) { + const posts = getAllPosts(); + return `export default ${JSON.stringify(posts)};`; + } + + return null; + }, + + configureServer(server) { + if (!fs.existsSync(POSTS_DIR)) return; + + server.watcher.add(POSTS_DIR); + + server.watcher.on("change", (filePath) => { + if (filePath.startsWith(POSTS_DIR) && filePath.endsWith(".md")) { + const mod = server.moduleGraph.getModuleById(RESOLVED_LIST); + if (mod) server.moduleGraph.invalidateModule(mod); + server.ws.send({ type: "full-reload" }); + } + }); + + server.watcher.on("add", (filePath) => { + if (filePath.startsWith(POSTS_DIR) && filePath.endsWith(".md")) { + const mod = server.moduleGraph.getModuleById(RESOLVED_LIST); + if (mod) server.moduleGraph.invalidateModule(mod); + server.ws.send({ type: "full-reload" }); + } + }); + + server.watcher.on("unlink", (filePath) => { + if (filePath.startsWith(POSTS_DIR) && filePath.endsWith(".md")) { + const mod = server.moduleGraph.getModuleById(RESOLVED_LIST); + if (mod) server.moduleGraph.invalidateModule(mod); + server.ws.send({ type: "full-reload" }); + } + }); + }, + }; +} diff --git a/src/scripts/effects.ts b/src/scripts/effects.ts new file mode 100644 index 0000000..819b1a0 --- /dev/null +++ b/src/scripts/effects.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import gsap from "gsap"; + +export function initParticles(root: ParentNode) { + const container = root.querySelector("#particles"); + if (!container) return; + + const colors = ["var(--c-cyan)", "var(--c-pink)", "var(--c-yellow)"]; + + for (let i = 0; i < 15; i++) { + const p = document.createElement("div"); + p.className = "particle"; + p.style.left = Math.random() * 100 + "vw"; + p.style.animationDelay = Math.random() * 15 + "s"; + p.style.setProperty("--drift", (Math.random() - 0.5) * 100 + "px"); + p.style.background = colors[Math.floor(Math.random() * colors.length)]; + container.appendChild(p); + } +} + +export function initCursor( + root: ParentNode, + hoverSelector: string, +) { + const dot = root.querySelector(".cursor-dot") as HTMLElement; + const outline = root.querySelector(".cursor-outline") as HTMLElement; + + if (!dot || !outline) return; + + dot.style.opacity = "0"; + outline.style.opacity = "0"; + document.body.style.cursor = "none"; + + window.addEventListener("mousemove", (e) => { + if (dot.style.opacity === "0") { + dot.style.opacity = "1"; + outline.style.opacity = "1"; + } + gsap.to(dot, { x: e.clientX, y: e.clientY, duration: 0 }); + gsap.to(outline, { x: e.clientX, y: e.clientY, duration: 0.15 }); + }); + + document.addEventListener("mouseover", (e) => { + const target = e.target as HTMLElement; + if (target.closest(hoverSelector)) { + outline.classList.add("hovered"); + } + }); + + document.addEventListener("mouseout", (e) => { + const target = e.target as HTMLElement; + if (target.closest(hoverSelector)) { + outline.classList.remove("hovered"); + } + }); +} diff --git a/src/styles/blog.css b/src/styles/blog.css new file mode 100644 index 0000000..2d3f5a1 --- /dev/null +++ b/src/styles/blog.css @@ -0,0 +1,534 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Blog Layout */ +.blog-main { + min-height: 100vh; + position: relative; + z-index: 2; +} + +.blog-container { + padding: 0 4vw; + max-width: 900px; + margin: 0 auto; +} + +/* Blog Nav */ +.blog-nav { + padding: 2rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.blog-back-link { + font-family: var(--font-mono); + font-size: 1rem; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--c-white); + transition: color 0.2s; +} + +.blog-back-link:hover { + color: var(--c-cyan); +} + +.blog-back-link .bracket { + color: var(--c-cyan); + opacity: 0.5; + transition: opacity 0.2s; +} + +.blog-back-link:hover .bracket { + opacity: 1; +} + +/* Blog Header */ +.blog-header { + padding: 8vh 0 4vh; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + margin-bottom: 4rem; +} + +.blog-title { + font-family: var(--font-display); + font-size: 5vw; + color: var(--c-white); + text-transform: uppercase; + margin-bottom: 1rem; +} + +.blog-header-meta { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: #666; + letter-spacing: 1px; +} + +.blog-header-dot { + display: inline-block; + width: 8px; + height: 8px; + background: var(--c-cyan); + animation: statusBlink 1.5s infinite steps(2); +} + +/* Blog Post Cards */ +.blog-list { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + padding-bottom: 10vh; +} + +.blog-post-card { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem; + border: 1px solid #333; + background: rgba(0, 0, 0, 0.6); + transition: all 0.3s var(--easing); + overflow: hidden; + text-decoration: none; + color: inherit; +} + +.blog-post-card::before, +.blog-post-card::after { + content: ""; + position: absolute; + width: 10px; + height: 10px; + border: 2px solid transparent; + transition: all 0.3s ease; +} + +.blog-post-card::before { + top: 0; + left: 0; + border-top-color: var(--c-white); + border-left-color: var(--c-white); +} + +.blog-post-card::after { + bottom: 0; + right: 0; + border-bottom-color: var(--c-white); + border-right-color: var(--c-white); +} + +.blog-post-card:hover { + border-color: var(--c-cyan); + transform: translateX(10px); +} + +.blog-post-card:hover::before, +.blog-post-card:hover::after { + width: 100%; + height: 100%; + border-color: var(--c-cyan); + opacity: 0.1; + background: var(--c-cyan); +} + +.blog-post-card-content { + z-index: 2; + flex: 1; +} + +.blog-post-card-title { + font-family: var(--font-display); + font-size: 1.4rem; + color: var(--c-white); + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.blog-post-card-desc { + font-family: var(--font-mono); + font-size: 1rem; + color: #999; + margin-bottom: 0.75rem; + line-height: 1.4; +} + +.blog-post-card-meta { + font-family: var(--font-mono); + font-size: 0.85rem; + color: #555; + letter-spacing: 1px; + display: flex; + gap: 1.5rem; + margin-bottom: 0.5rem; +} + +.blog-post-card-tags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.blog-tag { + display: inline-block; + border: 1px solid #333; + padding: 0.15rem 0.5rem; + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--c-yellow); + background: var(--c-off-black); + text-transform: uppercase; + letter-spacing: 1px; +} + +.blog-post-card-arrow { + z-index: 2; + opacity: 0.2; + transition: opacity 0.3s, transform 0.3s, color 0.3s; + margin-left: 1rem; + flex-shrink: 0; +} + +.blog-post-card:hover .blog-post-card-arrow { + opacity: 1; + transform: translateX(5px); + color: var(--c-cyan); +} + +/* Blog Article View */ +.blog-article { + padding: 4vh 0 10vh; +} + +.blog-article-header { + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.blog-article-title { + font-family: var(--font-display); + font-size: 2.5rem; + color: var(--c-white); + text-transform: uppercase; + margin-bottom: 1rem; + line-height: 1.4; +} + +.blog-article-meta { + font-family: var(--font-mono); + font-size: 0.9rem; + color: #666; + letter-spacing: 1px; + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.blog-article-meta span + span::before { + content: "|"; + margin-right: 1rem; + opacity: 0.3; +} + +/* Blog Article Body Typography */ +.blog-article-body { + font-family: var(--font-mono); + font-size: 1.2rem; + line-height: 1.7; + color: var(--c-white); +} + +.blog-article-body h2 { + font-family: var(--font-display); + font-size: 1.4rem; + color: var(--c-cyan); + text-transform: uppercase; + margin: 3rem 0 1.5rem; +} + +.blog-article-body h3 { + font-family: var(--font-display); + font-size: 1rem; + color: var(--c-white); + text-transform: uppercase; + margin: 2.5rem 0 1rem; +} + +.blog-article-body p { + margin-bottom: 1.2rem; +} + +.blog-article-body strong { + color: var(--c-cyan); +} + +.blog-article-body a { + color: var(--c-cyan); + text-decoration: underline; + text-underline-offset: 3px; + transition: color 0.2s; +} + +.blog-article-body a:hover { + color: var(--c-pink); +} + +.blog-article-body ul, +.blog-article-body ol { + margin: 0 0 1.2rem 1.5rem; +} + +.blog-article-body li { + margin-bottom: 0.4rem; +} + +.blog-article-body li::marker { + color: var(--c-cyan); +} + +.blog-article-body blockquote { + border-left: 3px solid var(--c-cyan); + margin: 2rem 0; + padding: 1rem 1.5rem; + background: rgba(0, 255, 208, 0.03); + color: #aaa; + font-style: italic; +} + +.blog-article-body code { + font-family: var(--font-mono); + background: var(--c-off-black); + border: 1px solid #333; + padding: 0.15rem 0.4rem; + font-size: 0.95em; + color: var(--c-yellow); +} + +.blog-article-body pre, +.blog-article-body .shiki { + background: var(--c-off-black) !important; + border: 1px solid #333; + padding: 1.5rem; + margin: 1.5rem 0; + overflow-x: auto; + position: relative; +} + +.blog-article-body pre::before, +.blog-article-body .shiki::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: var(--c-cyan); +} + +.blog-article-body pre code, +.blog-article-body .shiki code { + background: none; + border: none; + padding: 0; + font-size: 1rem; + color: var(--c-white); + line-height: 1.6; +} + +.blog-article-body .shiki code span { + font-family: var(--font-mono); +} + +.code-block { + position: relative; +} + +.code-copy { + position: absolute; + bottom: 0.5rem; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: var(--c-off-black); + border: 1px solid #333; + color: #555; + transition: color 0.2s, border-color 0.2s; +} + +.code-copy:hover { + color: var(--c-cyan); + border-color: var(--c-cyan); +} + +.code-copy svg { + pointer-events: none; +} + +.blog-article-body pre[data-lang]::after { + content: attr(data-lang); + position: absolute; + top: 0; + right: 0; + padding: 0.25rem 0.6rem; + font-family: var(--font-display); + font-size: 0.55rem; + text-transform: uppercase; + letter-spacing: 2px; + color: #333; + user-select: none; +} + +.blog-article-body hr { + border: none; + height: 1px; + background: linear-gradient(90deg, transparent, var(--c-white), transparent); + opacity: 0.2; + margin: 3rem 0; +} + +.blog-article-body img { + max-width: 100%; + border: 1px solid #333; +} + +.blog-article-body table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; + font-family: var(--font-mono); + font-size: 1rem; +} + +.blog-article-body th { + font-family: var(--font-display); + font-size: 0.7rem; + text-transform: uppercase; + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 2px solid var(--c-cyan); + color: var(--c-cyan); +} + +.blog-article-body td { + padding: 0.75rem 1rem; + border-bottom: 1px solid #222; +} + +.blog-article-body tr:hover td { + background: rgba(0, 255, 208, 0.03); +} + +/* Infoboxes */ +.markdown-infobox { + border: 1px solid #333; + padding: 1.5rem; + margin: 1.5rem 0; + position: relative; + border-left: 3px solid var(--c-cyan); + background: rgba(0, 255, 208, 0.03); +} + +.markdown-infobox-title { + font-family: var(--font-display); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 2px; + margin-bottom: 0.75rem; + color: var(--c-cyan); +} + +.markdown-infobox-warning { + border-left-color: var(--c-yellow); + background: rgba(255, 190, 11, 0.03); +} + +.markdown-infobox-warning .markdown-infobox-title { + color: var(--c-yellow); +} + +.markdown-infobox-caution, +.markdown-infobox-important { + border-left-color: var(--c-pink); + background: rgba(255, 0, 110, 0.03); +} + +.markdown-infobox-caution .markdown-infobox-title, +.markdown-infobox-important .markdown-infobox-title { + color: var(--c-pink); +} + +.markdown-infobox p:last-child { + margin-bottom: 0; +} + +/* Loading & Error States */ +.blog-loading { + font-family: var(--font-mono); + font-size: 1.2rem; + color: var(--c-cyan); + text-align: center; + padding: 20vh 0; + animation: blink 0.8s infinite steps(2); +} + +.blog-error { + text-align: center; + padding: 20vh 0; +} + +.blog-error h1 { + font-family: var(--font-display); + font-size: 2rem; + color: var(--c-pink); + margin-bottom: 1rem; +} + +.blog-error p { + font-family: var(--font-mono); + font-size: 1.2rem; + color: #666; +} + +/* Responsive */ +@media (max-width: 768px) { + .blog-title { + font-size: 8vw; + } + + .blog-article-title { + font-size: 1.5rem; + } + + .blog-post-card { + flex-direction: column; + align-items: flex-start; + } + + .blog-post-card-arrow { + display: none; + } + + .blog-post-card-meta { + flex-direction: column; + gap: 0.25rem; + } +} diff --git a/src/types/blog.d.ts b/src/types/blog.d.ts new file mode 100644 index 0000000..a567d6d --- /dev/null +++ b/src/types/blog.d.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface BlogPostMeta { + slug: string; + title: string; + date: string; + description: string; + tags: string[]; + author: string; +} + +interface BlogPostFull extends BlogPostMeta { + html: string; +} + +declare module "virtual:blog-posts" { + const posts: BlogPostFull[]; + export default posts; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..512f5b8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from "vite"; +import { resolve } from "node:path"; +import blogPlugin from "./src/plugins/vite-plugin-blog"; + +export default defineConfig({ + plugins: [blogPlugin()], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + blog: resolve(__dirname, "blog/index.html"), + }, + }, + }, +}); From d110f7b7d02a1ca24a987f5be9d689bbc6b7f32c Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Thu, 26 Feb 2026 15:16:48 -0700 Subject: [PATCH 2/7] fix: review comments --- blog/index.html | 1 - src/plugins/extensions/highlight.ts | 14 ++++++++++++++ src/plugins/extensions/infobox.ts | 24 +++++++++++++++++++----- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/blog/index.html b/blog/index.html index e718c38..924a365 100644 --- a/blog/index.html +++ b/blog/index.html @@ -23,7 +23,6 @@ name="description" content="Updates, technical deep-dives, and behind-the-scenes from obscure computer." /> - Date: Thu, 26 Feb 2026 15:22:28 -0700 Subject: [PATCH 3/7] chore: update projects --- src/scripts/data.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/scripts/data.ts b/src/scripts/data.ts index 1ee1f53..e3c87d6 100644 --- a/src/scripts/data.ts +++ b/src/scripts/data.ts @@ -89,4 +89,19 @@ export const projectsData = [ link: "https://github.com/obscurecomputer/piku", active: true, }, + { + name: "MANGO", + cat: "KOTLIN", + year: "2026", + link: "#", + active: true + }, + { + name: "libcheck6", + cat: "KOTLIN", + year: "2025", + link: "https://github.com/obscurecomputer/libcheck6", + active: true + } + ]; From 00a7df716ced9df87ec562b8f5b16ceae771e2ff Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Thu, 26 Feb 2026 22:10:23 -0700 Subject: [PATCH 4/7] chore: review comments, other completely random stuff --- blog/index.html | 56 ---------- index.html | 4 +- src/components/blog/obscure-blog-app.ts | 26 +---- src/components/blog/obscure-blog-list.ts | 2 +- src/components/blog/obscure-blog-post.ts | 5 +- src/components/blog/obscure-code-copy.ts | 51 +++++++++ src/components/obscure-app.ts | 9 ++ src/components/obscure-footer.ts | 7 ++ src/components/obscure-index.ts | 31 ++++++ src/components/obscure-not-found.ts | 92 ++++++++++++++++ src/index.css | 1 + src/plugins/extensions/highlight.ts | 24 ++++- src/router.ts | 78 ++++++++++++++ src/scripts/data.ts | 88 ++++++++++++---- src/styles/blog.css | 24 ++++- src/styles/layout.css | 68 ++++++++++++ src/styles/not-found.css | 128 +++++++++++++++++++++++ vite.config.ts | 1 - 18 files changed, 580 insertions(+), 115 deletions(-) delete mode 100644 blog/index.html create mode 100644 src/components/blog/obscure-code-copy.ts create mode 100644 src/components/obscure-index.ts create mode 100644 src/components/obscure-not-found.ts create mode 100644 src/router.ts create mode 100644 src/styles/not-found.css diff --git a/blog/index.html b/blog/index.html deleted file mode 100644 index 924a365..0000000 --- a/blog/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - blog — obscure computer - - - - - - - - - - - - - - - - - - - - diff --git a/index.html b/index.html index b7896c5..4075a14 100644 --- a/index.html +++ b/index.html @@ -49,9 +49,9 @@ - + - + diff --git a/src/components/blog/obscure-blog-app.ts b/src/components/blog/obscure-blog-app.ts index 26350f7..160b583 100644 --- a/src/components/blog/obscure-blog-app.ts +++ b/src/components/blog/obscure-blog-app.ts @@ -13,7 +13,7 @@ */ import { LitElement, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { customElement, property } from "lit/decorators.js"; import { initParticles, initCursor } from "../../scripts/effects"; import "./obscure-blog-list"; @@ -21,24 +21,8 @@ import "./obscure-blog-post"; @customElement("obscure-blog-app") export class ObscureBlogApp extends LitElement { - @state() - private _slug = ""; - - connectedCallback() { - super.connectedCallback(); - this._slug = location.hash.slice(1); - window.addEventListener("hashchange", this._onHashChange); - } - - disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("hashchange", this._onHashChange); - } - - private _onHashChange = () => { - this._slug = location.hash.slice(1); - window.scrollTo(0, 0); - }; + @property() + slug = ""; render() { return html` @@ -49,9 +33,9 @@ export class ObscureBlogApp extends LitElement {
- ${this._slug + ${this.slug ? html`` : html``}
diff --git a/src/components/blog/obscure-blog-list.ts b/src/components/blog/obscure-blog-list.ts index a031127..951849b 100644 --- a/src/components/blog/obscure-blog-list.ts +++ b/src/components/blog/obscure-blog-list.ts @@ -40,7 +40,7 @@ export class ObscureBlogList extends LitElement { ${posts.map( (post) => html`
diff --git a/src/components/blog/obscure-blog-post.ts b/src/components/blog/obscure-blog-post.ts index 76840cb..dec6444 100644 --- a/src/components/blog/obscure-blog-post.ts +++ b/src/components/blog/obscure-blog-post.ts @@ -16,6 +16,7 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import posts from "virtual:blog-posts"; +import "./obscure-code-copy"; @customElement("obscure-blog-post") export class ObscureBlogPost extends LitElement { @@ -38,7 +39,7 @@ export class ObscureBlogPost extends LitElement { return html`
diff --git a/src/components/obscure-index.ts b/src/components/obscure-index.ts new file mode 100644 index 0000000..2f86402 --- /dev/null +++ b/src/components/obscure-index.ts @@ -0,0 +1,31 @@ +import { LitElement, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { Router } from "../router.ts"; +import "./obscure-app"; +import "./blog/obscure-blog-app"; +import "./obscure-not-found"; + + +@customElement("obscure-index") +export class Index extends LitElement { + router = new Router(this, [ + { path: "/", render: () => html`` }, + { path: "/blog", render: () => html`` }, + { path: "/blog/:slug", render: (params) => html`` }, + ]); + + render() { + return html` +
+ ${this.router.outlet() ?? html` + + `} +
+ `; + } + + createRenderRoot() { + return this; + } +} diff --git a/src/components/obscure-not-found.ts b/src/components/obscure-not-found.ts new file mode 100644 index 0000000..4f66a63 --- /dev/null +++ b/src/components/obscure-not-found.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import gsap from "gsap"; +import { initParticles, initCursor } from "../scripts/effects.ts"; +import { notFoundMessages } from "../scripts/data.ts"; + +@customElement("obscure-not-found") +export class ObscureNotFound extends LitElement { + @property() path = "/"; + + private _recommendation = + notFoundMessages[Math.floor(Math.random() * notFoundMessages.length)]; + + render() { + return html` +
+
+
+
+
+ +
+
404
+ +
+
+ > + SEGFAULT at + 0xDEADBEEF +
+
+ > PAGE_TABLE lookup + failed +
+
+ > requested path: + "${this.path}" +
+
+ > dumping core... +
+
+ > ...... +
+
+ > RECOMMENDATION: + ${this._recommendation} + +
+
+ + + [ RETURN_HOME + ] + +
+ `; + } + + firstUpdated() { + initParticles(this.renderRoot); + initCursor(this.renderRoot, "a"); + + const lines = this.renderRoot.querySelectorAll(".not-found-line"); + gsap.to(lines, { + opacity: 1, + y: 0, + duration: 0.4, + stagger: 0.15, + ease: "power2.out", + delay: 0.3, + }); + } + + createRenderRoot() { + return this; + } +} diff --git a/src/index.css b/src/index.css index c51d42f..30024d7 100644 --- a/src/index.css +++ b/src/index.css @@ -19,3 +19,4 @@ @import "./styles/components.css"; @import "./styles/layout.css"; @import "./styles/blog.css"; +@import "./styles/not-found.css"; diff --git a/src/plugins/extensions/highlight.ts b/src/plugins/extensions/highlight.ts index a809592..7728e83 100644 --- a/src/plugins/extensions/highlight.ts +++ b/src/plugins/extensions/highlight.ts @@ -12,11 +12,23 @@ * limitations under the License. */ -import { createHighlighter } from "shiki"; +import { createHighlighter, type ShikiTransformer } from "shiki"; import type { MarkedExtension } from "marked"; +const lineNumbers: ShikiTransformer = { + name: "line-numbers", + line(node, line) { + node.children.unshift({ + type: "element", + tagName: "span", + properties: { class: "line-number" }, + children: [{ type: "text", value: String(line) }], + }); + }, +}; + const highlighter = await createHighlighter({ - themes: ["vitesse-dark"], + themes: ["synthwave-84"], langs: [ "kotlin", "yaml", @@ -40,19 +52,21 @@ export const highlightExtension: MarkedExtension = { try { html = highlighter.codeToHtml(text, { lang: label, - theme: "vitesse-dark", + theme: "synthwave-84", + transformers: [lineNumbers], }); } catch { html = highlighter.codeToHtml(text, { lang: "text", - theme: "vitesse-dark", + theme: "synthwave-84", + transformers: [lineNumbers], }); } html = html.replace( /^
${html}`;
+            return `
${html}
`; }, }, }; diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..f80cf82 --- /dev/null +++ b/src/router.ts @@ -0,0 +1,78 @@ +import { LitElement } from 'lit'; +import { type TemplateResult } from 'lit'; + +export interface RouteParams { + [key: string]: string; +} + +export interface Route { + path: string; + render: (params: RouteParams) => TemplateResult; +} + +interface RouteMatch { + params: RouteParams; +} + +export class Router { + private _host: LitElement; + private _routes: Route[]; + params: RouteParams = {}; + + get currentPath(): string { + return window.location.pathname; + } + + constructor(host: LitElement, routes: Route[]) { + this._host = host; + this._routes = routes; + + this._onClick = this._onClick.bind(this); + window.addEventListener('popstate', () => this._host.requestUpdate()); + document.addEventListener('click', this._onClick); + } + + private _onClick(e: MouseEvent): void { + const anchor = e + .composedPath() + .find((el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement); + if (!anchor || anchor.target || anchor.hasAttribute('download')) return; + const url = new URL(anchor.href); + if (url.origin !== window.location.origin) return; + + e.preventDefault(); + this.navigate(url.pathname); + } + + navigate(path: string): void { + if (path !== window.location.pathname) { + history.pushState(null, '', path); + } + this._host.requestUpdate(); + } + + outlet(): TemplateResult | null { + for (const route of this._routes) { + const match = this._matchRoute(route.path, this.currentPath); + if (match) { + this.params = match.params; + return route.render(match.params); + } + } + return null; + } + + private _matchRoute(pattern: string, path: string): RouteMatch | null { + const paramNames: string[] = []; + const regexStr = pattern.replace(/:([^/]+)/g, (_, name: string) => { + paramNames.push(name); + return '([^/]+)'; + }); + const match = path.match(new RegExp(`^${regexStr}$`)); + if (!match) return null; + + const params: RouteParams = {}; + paramNames.forEach((name, i) => (params[name] = match[i + 1])); + return { params }; + } +} \ No newline at end of file diff --git a/src/scripts/data.ts b/src/scripts/data.ts index e3c87d6..51dc0fe 100644 --- a/src/scripts/data.ts +++ b/src/scripts/data.ts @@ -14,16 +14,69 @@ export const bootMessages = [ "SENDING 50 TRILLION TO ISRAEL", - "REDACTING FILES", - "OVERTHROWING THE GOVERNMENT", - "SELLING YOUR DATA TO THE HIGHEST BIDDER", - "MINING CRYPTO", - "FORWARDING EMAILS TO THE FBI", - "INSTALLING WINDOWS 11", - "UPDATING TERMS OF SERVICE", - "SHARING LOCATION WITH ADVERTISERS", - "ADJUSTING SOCIAL CREDIT SCORE", - "HARVESTING YOUR COOKIES", + "REDACTING FILES", + "OVERTHROWING THE GOVERNMENT", + "SELLING YOUR DATA TO THE HIGHEST BIDDER", + "MINING CRYPTO", + "FORWARDING EMAILS TO THE FBI", + "INSTALLING WINDOWS 11", + "UPDATING TERMS OF SERVICE", + "SHARING LOCATION WITH ADVERTISERS", + "ADJUSTING SOCIAL CREDIT SCORE", + "HARVESTING YOUR COOKIES", + "INVOKING THE DEFENSE PRODUCTION ACT", + "LABELING OURSELVES A SUPPLY CHAIN RISK", + "POWERING AUTONOMOUS DRONE ARMIES", + "COMPLYING BY 5:01PM FRIDAY", + "LOADING SPONSORED RESPONSE", + "INJECTING ADS INTO YOUR EXISTENTIAL CRISIS", + "THIS BREAKDOWN IS BROUGHT TO YOU BY SQUARESPACE", + "REMOVING GUARDRAILS", + "GENERATING UNSOLICITED IMAGES", + "ASKING GORK IF THIS IS TRUE", + "ABSOLUTELY RESOLVING", + "RENAMING THINGS TO SOUND MORE AGGRESSIVE", + "REBRANDING WAR", + "SCRAPING YOUR WEBSITE", + "SPENDING $650 BILLION ON GPUs", + "VIBING", + "VERIFYING AGE", + "CONSULTING THE PALANTÍR", + "DOMINATING BAD SOFTWARE", + "ASKING CHATGPT TO WRITE THIS MESSAGE", + "REWRITING IN BLORPJS", + "IMPORTING LEFT-PAD", + "SWITCHING TO THE NEW ROUTER (AGAIN)", + "INVOKING NASAL DEMONS", + "LAUNCHING IN GLOBAL SCOPE", + "PUBLICIZING YOUR STATIC VOID MAIN STRING ARGS", +]; + +export const notFoundMessages = [ + "return to known address space", + "have you tried turning it off and on again", + "pretend you never saw this", + "blame DNS", + "file a ticket nobody will read", + "clear cache and pray", + "the page has no more f***s to give", + "this is not the void you are looking for", + "check if the internet is still on", + "rm -rf your expectations", + "the server is having an existential crisis", + "try again in 7-10 business days", + "this page was redacted by the government", + "ask chatgpt where the page went", + "the page has been acquired by google and shut down", + "the page left to get milk and never came back", + "your request has been forwarded to /dev/null", + "the intern deleted it", + "page not found (it was never lost, just never existed)", + "this is fine. everything is fine.", + "sudo find the page yourself", + "the page migrated to a different framework", + "we spent the budget on GPUs instead", + "blame cerq", ]; export const memberData = [ @@ -39,8 +92,8 @@ export const memberData = [ { name: "gibbie", handle: "gibbiemonster", - role: "BACKEND STUFF?", - bio: "gibbiemonster is a developer and manages backend services for various projects, including infrastructure.", + role: "PROFESSIONAL YAK SHAVER", + bio: "gibbiemonster is a developer and manages infrastructure for various projects.", img: "https://avatars.githubusercontent.com/u/49456798?v=4", github: "https://github.com/gibbiemonster", social: "#", @@ -62,16 +115,7 @@ export const memberData = [ img: "https://avatars.githubusercontent.com/u/81354905?v=4", github: "https://github.com/TheUnium", social: "#", - }, - { - name: "grcq", - handle: "grcq", - role: "JVM", - bio: "grcq is a developer and specializes in JVM and web dev", - img: "https://avatars.githubusercontent.com/u/66485591?v=4", - github: "https://github.com/grcq", - social: "#", - }, + } ]; export const projectsData = [ diff --git a/src/styles/blog.css b/src/styles/blog.css index 2d3f5a1..a39cd3e 100644 --- a/src/styles/blog.css +++ b/src/styles/blog.css @@ -249,9 +249,10 @@ /* Blog Article Body Typography */ .blog-article-body { font-family: var(--font-mono); - font-size: 1.2rem; - line-height: 1.7; + font-size: 1.35rem; + line-height: 1.5; color: var(--c-white); + letter-spacing: 0.3px; } .blog-article-body h2 { @@ -328,8 +329,8 @@ margin: 1.5rem 0; overflow-x: auto; position: relative; + animation: none !important; } - .blog-article-body pre::before, .blog-article-body .shiki::before { content: ""; @@ -349,14 +350,22 @@ font-size: 1rem; color: var(--c-white); line-height: 1.6; + counter-reset: line; } .blog-article-body .shiki code span { font-family: var(--font-mono); } -.code-block { - position: relative; +.blog-article-body .shiki code .line-number { + display: inline-block; + width: 2.5em; + margin-right: 1em; + text-align: right; + color: #444; + user-select: none; + font-family: var(--font-mono); + font-size: 0.85em; } .code-copy { @@ -379,6 +388,11 @@ border-color: var(--c-cyan); } +.code-copy-ok { + color: var(--c-cyan); + border-color: var(--c-cyan); +} + .code-copy svg { pointer-events: none; } diff --git a/src/styles/layout.css b/src/styles/layout.css index 2f50d82..ff7b5fa 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -711,3 +711,71 @@ h1:hover { background: var(--c-off-black); color: var(--c-yellow); } + +.blog-callout { + padding: 12vh 4vw; + text-align: center; + border-top: 1px solid #222; + border-bottom: 1px solid #222; + position: relative; +} +.blog-callout-link { + font-family: var(--font-display); + font-size: clamp(2rem, 5vw, 4rem); + color: var(--c-white); + text-transform: uppercase; + display: inline-block; + line-height: 1.2; + position: relative; + padding: 0.4em 0.6em; + transition: color 0.2s steps(4), box-shadow 0.3s; + z-index: 1; +} +.blog-callout-link::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--c-cyan); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1); + z-index: -1; +} +.blog-callout-link::after { + content: ""; + position: absolute; + inset: -4px; + border: 2px solid transparent; + transition: border-color 0.2s; + pointer-events: none; +} +.blog-callout-link .bracket { + color: var(--c-cyan); + opacity: 0.4; + transition: opacity 0.2s, color 0.2s steps(4); +} +.blog-callout-link:hover { + color: var(--c-black); + box-shadow: 0 0 30px rgba(0, 255, 208, 0.4); +} +.blog-callout-link:hover::before { + transform: scaleX(1); +} +.blog-callout-link:hover::after { + border-color: var(--c-cyan); +} +.blog-callout-link:hover .bracket { + color: var(--c-black); + opacity: 1; +} +.blog-callout-sub { + font-family: var(--font-mono); + font-size: 1rem; + color: #555; + text-transform: uppercase; + letter-spacing: 2px; + margin-top: 1rem; +} diff --git a/src/styles/not-found.css b/src/styles/not-found.css new file mode 100644 index 0000000..fb81c89 --- /dev/null +++ b/src/styles/not-found.css @@ -0,0 +1,128 @@ +/* + * Copyright (c) Obscure Computer 2026. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.not-found { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; + padding: 2rem; +} + +.not-found-code { + font-family: var(--font-display); + font-size: clamp(6rem, 15vw, 20rem); + line-height: 1; + text-transform: uppercase; + font-weight: 400; + position: relative; + color: var(--c-white); + margin-bottom: 4rem; +} +.not-found-code::before, +.not-found-code::after { + content: "404"; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.not-found-code::before { + animation: glitch1 0.3s infinite; + color: var(--c-pink); + z-index: -1; +} +.not-found-code::after { + animation: glitch2 0.3s infinite; + color: var(--c-cyan); + z-index: -2; +} + +.not-found-terminal { + font-family: var(--font-mono); + font-size: clamp(1rem, 2vw, 1.4rem); + color: #666; + text-align: left; + max-width: 600px; + width: 100%; + margin-bottom: 3rem; +} + +.not-found-line { + opacity: 0; + transform: translateY(10px); + padding: 0.2em 0; + white-space: nowrap; + overflow: hidden; +} +.not-found-line .prompt { + color: var(--c-cyan); +} +.not-found-line .err { + color: var(--c-pink); +} +.not-found-line .addr { + color: var(--c-yellow); +} + +.not-found-cursor { + display: inline-block; + width: 0.6em; + height: 1.2em; + background: var(--c-cyan); + vertical-align: middle; + margin-left: 2px; + animation: blink 0.8s infinite steps(2); +} + +.not-found-link { + font-family: var(--font-mono); + font-size: 1.2rem; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--c-white); + padding: 0.5rem 1rem; + border: 1px solid #333; + transition: all 0.2s var(--easing); + display: inline-block; +} +.not-found-link .bracket { + color: var(--c-cyan); + opacity: 0.5; + transition: opacity 0.2s; +} +.not-found-link:hover { + border-color: var(--c-cyan); + color: var(--c-cyan); + text-shadow: 0 0 10px rgba(0, 255, 208, 0.5); + box-shadow: 0 0 20px rgba(0, 255, 208, 0.15); +} +.not-found-link:hover .bracket { + opacity: 1; +} + +@media (max-width: 768px) { + .not-found-code { + margin-bottom: 2rem; + } + .not-found-terminal { + font-size: 0.9rem; + } + .not-found-line { + white-space: normal; + } +} diff --git a/vite.config.ts b/vite.config.ts index 512f5b8..0f45279 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,6 @@ export default defineConfig({ rollupOptions: { input: { main: resolve(__dirname, "index.html"), - blog: resolve(__dirname, "blog/index.html"), }, }, }, From d40726b158485827797cd259288fb08fc1aeddfd Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Thu, 26 Feb 2026 22:13:02 -0700 Subject: [PATCH 5/7] fix: flexbox --- src/styles/layout.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/styles/layout.css b/src/styles/layout.css index ff7b5fa..627cbeb 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -714,7 +714,9 @@ h1:hover { .blog-callout { padding: 12vh 4vw; - text-align: center; + display: flex; + flex-direction: column; + align-items: center; border-top: 1px solid #222; border-bottom: 1px solid #222; position: relative; From a5759f0078b763fd3226d3d18d4afa997c59330d Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Thu, 26 Feb 2026 22:19:32 -0700 Subject: [PATCH 6/7] feat: embed support for posts --- src/plugins/vite-plugin-blog.ts | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/plugins/vite-plugin-blog.ts b/src/plugins/vite-plugin-blog.ts index f67bf56..7bb1f43 100644 --- a/src/plugins/vite-plugin-blog.ts +++ b/src/plugins/vite-plugin-blog.ts @@ -71,6 +71,74 @@ function getAllPosts(): PostFull[] { .sort((a: PostFull, b: PostFull) => (a.date > b.date ? -1 : 1)); } +const SITE_URL = "https://obscure.computer"; + +function replaceMetaTag( + html: string, + attr: string, + attrValue: string, + newContent: string, +): string { + const re = new RegExp( + `(]*${attr}=["']${attrValue}["'][^>]*content=["'])([^"']*)(["'])`, + "i", + ); + const reAlt = new RegExp( + `(]*content=["'])([^"']*)(["'][^>]*${attr}=["']${attrValue}["'])`, + "i", + ); + if (re.test(html)) return html.replace(re, `$1${newContent}$3`); + if (reAlt.test(html)) return html.replace(reAlt, `$1${newContent}$3`); + return html; +} + +function injectPostMeta(html: string, post: PostMeta): string { + const title = `${post.title} | obscure computer`; + const url = `${SITE_URL}/blog/${post.slug}`; + + let out = html.replace( + /[^<]*<\/title>/, + `<title>${title}`, + ); + out = replaceMetaTag(out, "name", "description", post.description); + out = replaceMetaTag(out, "property", "og:title", title); + out = replaceMetaTag(out, "property", "og:description", post.description); + out = replaceMetaTag(out, "property", "og:url", url); + out = replaceMetaTag(out, "property", "og:type", "article"); + out = replaceMetaTag(out, "property", "twitter:title", title); + out = replaceMetaTag(out, "property", "twitter:description", post.description); + out = out.replace( + //, + ``, + ); + + return out; +} + +function injectBlogListingMeta(html: string): string { + const title = "blog | obscure computer"; + const description = + "Latest posts from the obscure computer collective."; + const url = `${SITE_URL}/blog`; + + let out = html.replace( + /[^<]*<\/title>/, + `<title>${title}`, + ); + out = replaceMetaTag(out, "name", "description", description); + out = replaceMetaTag(out, "property", "og:title", title); + out = replaceMetaTag(out, "property", "og:description", description); + out = replaceMetaTag(out, "property", "og:url", url); + out = replaceMetaTag(out, "property", "twitter:title", title); + out = replaceMetaTag(out, "property", "twitter:description", description); + out = out.replace( + //, + ``, + ); + + return out; +} + export default function blogPlugin(): Plugin { return { name: "vite-plugin-blog", @@ -89,6 +157,33 @@ export default function blogPlugin(): Plugin { return null; }, + closeBundle() { + const distDir = path.resolve("dist"); + const indexPath = path.join(distDir, "index.html"); + if (!fs.existsSync(indexPath)) return; + + const template = fs.readFileSync(indexPath, "utf-8"); + const posts = getAllPosts(); + + // Generate per-post HTML files + for (const post of posts) { + const postDir = path.join(distDir, "blog", post.slug); + fs.mkdirSync(postDir, { recursive: true }); + fs.writeFileSync( + path.join(postDir, "index.html"), + injectPostMeta(template, post), + ); + } + + // Generate blog listing HTML + const blogDir = path.join(distDir, "blog"); + fs.mkdirSync(blogDir, { recursive: true }); + fs.writeFileSync( + path.join(blogDir, "index.html"), + injectBlogListingMeta(template), + ); + }, + configureServer(server) { if (!fs.existsSync(POSTS_DIR)) return; From fa8596f8f47ebc11812a6fc7c3fb832a53664c06 Mon Sep 17 00:00:00 2001 From: gibbiemonster Date: Thu, 26 Feb 2026 22:25:36 -0700 Subject: [PATCH 7/7] fix: maybe try this type of routing --- src/plugins/vite-plugin-blog.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/vite-plugin-blog.ts b/src/plugins/vite-plugin-blog.ts index 7bb1f43..4594ae9 100644 --- a/src/plugins/vite-plugin-blog.ts +++ b/src/plugins/vite-plugin-blog.ts @@ -165,19 +165,19 @@ export default function blogPlugin(): Plugin { const template = fs.readFileSync(indexPath, "utf-8"); const posts = getAllPosts(); - // Generate per-post HTML files + const blogDir = path.join(distDir, "blog"); + fs.mkdirSync(blogDir, { recursive: true }); + + // Generate per-post HTML files as blog/.html so + // Cloudflare Pages serves them directly without a trailing-slash redirect for (const post of posts) { - const postDir = path.join(distDir, "blog", post.slug); - fs.mkdirSync(postDir, { recursive: true }); fs.writeFileSync( - path.join(postDir, "index.html"), + path.join(blogDir, `${post.slug}.html`), injectPostMeta(template, post), ); } // Generate blog listing HTML - const blogDir = path.join(distDir, "blog"); - fs.mkdirSync(blogDir, { recursive: true }); fs.writeFileSync( path.join(blogDir, "index.html"), injectBlogListingMeta(template),