diff --git a/.gitignore b/.gitignore index 9da5cd0..14120ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vim +.cache _site node_modules .idea diff --git a/eleventy.config.js b/eleventy.config.js index 424a870..ec456f7 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -1,14 +1,58 @@ import pugPlugin from "@11ty/eleventy-plugin-pug"; import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; +import { EleventyRenderPlugin } from '@11ty/eleventy'; +import * as fs from 'fs'; +import * as opml from 'opml'; export default function (eleventyConfig) { eleventyConfig.setInputDirectory("src") eleventyConfig.addPlugin(pugPlugin); eleventyConfig.addPlugin(eleventyImageTransformPlugin); - eleventyConfig.addPassthroughCopy("assets/img/markmark-light.svg") - eleventyConfig.addPassthroughCopy("assets/font/obsidian/Obsidian-Roman.otf") - eleventyConfig.addPassthroughCopy("assets/font/reckless/WEB/Reckless-Bold.woff2") - eleventyConfig.addPassthroughCopy("assets/font/reckless/WEB/Reckless-Regular.woff2") - eleventyConfig.addPassthroughCopy("assets/font/reckless/WEB/Reckless-RegularItalic.woff2") + eleventyConfig.addPlugin(EleventyRenderPlugin); + eleventyConfig.addPassthroughCopy("src/assets/**") + + eleventyConfig.addCollection("blogByYear", api => { + const postsByYear = {} + const posts = api.getFilteredByTag('blog') + const years = [] + for ( const post of posts ) { + const year = post.date.getFullYear() + if ( !postsByYear[year] ) postsByYear[year] = [] + postsByYear[year].push(post) + if ( !years.includes(year) ) years.push(year) + } + + return years.sort().reverse().map(year => ({ + year, + posts: postsByYear[year], + })) + }) + + eleventyConfig.addCollection("blogByTag", api => { + const postsByTag = {} + const posts = api.getFilteredByTag('blog') + const tags = [] + for ( const post of posts ) { + for ( const tag of post.data.blogtags || [] ) { + if ( !postsByTag[tag] ) postsByTag[tag] = [] + postsByTag[tag].push(post) + if ( !tags.includes(tag) ) tags.push(tag) + } + } + + return tags.sort().map(tag => ({ + tag, + posts: postsByTag[tag], + })) + }) + + eleventyConfig.addCollection("opmlByCategory", async api => { + const xml = fs.readFileSync("./src/assets/rss_opml.xml") + const parsed = await new Promise((res, rej) => { + opml.parse(xml, (err, doc) => err ? rej(err) : res(doc)) + }) + + return parsed.opml.body.subs + }) } diff --git a/package.json b/package.json index 240bdec..28aa754 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "dependencies": { "@11ty/eleventy": "^3.0.0", "@11ty/eleventy-img": "^6.0.1", - "@11ty/eleventy-plugin-pug": "^1.0.0" + "@11ty/eleventy-plugin-pug": "^1.0.0", + "opml": "^0.5.7" }, "type": "module" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12200b2..18409c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@11ty/eleventy-plugin-pug': specifier: ^1.0.0 version: 1.0.0 + opml: + specifier: ^0.5.7 + version: 0.5.7 packages: @@ -248,6 +251,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -293,9 +299,25 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assert-never@1.4.0: resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -315,6 +337,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -347,6 +372,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} @@ -371,6 +399,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -385,10 +417,20 @@ packages: constantinople@4.0.1: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + daveutils@0.4.74: + resolution: {integrity: sha512-FXxxDvZk3QopKWtBdcvoLRw19UExu/0h+gFH7SDKSduVB1zCduiNcEfeiRAUN2T9yVH7HFODMQeGfpQogsALJA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -406,6 +448,10 @@ packages: supports-color: optional: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -448,6 +494,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -526,10 +575,23 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -555,6 +617,13 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -575,6 +644,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -594,6 +666,15 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -620,6 +701,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -684,6 +769,9 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -691,6 +779,9 @@ packages: resolution: {integrity: sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==} engines: {node: '>=6.0'} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -705,6 +796,22 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} @@ -825,6 +932,9 @@ packages: chokidar: optional: true + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -833,6 +943,12 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + opml@0.5.7: + resolution: {integrity: sha512-FqWQRkuGyBrirwzwgbIRTosV8+waYpHbT/Qz8cXmSkD4ztShvH9fG20YUeNo+Y6ZmZjOe2ClLoJBdTBRCB6qYg==} + + opmltojs@0.4.14: + resolution: {integrity: sha512-pQACXvUPD1tar/L28Rf1OYJuJMNHXfs3/TL6NN96WnH9rNGl2JeZwAnDoywYY5nnV3cq1wrgoZwFxJC0GoDR8g==} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -866,6 +982,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -901,6 +1020,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pug-attrs@3.0.0: resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} @@ -941,6 +1063,14 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -955,6 +1085,11 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -971,6 +1106,15 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -1020,6 +1164,11 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + ssri@11.0.0: resolution: {integrity: sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -1063,9 +1212,19 @@ packages: token-stream@1.0.0: resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -1073,9 +1232,21 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -1109,6 +1280,14 @@ packages: utf-8-validate: optional: true + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + snapshots: '@11ty/dependency-tree-esm@1.0.2': @@ -1393,6 +1572,13 @@ snapshots: acorn@8.14.1: {} + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -1426,8 +1612,20 @@ snapshots: asap@2.0.6: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assert-never@1.4.0: {} + assert-plus@1.0.0: {} + + asynckit@0.4.0: {} + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + babel-walk@3.0.0-canary-5: dependencies: '@babel/types': 7.26.10 @@ -1449,6 +1647,10 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -1488,6 +1690,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caseless@0.12.0: {} + character-parser@2.2.0: dependencies: is-regex: 1.2.1 @@ -1522,6 +1726,10 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} commander@5.1.0: {} @@ -1533,12 +1741,22 @@ snapshots: '@babel/parser': 7.26.10 '@babel/types': 7.26.10 + core-util-is@1.0.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + daveutils@0.4.74: + dependencies: + request: 2.88.2 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -1547,6 +1765,8 @@ snapshots: dependencies: ms: 2.1.3 + delayed-stream@1.0.0: {} + depd@2.0.0: {} dependency-graph@1.0.0: {} @@ -1585,6 +1805,11 @@ snapshots: eastasianwidth@0.2.0: {} + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + ee-first@1.1.1: {} emoji-regex@8.0.0: {} @@ -1635,6 +1860,12 @@ snapshots: dependencies: is-extendable: 0.1.1 + extend@3.0.2: {} + + extsprintf@1.3.0: {} + + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1643,6 +1874,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -1678,6 +1911,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fresh@0.5.2: {} fsevents@2.3.3: @@ -1703,6 +1944,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -1727,6 +1972,13 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -1756,6 +2008,12 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + ieee754@1.2.1: {} image-size@1.2.0: @@ -1811,10 +2069,14 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + is-typedarray@1.0.0: {} + isexe@2.0.0: {} iso-639-1@3.1.5: {} + isstream@0.1.2: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -1832,6 +2094,21 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@0.1.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stringify-safe@5.0.1: {} + + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + jstransformer@1.0.0: dependencies: is-promise: 2.2.2 @@ -1934,12 +2211,27 @@ snapshots: optionalDependencies: chokidar: 3.6.0 + oauth-sign@0.9.0: {} + object-assign@4.1.1: {} on-finished@2.4.1: dependencies: ee-first: 1.1.1 + opml@0.5.7: + dependencies: + daveutils: 0.4.74 + opmltojs: 0.4.14 + request: 2.88.2 + xml2js: 0.6.2 + + opmltojs@0.4.14: + dependencies: + daveutils: 0.4.74 + request: 2.88.2 + xml2js: 0.6.2 + p-finally@1.0.0: {} p-queue@6.6.2: @@ -1966,6 +2258,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + performance-now@2.1.0: {} + picomatch@2.3.1: {} pify@2.3.0: {} @@ -1997,6 +2291,10 @@ snapshots: prr@1.0.1: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pug-attrs@3.0.0: dependencies: constantinople: 4.0.1 @@ -2066,6 +2364,10 @@ snapshots: punycode.js@2.3.1: {} + punycode@2.3.1: {} + + qs@6.5.3: {} + queue-microtask@1.2.3: {} queue@6.0.2: @@ -2078,6 +2380,29 @@ snapshots: dependencies: picomatch: 2.3.1 + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -2094,6 +2419,12 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.4.1: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -2166,6 +2497,18 @@ snapshots: sprintf-js@1.0.3: {} + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + ssri@11.0.0: dependencies: minipass: 7.1.2 @@ -2204,15 +2547,38 @@ snapshots: token-stream@1.0.0: {} + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + tslib@2.8.1: optional: true + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + uc.micro@2.1.0: {} unpipe@1.0.0: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + urlpattern-polyfill@10.0.0: {} + uuid@3.4.0: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + void-elements@3.1.0: {} which@2.0.2: @@ -2239,3 +2605,10 @@ snapshots: strip-ansi: 7.1.0 ws@8.18.1: {} + + xml2js@0.6.2: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} diff --git a/src/_includes/blog.pug b/src/_includes/blog.pug new file mode 100644 index 0000000..3e0d110 --- /dev/null +++ b/src/_includes/blog.pug @@ -0,0 +1,15 @@ +extends ./obsidian + +block content + .container#top + .inner + section#blog-header + h1 Garrett's Blog + p.button-links + a.button(href='/blog') Home + a.button(href='/blog/archive') Archive + a.button(href='/blog/tags') Tags + a.button(href='/blog/feeds') Feeds + + block blog_content + .content-wrapper !{content} diff --git a/src/_includes/blog_post.pug b/src/_includes/blog_post.pug new file mode 100644 index 0000000..ba2dff1 --- /dev/null +++ b/src/_includes/blog_post.pug @@ -0,0 +1,10 @@ +extends ./blog + +block blog_content + h2 #{title} + p by Garrett Mills on #{page.date.toISOString().split('T')[0]} + .post-tags + p.button-links + each tag in blogtags + a.button-small.secondary(href='/blog/tag/' + tag) ##{tag} + .blog.content-wrapper !{content} diff --git a/src/assets/css/obsidian.css b/src/assets/css/obsidian.css index 62519c9..69b01fd 100644 --- a/src/assets/css/obsidian.css +++ b/src/assets/css/obsidian.css @@ -213,10 +213,19 @@ blockquote { margin-left: 10px; } +blockquote p { + font-size: 1em; + margin: 0; +} + img { max-width: 100%; } +.blog.content-wrapper img { + height: auto !important; +} + center { margin: 0 20px; color: var(--color-2); diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Bold.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Bold.woff2 new file mode 100644 index 0000000..4917f43 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Bold.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 new file mode 100644 index 0000000..536d3f7 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBold.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBold.woff2 new file mode 100644 index 0000000..8f88c54 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBold.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..d1478ba Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLight.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLight.woff2 new file mode 100644 index 0000000..b97239f Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLight.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLightItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLightItalic.woff2 new file mode 100644 index 0000000..be01aac Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-ExtraLightItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Italic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Italic.woff2 new file mode 100644 index 0000000..d60c270 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Italic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Light.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Light.woff2 new file mode 100644 index 0000000..6538498 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Light.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-LightItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-LightItalic.woff2 new file mode 100644 index 0000000..66ca3d2 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-LightItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Medium.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Medium.woff2 new file mode 100644 index 0000000..669d04c Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Medium.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-MediumItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-MediumItalic.woff2 new file mode 100644 index 0000000..80cfd15 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-MediumItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Regular.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Regular.woff2 new file mode 100644 index 0000000..40da427 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Regular.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBold.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBold.woff2 new file mode 100644 index 0000000..5ead7b0 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBold.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBoldItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBoldItalic.woff2 new file mode 100644 index 0000000..c5dd294 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-SemiBoldItalic.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-Thin.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-Thin.woff2 new file mode 100644 index 0000000..17270e4 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-Thin.woff2 differ diff --git a/src/assets/font/jetbrains-mono/JetBrainsMono-ThinItalic.woff2 b/src/assets/font/jetbrains-mono/JetBrainsMono-ThinItalic.woff2 new file mode 100644 index 0000000..a643215 Binary files /dev/null and b/src/assets/font/jetbrains-mono/JetBrainsMono-ThinItalic.woff2 differ diff --git a/src/assets/rss_opml.xml b/src/assets/rss_opml.xml new file mode 100644 index 0000000..732ab36 --- /dev/null +++ b/src/assets/rss_opml.xml @@ -0,0 +1,259 @@ + + + + NewsFlash OPML export + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/blog/archive.pug b/src/blog/archive.pug new file mode 100644 index 0000000..526b84e --- /dev/null +++ b/src/blog/archive.pug @@ -0,0 +1,13 @@ +extends /blog + +block blog_content + h2 Post Archive + + .recent-posts + each year in collections.blogByYear + h3 #{year.year} + ul.plain + each post in year.posts + li + .secondary #{post.date.toISOString().split('T')[0]} + a.title(href=post.url) #{post.data.title} diff --git a/src/blog/feeds-stub.md b/src/blog/feeds-stub.md new file mode 100644 index 0000000..6e5c1aa --- /dev/null +++ b/src/blog/feeds-stub.md @@ -0,0 +1,23 @@ +## An RSS Manifesto + +> More than a convenience, _RSS is good for the web_. + +This blog is available via [RSS](/blog/rss2.xml), [Atom](/blog/atom.xml), or [JSON](/blog/json.json) syndication. + +For the uninitiated, RSS/Atom/JSON feeds make the content of a website available in a standard format. This allows you to consume the blog's content in whatever feed reader you prefer, rather than visiting the site directly. + +RSS & Atom are standard feed formats based on XML. RSS is by far the most popular. [JSON feed](https://www.jsonfeed.org/) is a somewhat less popular syndication format based on JSON. + +While RSS is no longer a "mainstream" format for consuming works on the internet, it still has a thriving community of dedicated fans. + +You might be surprised to learn that RSS feeds are still everywhere. Nearly every news/blog-style website will have an RSS feed out-of-the box. Everything from [The New York Times](https://www.nytimes.com/rss) to [NASA](https://www.nasa.gov/content/nasa-rss-feeds) to [_every single podcast_](https://99percentinvisible.org/listen/) can be subscribed to via RSS. + +More than a convenience, _RSS is good for the web_. It provides a standard format for programmatically fetching content from a disparate collection of websites, is (nearly) always available. + +From a user's perspective, it allows us (as consumers) to choose what content we subscribe to and receive timely updates without relying on an algorithm. Plus, since RSS is a pure HTML format, it is often better from a user-privacy standpoint than visiting websites directly. + +Finally, RSS promotes platform diversity by enabling small authors & creators to publish their works online in a standard, approachable format without needing a larger platform to reach their subscribers. + +If you couldn't tell, I ❤️ open syndication, and I think it's really important to continue making these feeds available to users. + +To that end, I'm trying to collect a list of independent RSS feeds that I enjoy: diff --git a/src/blog/feeds.njk b/src/blog/feeds.njk new file mode 100644 index 0000000..3911648 --- /dev/null +++ b/src/blog/feeds.njk @@ -0,0 +1,20 @@ +--- +layout: blog +--- + +{% renderFile "src/blog/feeds-stub.md" %} + +

Garrett's RSS List

+ +
+This list is also available in the standard OPML format. +
+ +{% for cat in collections.opmlByCategory %} +

{{ cat.title }}

+ +{% endfor %} diff --git a/src/blog/index.pug b/src/blog/index.pug new file mode 100644 index 0000000..d1ee20c --- /dev/null +++ b/src/blog/index.pug @@ -0,0 +1,13 @@ +extends /blog + +block blog_content + p Write-ups and musings, often technical, sometimes not. + + h2 Recent(ish) Posts + + .recent-posts + ul.plain + each post in collections.blog.slice(0, 10) + li + .secondary #{post.data.date.toISOString().split('T')[0]} + a.title(href=post.url) #{post.data.title} diff --git a/src/blog/posts/2025-01-11-postgres.md b/src/blog/posts/2025-01-11-postgres.md new file mode 100644 index 0000000..e2e19c7 --- /dev/null +++ b/src/blog/posts/2025-01-11-postgres.md @@ -0,0 +1,129 @@ +--- +layout: blog_post +tags: blog +title: Salvaging a Corrupted Table from PostgreSQL +permalink: /blog/2025/01/11/Salvaging-a-Corrupted-Table-from-PostgreSQL/ +slug: Salvaging-a-Corrupted-Table-from-PostgreSQL +date: 2025-01-11 20:00:00 +blogtags: +- linux +- hosting +- postgres +--- + +![](https://static.garrettmills.dev/assets/blog-images/pg-recovery-1.png) + +> ⚠️ DO NOT DO THIS... well ever really, but especially on a server with failing disks. This is done on a server with perfectly fine disks, but corrupted Postgres blocks. + +I spend a lot of time in my professional work and my home lab trying to learn and implement the “correct” or “responsible” way of implementing a solution — highly-available deployments, automated and tested backups, infrastructure-as-code, &c. + +This is not that. + +This is a very dirty, no-holds-barred, absolutely insane thing to do, and if you’re working in any kind of environment that _matters_, you should [read this](https://wiki.postgresql.org/wiki/Corruption) and hire a professional. + +For unimportant reasons, I’ve been dealing with data corruption on the Postgres server in my home lab. The server was terminated uncleanly a couple times and the disk data was corrupted. Because there’s nothing more permanent than a temporary solution, I did not have backups for this server. + +For most of the data, I was able to use `pg_dump` to dump the schemata and data and re-import it into my new Postgres server (which, yes, has backups configured now). + +```none +pg_dump -U postgres -h localhost my_database > my_database.sql +``` + +For databases with corrupted tables, though, `pg_dump` fails out with this unsettling error: + +```none +> pg_dump -U postgres -h localhost www_p1 > www_p1.sql +pg_dump: error: Dumping the contents of table "page_views" failed: PQgetResult() failed. +pg_dump: detail: Error message from server: ERROR: invalid page in block 31869 of relation base/16384/16417 +pg_dump: detail: Command was: COPY public.page_views (page_view_id, visited_at, hostname, ip, method, endpoint, user_id, xhr) TO stdout; +``` + +(…yes, that’s the database for my personal website. 👀) Somewhat to my surprise, I couldn’t find many details/strategies for how to “best effort” recover data from a corrupt Postgres table, so here we go. + +Luckily, since the corruption was the result of unclean Postgres exits and not bad physical disks, it only affected table(s) with frequent writes at the time. In this case, that was the `sessions` table and the `page_views` table. The `sessions` table is entirely disposable — I just re-created it empty on the new server and moved on with my life. + +It wouldn’t be the end of the world if I lost the `page_views` table, but there are some 6.5 million historical page-views recorded in that table that would kind of suck to lose. So… let’s do some sketchy shit. + +My goal here isn’t to recover the entire table. If that was the goal, I would’ve stopped and hired a professional. Instead, my goal is to recover as many rows of the table as possible. + +One reason `pg_dump` fails is because it tries to read the data using a cursor, which fails when the fundamental assumptions of Postgres are violated (e.g. bad data in disk blocks, invalid indices). + +My strategy here is to create a 2nd table on the bad server with the same schema, then loop over each row in the `page_views` individually and insert them into the clean table, skipping rows in disk blocks with bad data. Shout out to [this Stack Overflow answer](https://stackoverflow.com/a/63905054/4971138) that loosely inspired this strategy. + +```sql +CREATE OR REPLACE PROCEDURE pg_recover_proc() +LANGUAGE plpgsql AS $$ +DECLARE + cnt BIGINT := 0; +BEGIN + -- Get the maximum page_view_id from the page_views table + cnt := (SELECT MAX(page_view_id) FROM page_views); + + -- Loop through the page_views table in reverse order by page_view_id + LOOP + BEGIN + -- Insert the row with the current page_view_id into page_views_recovery + INSERT INTO page_views_recovery + SELECT * FROM page_views WHERE page_view_id = cnt and entrypoint is not null; + + -- Decrement the counter + cnt := cnt - 1; + + -- Exit the loop when cnt < 1 + EXIT WHEN cnt < 1; + EXCEPTION + WHEN OTHERS THEN + -- Handle exceptions (e.g., data corruption) + IF POSITION('block' in SQLERRM) > 0 OR POSITION('status of transaction' in SQLERRM) > 0 OR POSITION('memory alloc' in SQLERRM) > 0 OR POSITION('data is corrupt' in SQLERRM) > 0 OR POSITION('MultiXactId' in SQLERRM) > 0 THEN + RAISE WARNING 'PGR_SKIP: %', cnt; + cnt := cnt - 1; + CONTINUE; + ELSE + RAISE; + END IF; + END; + + IF MOD(cnt, 500) = 0 THEN + RAISE WARNING 'PGR_COMMIT: %', cnt; + COMMIT; + END IF; + END LOOP; +END; +$$; +``` + +There are some cool and absolutely terrible things here. In modern versions of Postgres, stored procedures can periodically commit their in-progress top-level transactions by calling `COMMIT` repeatedly. I’m (ab)using this here to flush the recovered rows to the new table as the procedure runs in case it fails partway through. + +I'm doing some rough string analysis for error messages related to corrupt data and skipping the current row if that's the case. Another interesting edge-case: a couple times, I ran into a case where the `INSERT` into the recovery table failed because the `SELECT` query against the bad table was returning `null` values, even though that should technically never be possible. Told you we're violating some foundational assumptions about Postgres here. Adding an `is not null` to a different non-null column helped avoid this. + +My original draft of this procedure was designed to keep looping and just skip the fatal errors caused by disk corruption (the various dirty `POSITION` checks in the error handler). + +Quickly, however, I ran into a new error: + +> SQL Error \[57P03\]: FATAL: the database system is in recovery mode + +Turns out, if you keep intentionally forcing Postgres to try to read data from bad disk blocks, eventually its internal data structures hit an inconsistent state and the server process restarts itself out for safety. + +This is (obviously) a problem because we can’t catch that and force the procedure to keep running against its will. So instead I resorted to adding `IF` conditions to manually skip over primary key regions that caused the server process to crash. (I told you this was crazy.) + +Every time the server would crash, I would dump out the rows I’d recovered so far, just in case: + +```none +pg_dump -U postgres -h localhost --table page_views2 www_p1 > page_views2-1.sql +``` + +Then I’d skip a new region of primary key, drop and re-create the recovery table, and try again. Why drop and re-create it? Because I discovered that when the server process crashed, it would occasionally write bad data to the *recovery* table, which is obviously no good: + +```none +pg_dump: error: Dumping the contents of table "page_views_recovery" failed: PQgetResult() failed. +pg_dump: detail: Error message from server: ERROR: invalid memory alloc request size 18446744073709551613 +pg_dump: detail: Command was: COPY public.page_views_recovery (page_view_id, visited_at, hostname, ip, method, endpoint, user_id, xhr) TO stdout; +``` + +Predictably, this got really annoying to do by hand, so I did what any good Linux nerd would do and wrote a script for it, which [you can find here](https://code.garrettmills.dev/garrettmills/pg-recover). The gist: + +```none +./pg-recover.sh postgres localhost www_p1 page_views page_view_id entrypoint +``` + +Of the 6,628,903 rows in the corrupt table, I was able to recover 6,444,118 of them. You know what they say — if it’s stupid and it works, it’s still stupid and you’re just lucky. diff --git a/src/blog/tag.njk b/src/blog/tag.njk new file mode 100644 index 0000000..77784ba --- /dev/null +++ b/src/blog/tag.njk @@ -0,0 +1,21 @@ +--- +layout: blog +pagination: + data: collections.blogByTag + size: 1 + alias: tag +permalink: "/blog/tag/{{ tag.tag }}/" +--- + +

Posts w/ Tag: {{ tag.tag }}

+ +
+ +
diff --git a/src/blog/tags.pug b/src/blog/tags.pug new file mode 100644 index 0000000..27a19bf --- /dev/null +++ b/src/blog/tags.pug @@ -0,0 +1,9 @@ +extends /blog + +block blog_content + h2 Tags + + .post-tags.listed + ul + each tag in collections.blogByTag + li ##{tag.tag}   (#{tag.posts.length})