From 94941d0667d479c429e744a8d39242fe97fe1bdd Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Thu, 16 Jul 2026 19:03:59 -0400 Subject: [PATCH 1/6] blog: hold KVM note for editorial review (TIN-2979) --- src/posts/2026-06-23-ssh-macos-kvm-hid-accessory-approval.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/2026-06-23-ssh-macos-kvm-hid-accessory-approval.md b/src/posts/2026-06-23-ssh-macos-kvm-hid-accessory-approval.md index 3a708fa..da13668 100644 --- a/src/posts/2026-06-23-ssh-macos-kvm-hid-accessory-approval.md +++ b/src/posts/2026-06-23-ssh-macos-kvm-hid-accessory-approval.md @@ -3,7 +3,7 @@ title: "The tiny SSH trick for letting a USB KVM talk to a locked Mac" date: "2026-06-23" description: "A quick macOS tip for allowing a NanoKVM-style USB HID accessory from an SSH session when Screen Sharing is unavailable and the Mac is stuck at the login window." tags: ["macOS", "hardware", "homelab", "usb-protocol", "kvm", "ssh"] -published: true +published: false slug: "ssh-macos-kvm-hid-accessory-approval" category: "hardware" author_slug: "jesssullivan" From bd9824b36057d521aa86837346bf285b33e4c617 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Thu, 16 Jul 2026 22:29:12 -0400 Subject: [PATCH 2/6] fix(blog): preserve explicit publication holds (TIN-2979) --- scripts/generate-search-index.mts | 15 +++++++++++++-- src/lib/tinyland/blogBrokerStream.test.ts | 9 +++++++++ src/lib/tinyland/blogBrokerStream.ts | 14 ++++++++++---- src/routes/blog/+page.svelte | 8 ++++++-- src/routes/blog/+page.ts | 3 ++- src/routes/blog/[slug]/+page.ts | 7 +++++++ static/blog-publication-holds.json | 1 + 7 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 static/blog-publication-holds.json diff --git a/scripts/generate-search-index.mts b/scripts/generate-search-index.mts index 91cf83e..339d45a 100644 --- a/scripts/generate-search-index.mts +++ b/scripts/generate-search-index.mts @@ -6,6 +6,7 @@ import { parseFrontmatter } from './lib/frontmatter.mts'; const POSTS_DIR = 'src/posts'; const OUTPUT = 'static/search-index.json'; +const PUBLICATION_HOLDS_OUTPUT = 'static/blog-publication-holds.json'; const WORDS_PER_MINUTE = 230; function computeReadingTime(text: string): number { @@ -16,14 +17,19 @@ function computeReadingTime(text: string): number { async function main(): Promise { const files = (await readdir(POSTS_DIR)).filter((f) => f.endsWith('.md')); const index: SearchIndexEntry[] = []; + const publicationHolds: string[] = []; for (const file of files) { const content = await readFile(join(POSTS_DIR, file), 'utf-8'); const meta = parseFrontmatter(content); - if (!meta || !meta.published) continue; + if (!meta) continue; const slug = (meta.slug as string) ?? file.replace('.md', '').replace(/^\d{4}-\d{2}-\d{2}-/, ''); + if (meta.published !== true) { + if (meta.published === false) publicationHolds.push(slug); + continue; + } // Extract body excerpt (~150 chars of plain text) const body = content.replace(/^---[\s\S]*?---/, ''); @@ -84,9 +90,14 @@ async function main(): Promise { // Sort by date descending index.sort((a, b) => b.date.localeCompare(a.date)); + publicationHolds.sort((a, b) => a.localeCompare(b)); - await writeFile(OUTPUT, JSON.stringify(index), 'utf-8'); + await Promise.all([ + writeFile(OUTPUT, JSON.stringify(index), 'utf-8'), + writeFile(PUBLICATION_HOLDS_OUTPUT, JSON.stringify(publicationHolds), 'utf-8'), + ]); console.log(`Search index: ${index.length} posts -> ${OUTPUT}`); + console.log(`Publication holds: ${publicationHolds.length} slugs -> ${PUBLICATION_HOLDS_OUTPUT}`); } main().catch((err) => { diff --git a/src/lib/tinyland/blogBrokerStream.test.ts b/src/lib/tinyland/blogBrokerStream.test.ts index 7a59005..76e2371 100644 --- a/src/lib/tinyland/blogBrokerStream.test.ts +++ b/src/lib/tinyland/blogBrokerStream.test.ts @@ -295,4 +295,13 @@ describe('mergeBrokerPostsIntoStatic', () => { ); expect(merged.map((p) => p.slug)).toEqual(['canon', 'federated', 'old']); }); + + it('does not revive an explicitly held slug from the broker', () => { + const merged = mergeBrokerPostsIntoStatic( + [post('canon', '2026-06-09')], + [post('held', '2026-06-10'), post('federated', '2026-06-01')], + new Set(['held']), + ); + expect(merged.map((p) => p.slug)).toEqual(['canon', 'federated']); + }); }); diff --git a/src/lib/tinyland/blogBrokerStream.ts b/src/lib/tinyland/blogBrokerStream.ts index 0799c2d..d1a0ed1 100644 --- a/src/lib/tinyland/blogBrokerStream.ts +++ b/src/lib/tinyland/blogBrokerStream.ts @@ -379,22 +379,28 @@ export function tinylandBlogBrokerStreamToPosts(stream: TinylandBlogBrokerStream * - a static post with no feature_image is backfilled from the matching broker * post only, * - broker-only posts (live posts not present in the repo) are appended, + * - explicit publication holds are removed from both sources, * - the result is re-sorted newest-first to match the static ordering. * * Net: new-post visibility is independent of broker freshness. */ -export function mergeBrokerPostsIntoStatic(staticPosts: readonly Post[], brokerPosts: readonly Post[]): Post[] { +export function mergeBrokerPostsIntoStatic( + staticPosts: readonly Post[], + brokerPosts: readonly Post[], + publicationHolds: ReadonlySet = new Set(), +): Post[] { const brokerBySlug = new Map(brokerPosts.map((post) => [post.slug, post])); - const staticSlugs = new Set(staticPosts.map((post) => post.slug)); + const visibleStaticPosts = staticPosts.filter((post) => !publicationHolds.has(post.slug)); + const staticSlugs = new Set(visibleStaticPosts.map((post) => post.slug)); - const merged: Post[] = staticPosts.map((post) => { + const merged: Post[] = visibleStaticPosts.map((post) => { if (post.feature_image) return post; const broker = brokerBySlug.get(post.slug); return broker?.feature_image ? { ...post, feature_image: broker.feature_image } : post; }); for (const post of brokerPosts) { - if (!staticSlugs.has(post.slug)) merged.push(post); + if (!staticSlugs.has(post.slug) && !publicationHolds.has(post.slug)) merged.push(post); } return merged.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() || a.slug.localeCompare(b.slug)); diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index dbe101e..9e09611 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -20,6 +20,7 @@ const POSTS_PER_PAGE = 20; const brokerEndpoint = TINYLAND_BLOG_BROKER_STREAM_URL; + const publicationHolds = new Set(data.publicationHolds); let brokerState = $state({ status: 'loading', @@ -31,10 +32,13 @@ // can lag the deploy (a just-published post sits in the hub's held-back // remainder for a window), so it is merged additively: it enriches existing // posts and appends broker-only ones, but can never drop a freshly published - // post. This fixes the listing flash-then-disappear on hydration; see + // post. Explicit local publication holds still win over either source. This + // fixes the listing flash-then-disappear on hydration; see // mergeBrokerPostsIntoStatic for the full rationale. let displayPosts = $derived( - brokerState.status === 'ready' ? mergeBrokerPostsIntoStatic(data.posts, brokerState.posts) : data.posts, + brokerState.status === 'ready' + ? mergeBrokerPostsIntoStatic(data.posts, brokerState.posts, publicationHolds) + : data.posts.filter((post) => !publicationHolds.has(post.slug)), ); let totalPages = $derived(Math.ceil(displayPosts.length / POSTS_PER_PAGE)); diff --git a/src/routes/blog/+page.ts b/src/routes/blog/+page.ts index fae0a5f..638783b 100644 --- a/src/routes/blog/+page.ts +++ b/src/routes/blog/+page.ts @@ -1,7 +1,8 @@ import { getPosts } from '$lib/posts'; import type { PageLoad } from './$types'; +import publicationHoldsData from '../../../static/blog-publication-holds.json'; export const load: PageLoad = async () => { const posts = await getPosts(); - return { posts }; + return { posts, publicationHolds: publicationHoldsData as string[] }; }; diff --git a/src/routes/blog/[slug]/+page.ts b/src/routes/blog/[slug]/+page.ts index 35d66db..7ce7589 100644 --- a/src/routes/blog/[slug]/+page.ts +++ b/src/routes/blog/[slug]/+page.ts @@ -1,5 +1,7 @@ import type { PageLoad, EntryGenerator } from './$types'; +import { error } from '@sveltejs/kit'; import searchIndexData from '../../../../static/search-index.json'; +import publicationHoldsData from '../../../../static/blog-publication-holds.json'; interface PostMeta { title: string; @@ -24,6 +26,7 @@ interface SearchIndexEntry { } const searchIndex = (searchIndexData as SearchIndexEntry[]).filter((entry) => entry.published !== false); +const publicationHolds = new Set(publicationHoldsData as string[]); function getSortedPosts(): { slug: string; title: string; date: string; tags: string[] }[] { return [...searchIndex] @@ -67,6 +70,10 @@ export const entries: EntryGenerator = async () => { }; export const load: PageLoad = async ({ params }) => { + if (publicationHolds.has(params.slug)) { + throw error(404, 'Post not found'); + } + const lazyModules = import.meta.glob('/src/posts/*.md'); const searchEntry = searchIndex.find((entry) => entry.slug === params.slug); const matchedPath = searchEntry?.source_file; diff --git a/static/blog-publication-holds.json b/static/blog-publication-holds.json new file mode 100644 index 0000000..4d7f027 --- /dev/null +++ b/static/blog-publication-holds.json @@ -0,0 +1 @@ +["1257","1342","2019-application-documents","2393-august-2021","aperture-and-the-tagged-device-identity-gap","bootstrapping-aperture-config-with-tsnet","darwin-nic-mac-bastion-usb-oob","datasets-plots-graphs-charts-august-2021","datasets-plots-graphs-charts-june-2021","evening-metal-9-14-20-original","experiential-investigation-in-ecological-science","gregorian","hello-world","ids-6","ids-6-3-1","ids-7-musings","ids-brief-musings-on-life-work","ids-precis","ids-ra-ap","ids-revision-7-02","ids-take-home-exam","ids-web-why","ids-web-why-2","open-source-methodology-at-the-university","pln","pln-portfolio","response-to-dweck","ssh-macos-kvm-hid-accessory-approval","the-canon-takes-its-shape","this-and-that-april-2022","tmpui-the-merlin-sound-id-project-november-2020","tmpui-the-merlin-sound-id-project-september-2020","watters-campbell-cheney","xoes-fomitopsis-betulina-huperzia-lucidula","xoes-plot-review"] \ No newline at end of file From ac9bba4c8939a4b6ab7bb33a0d2404ed77d4a515 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Thu, 16 Jul 2026 23:06:09 -0400 Subject: [PATCH 3/6] fix(blog): exclude held posts from client assets (TIN-2979) --- scripts/generate-search-index.mts | 55 +++-- scripts/run-sveltekit-vite-build-bazel.mjs | 81 +++++++ src/lib/data/blog-post-loaders.generated.ts | 244 ++++++++++++++++++++ src/routes/blog/[slug]/+page.ts | 13 +- 4 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 src/lib/data/blog-post-loaders.generated.ts diff --git a/scripts/generate-search-index.mts b/scripts/generate-search-index.mts index 339d45a..df3e03d 100644 --- a/scripts/generate-search-index.mts +++ b/scripts/generate-search-index.mts @@ -1,12 +1,14 @@ #!/usr/bin/env node import { readdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { format as formatWithPrettier } from 'prettier'; import type { SearchIndexEntry } from './lib/types.mts'; import { parseFrontmatter } from './lib/frontmatter.mts'; const POSTS_DIR = 'src/posts'; const OUTPUT = 'static/search-index.json'; const PUBLICATION_HOLDS_OUTPUT = 'static/blog-publication-holds.json'; +const PUBLISHED_POST_LOADERS_OUTPUT = 'src/lib/data/blog-post-loaders.generated.ts'; const WORDS_PER_MINUTE = 230; function computeReadingTime(text: string): number { @@ -18,14 +20,14 @@ async function main(): Promise { const files = (await readdir(POSTS_DIR)).filter((f) => f.endsWith('.md')); const index: SearchIndexEntry[] = []; const publicationHolds: string[] = []; + const publishedSourceFiles: string[] = []; for (const file of files) { const content = await readFile(join(POSTS_DIR, file), 'utf-8'); const meta = parseFrontmatter(content); if (!meta) continue; - const slug = - (meta.slug as string) ?? file.replace('.md', '').replace(/^\d{4}-\d{2}-\d{2}-/, ''); + const slug = (meta.slug as string) ?? file.replace('.md', '').replace(/^\d{4}-\d{2}-\d{2}-/, ''); if (meta.published !== true) { if (meta.published === false) publicationHolds.push(slug); continue; @@ -49,9 +51,10 @@ async function main(): Promise { } const tagList = Array.isArray(meta.tags) ? (meta.tags as string[]) : []; - const editorialTier = - typeof meta.editorial_tier === 'string' ? meta.editorial_tier : undefined; + const editorialTier = typeof meta.editorial_tier === 'string' ? meta.editorial_tier : undefined; + const sourceFile = `/src/posts/${file}`; + publishedSourceFiles.push(sourceFile); index.push({ id: slug, title: String(meta.title ?? slug), @@ -67,37 +70,49 @@ async function main(): Promise { : {}), slug, date: String(meta.date ?? ''), - source_file: `/src/posts/${file}`, + source_file: sourceFile, body_excerpt, published: Boolean(meta.published), - reading_time: - typeof meta.reading_time === 'number' - ? meta.reading_time - : computeReadingTime(plain), - feature_image: - typeof meta.feature_image === 'string' ? meta.feature_image : undefined, - thumbnail_image: - typeof meta.thumbnail_image === 'string' ? meta.thumbnail_image : undefined, - featured: - typeof meta.featured === 'boolean' ? meta.featured : undefined, - author_slug: - typeof meta.author_slug === 'string' ? meta.author_slug : undefined, - original_url: - typeof meta.original_url === 'string' ? meta.original_url : undefined, - excerpt: typeof meta.excerpt === 'string' ? meta.excerpt : undefined + reading_time: typeof meta.reading_time === 'number' ? meta.reading_time : computeReadingTime(plain), + feature_image: typeof meta.feature_image === 'string' ? meta.feature_image : undefined, + thumbnail_image: typeof meta.thumbnail_image === 'string' ? meta.thumbnail_image : undefined, + featured: typeof meta.featured === 'boolean' ? meta.featured : undefined, + author_slug: typeof meta.author_slug === 'string' ? meta.author_slug : undefined, + original_url: typeof meta.original_url === 'string' ? meta.original_url : undefined, + excerpt: typeof meta.excerpt === 'string' ? meta.excerpt : undefined, }); } // Sort by date descending index.sort((a, b) => b.date.localeCompare(a.date)); publicationHolds.sort((a, b) => a.localeCompare(b)); + publishedSourceFiles.sort((a, b) => a.localeCompare(b)); + + const publishedPostLoadersSource = [ + '// Generated by scripts/generate-search-index.mts. Do not edit by hand.', + 'export const publishedPostLoaders = {', + ...publishedSourceFiles.map( + (sourceFile) => `\t${JSON.stringify(sourceFile)}: () => import(${JSON.stringify(sourceFile)}),`, + ), + '};', + '', + ].join('\n'); + const publishedPostLoaders = await formatWithPrettier(publishedPostLoadersSource, { + parser: 'typescript', + printWidth: 120, + singleQuote: true, + trailingComma: 'all', + useTabs: true, + }); await Promise.all([ writeFile(OUTPUT, JSON.stringify(index), 'utf-8'), writeFile(PUBLICATION_HOLDS_OUTPUT, JSON.stringify(publicationHolds), 'utf-8'), + writeFile(PUBLISHED_POST_LOADERS_OUTPUT, publishedPostLoaders, 'utf-8'), ]); console.log(`Search index: ${index.length} posts -> ${OUTPUT}`); console.log(`Publication holds: ${publicationHolds.length} slugs -> ${PUBLICATION_HOLDS_OUTPUT}`); + console.log(`Published post loaders: ${publishedSourceFiles.length} modules -> ${PUBLISHED_POST_LOADERS_OUTPUT}`); } main().catch((err) => { diff --git a/scripts/run-sveltekit-vite-build-bazel.mjs b/scripts/run-sveltekit-vite-build-bazel.mjs index c678036..7693637 100644 --- a/scripts/run-sveltekit-vite-build-bazel.mjs +++ b/scripts/run-sveltekit-vite-build-bazel.mjs @@ -64,6 +64,8 @@ if (!indexHtml.includes('')) { throw new Error('SvelteKit build output index.html is missing doctype'); } +assertUnpublishedPostContentExcluded(); + console.log( `SvelteKit/Vite build smoke passed for ${packageJson.name}; output=${indexPath}; mermaid=${process.env.MERMAID_PRERENDER}`, ); @@ -86,6 +88,85 @@ function copyInputsToBuildRoot() { } } +function assertUnpublishedPostContentExcluded() { + const postsDir = join(buildRoot, 'src', 'posts'); + const appDir = join(buildRoot, 'build', '_app'); + const posts = readdirSync(postsDir) + .filter((file) => file.endsWith('.md')) + .map((file) => ({ file, source: readFileSync(join(postsDir, file), 'utf8') })); + const publishedCorpus = posts + .filter(({ source }) => /^published:\s*true\s*$/m.test(frontmatter(source))) + .map(({ source }) => source) + .join('\n'); + const unpublishedPosts = posts + .filter(({ source }) => !/^published:\s*true\s*$/m.test(frontmatter(source))) + .map(({ file, source }) => ({ + file, + sentinel: publicationSentinel(file, source, publishedCorpus), + })); + const appAssets = collectFiles(appDir).map((file) => ({ + file, + source: readFileSync(file, 'utf8'), + })); + + for (const unpublishedPost of unpublishedPosts) { + const leak = appAssets.find(({ source }) => assetContains(source, unpublishedPost.sentinel)); + if (leak) { + throw new Error(`Unpublished post ${unpublishedPost.file} leaked into client asset ${leak.file}`); + } + } + + console.log(`Unpublished-post asset scan passed for ${unpublishedPosts.length} posts`); +} + +function frontmatter(source) { + const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + return match?.[1] ?? ''; +} + +function publicationSentinel(file, source, publishedCorpus) { + const block = frontmatter(source); + const bodyLines = source + .slice(source.indexOf(block) + block.length) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length >= 32 && !/^(?:[#>*_`-]|\d+\.)/.test(line)); + const candidates = [frontmatterScalar(block, 'description'), frontmatterScalar(block, 'title'), ...bodyLines].filter( + (candidate) => candidate.length >= 24, + ); + const sentinel = candidates.find((candidate) => !publishedCorpus.includes(candidate)); + if (!sentinel) { + throw new Error(`Unpublished post ${file} has no unique public-asset sentinel`); + } + return sentinel; +} + +function frontmatterScalar(block, key) { + const match = block.match(new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm')); + if (!match) return ''; + const raw = match[1].trim(); + if (raw.startsWith('"') && raw.endsWith('"')) { + return JSON.parse(raw); + } + if (raw.startsWith("'") && raw.endsWith("'")) { + return raw.slice(1, -1).replaceAll("''", "'"); + } + return raw; +} + +function assetContains(source, sentinel) { + const jsonEscaped = JSON.stringify(sentinel).slice(1, -1); + const singleQuoteEscaped = sentinel.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); + return source.includes(sentinel) || source.includes(jsonEscaped) || source.includes(singleQuoteEscaped); +} + +function collectFiles(root) { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const path = join(root, entry.name); + return entry.isDirectory() ? collectFiles(path) : [path]; + }); +} + function copyPath(source, destination) { if (!existsSync(source)) { throw new Error(`Missing declared build input: ${source}`); diff --git a/src/lib/data/blog-post-loaders.generated.ts b/src/lib/data/blog-post-loaders.generated.ts new file mode 100644 index 0000000..e448fe5 --- /dev/null +++ b/src/lib/data/blog-post-loaders.generated.ts @@ -0,0 +1,244 @@ +// Generated by scripts/generate-search-index.mts. Do not edit by hand. +export const publishedPostLoaders = { + '/src/posts/2016-12-13-example-photo-work.md': () => import('/src/posts/2016-12-13-example-photo-work.md'), + '/src/posts/2016-12-13-required-custom-tools.md': () => import('/src/posts/2016-12-13-required-custom-tools.md'), + '/src/posts/2016-12-13-the-audeasy-build.md': () => import('/src/posts/2016-12-13-the-audeasy-build.md'), + '/src/posts/2016-12-13-waxwing-audio.md': () => import('/src/posts/2016-12-13-waxwing-audio.md'), + '/src/posts/2016-12-14-opportunistic-birding-14122016.md': () => + import('/src/posts/2016-12-14-opportunistic-birding-14122016.md'), + '/src/posts/2016-12-15-beta-how-to-build-the-phone-charger.md': () => + import('/src/posts/2016-12-15-beta-how-to-build-the-phone-charger.md'), + '/src/posts/2016-12-15-building-electrostatic-earspeakers.md': () => + import('/src/posts/2016-12-15-building-electrostatic-earspeakers.md'), + '/src/posts/2016-12-15-diy-electrostatic-amp-options.md': () => + import('/src/posts/2016-12-15-diy-electrostatic-amp-options.md'), + '/src/posts/2016-12-15-music-sketches-1-playing-with-rhythm-division-and-jazzy-chords.md': () => + import('/src/posts/2016-12-15-music-sketches-1-playing-with-rhythm-division-and-jazzy-chords.md'), + '/src/posts/2016-12-16-music-sketches-2-rethinking-funtwos-guitar-tone-discipline.md': () => + import('/src/posts/2016-12-16-music-sketches-2-rethinking-funtwos-guitar-tone-discipline.md'), + '/src/posts/2016-12-21-develop-limitless-habitual-soithen_soicould_-ethos.md': () => + import('/src/posts/2016-12-21-develop-limitless-habitual-soithen_soicould_-ethos.md'), + '/src/posts/2017-02-20-wolf-pine-fox-park-1-bonus-winter-birds.md': () => + import('/src/posts/2017-02-20-wolf-pine-fox-park-1-bonus-winter-birds.md'), + '/src/posts/2017-03-02-wolf-pine-fox-park-2-22717.md': () => + import('/src/posts/2017-03-02-wolf-pine-fox-park-2-22717.md'), + '/src/posts/2017-03-04-wolf-pine-fox-park-3-3317-noon-ish-time.md': () => + import('/src/posts/2017-03-04-wolf-pine-fox-park-3-3317-noon-ish-time.md'), + '/src/posts/2017-03-05-wolf-pine-fox-park-4-3517-noon-ish-time.md': () => + import('/src/posts/2017-03-05-wolf-pine-fox-park-4-3517-noon-ish-time.md'), + '/src/posts/2017-03-11-wolf-pine-fox-park-6-31017-theoretical-owls-and-real-tree-bark.md': () => + import('/src/posts/2017-03-11-wolf-pine-fox-park-6-31017-theoretical-owls-and-real-tree-bark.md'), + '/src/posts/2017-03-12-wolf-pine-fox-park-7-31217-surprise-its-cold.md': () => + import('/src/posts/2017-03-12-wolf-pine-fox-park-7-31217-surprise-its-cold.md'), + '/src/posts/2017-03-13-sit-spot-n-1-the-gho-talk.md': () => + import('/src/posts/2017-03-13-sit-spot-n-1-the-gho-talk.md'), + '/src/posts/2017-03-17-wolf-pine-fox-park-8-31617-someone-bumped-the-snow-machine.md': () => + import('/src/posts/2017-03-17-wolf-pine-fox-park-8-31617-someone-bumped-the-snow-machine.md'), + '/src/posts/2017-03-20-boutique-everything-when-the-hobby-grows-up.md': () => + import('/src/posts/2017-03-20-boutique-everything-when-the-hobby-grows-up.md'), + '/src/posts/2017-03-28-wolf-pine-fox-park-9-32617-everyone-is-on-vacation.md': () => + import('/src/posts/2017-03-28-wolf-pine-fox-park-9-32617-everyone-is-on-vacation.md'), + '/src/posts/2017-03-30-beaver-dam-quincy-bog-1-33017-the-world-has-gone-mad.md': () => + import('/src/posts/2017-03-30-beaver-dam-quincy-bog-1-33017-the-world-has-gone-mad.md'), + '/src/posts/2017-03-31-parking-lotsit-spot-fox-park-10-33117-whoos-clues.md': () => + import('/src/posts/2017-03-31-parking-lotsit-spot-fox-park-10-33117-whoos-clues.md'), + '/src/posts/2017-04-02-wolf-pine-fox-park-10-4117-it-is-a-snow-show-debunking-the-melanistic-dogamount.md': () => + import('/src/posts/2017-04-02-wolf-pine-fox-park-10-4117-it-is-a-snow-show-debunking-the-melanistic-dogamount.md'), + '/src/posts/2017-04-03-parking-lotsit-spot-fox-park-12-4317-owling-2-hours-before-sunrise.md': () => + import('/src/posts/2017-04-03-parking-lotsit-spot-fox-park-12-4317-owling-2-hours-before-sunrise.md'), + '/src/posts/2017-04-04-603.md': () => import('/src/posts/2017-04-04-603.md'), + '/src/posts/2017-04-08-pt-1-reflecting-on-stunt-culture-theories-and-frisbee.md': () => + import('/src/posts/2017-04-08-pt-1-reflecting-on-stunt-culture-theories-and-frisbee.md'), + '/src/posts/2017-04-08-pt-2-reflecting-on-stunt-culture-scooters.md': () => + import('/src/posts/2017-04-08-pt-2-reflecting-on-stunt-culture-scooters.md'), + '/src/posts/2017-04-11-bird-observations-today-langdon-woods.md': () => + import('/src/posts/2017-04-11-bird-observations-today-langdon-woods.md'), + '/src/posts/2017-04-12-fox-park-14-41017-1-if-by-land-16-if-by-ear.md': () => + import('/src/posts/2017-04-12-fox-park-14-41017-1-if-by-land-16-if-by-ear.md'), + '/src/posts/2017-04-12-fox-park-15-41217-1-if-by-land-17-if-by-ear.md': () => + import('/src/posts/2017-04-12-fox-park-15-41217-1-if-by-land-17-if-by-ear.md'), + '/src/posts/2017-04-17-fox-park-16-and-17-41617big-toad-small-vireo-yay.md': () => + import('/src/posts/2017-04-17-fox-park-16-and-17-41617big-toad-small-vireo-yay.md'), + '/src/posts/2017-04-19-fox-park-18-41917-in-class-roamings.md': () => + import('/src/posts/2017-04-19-fox-park-18-41917-in-class-roamings.md'), + '/src/posts/2017-04-20-addendum-to-secret-beach-area-natural-history-class-walk.md': () => + import('/src/posts/2017-04-20-addendum-to-secret-beach-area-natural-history-class-walk.md'), + '/src/posts/2017-05-02-langdon-woods-with-kurt-plants.md': () => + import('/src/posts/2017-05-02-langdon-woods-with-kurt-plants.md'), + '/src/posts/2017-05-03-the-big-len-list.md': () => import('/src/posts/2017-05-03-the-big-len-list.md'), + '/src/posts/2017-05-04-early-morning-guitar-session-ala-laundry.md': () => + import('/src/posts/2017-05-04-early-morning-guitar-session-ala-laundry.md'), + '/src/posts/2017-05-04-fox-park-to-langdon-morning-birds.md': () => + import('/src/posts/2017-05-04-fox-park-to-langdon-morning-birds.md'), + '/src/posts/2017-05-05-new-photopromotional-media-business-pages-in-the-works.md': () => + import('/src/posts/2017-05-05-new-photopromotional-media-business-pages-in-the-works.md'), + '/src/posts/2017-05-07-hunting-for-trees-birds.md': () => import('/src/posts/2017-05-07-hunting-for-trees-birds.md'), + '/src/posts/2017-05-07-mega-langdon.md': () => import('/src/posts/2017-05-07-mega-langdon.md'), + '/src/posts/2017-05-08-new-business-site-up-and-running.md': () => + import('/src/posts/2017-05-08-new-business-site-up-and-running.md'), + '/src/posts/2017-05-08-pre-dawn-fox-park-lot-walk-birding-by-ear.md': () => + import('/src/posts/2017-05-08-pre-dawn-fox-park-lot-walk-birding-by-ear.md'), + '/src/posts/2017-05-12-wolf-pine-fox-park-silence.md': () => + import('/src/posts/2017-05-12-wolf-pine-fox-park-silence.md'), + '/src/posts/2017-05-17-rugby-morning-2.md': () => import('/src/posts/2017-05-17-rugby-morning-2.md'), + '/src/posts/2017-05-18-rugby-morning-3-6.md': () => import('/src/posts/2017-05-18-rugby-morning-3-6.md'), + '/src/posts/2017-07-03-birding-beyond-binos-bird-apps-and-the-guide.md': () => + import('/src/posts/2017-07-03-birding-beyond-binos-bird-apps-and-the-guide.md'), + '/src/posts/2017-07-25-mpcnc-it-moves.md': () => import('/src/posts/2017-07-25-mpcnc-it-moves.md'), + '/src/posts/2017-07-27-gallery-of-my-proper-photos-nh-warbler-research.md': () => + import('/src/posts/2017-07-27-gallery-of-my-proper-photos-nh-warbler-research.md'), + '/src/posts/2017-07-27-gallery-of-warblers-in-the-hand.md': () => + import('/src/posts/2017-07-27-gallery-of-warblers-in-the-hand.md'), + '/src/posts/2018-03-08-how-to-make-a-aws-r-server.md': () => + import('/src/posts/2018-03-08-how-to-make-a-aws-r-server.md'), + '/src/posts/2018-03-13-pages-fresh-from-the-book.md': () => + import('/src/posts/2018-03-13-pages-fresh-from-the-book.md'), + '/src/posts/2018-03-19-ebpp-my-epic-birding-prediction-project-in-r.md': () => + import('/src/posts/2018-03-19-ebpp-my-epic-birding-prediction-project-in-r.md'), + '/src/posts/2018-04-03-using-esri-arcgis-arcmap-on-mac-osx-2-methods.md': () => + import('/src/posts/2018-04-03-using-esri-arcgis-arcmap-on-mac-osx-2-methods.md'), + '/src/posts/2018-04-05-using-esri-arcgis-arcmap-in-the-aws-cloud.md': () => + import('/src/posts/2018-04-05-using-esri-arcgis-arcmap-in-the-aws-cloud.md'), + '/src/posts/2018-04-10-intro-to-the-aws-cloud-9-ide.md': () => + import('/src/posts/2018-04-10-intro-to-the-aws-cloud-9-ide.md'), + '/src/posts/2018-04-18-signs-of-thoughts-of-possible-spring.md': () => + import('/src/posts/2018-04-18-signs-of-thoughts-of-possible-spring.md'), + '/src/posts/2018-05-23-birding-at-plumb-island.md': () => import('/src/posts/2018-05-23-birding-at-plumb-island.md'), + '/src/posts/2018-05-24-840-watts-of-solar-power.md': () => + import('/src/posts/2018-05-24-840-watts-of-solar-power.md'), + '/src/posts/2018-05-28-gathering-point-data-using-compass-55-on-apple-ios.md': () => + import('/src/posts/2018-05-28-gathering-point-data-using-compass-55-on-apple-ios.md'), + '/src/posts/2018-05-31-research-year-two-three-photos.md': () => + import('/src/posts/2018-05-31-research-year-two-three-photos.md'), + '/src/posts/2018-05-31-solar-upgrades.md': () => import('/src/posts/2018-05-31-solar-upgrades.md'), + '/src/posts/2018-06-10-how-to-query-kml-point-data-as-csv-using-qgis-and-r.md': () => + import('/src/posts/2018-06-10-how-to-query-kml-point-data-as-csv-using-qgis-and-r.md'), + '/src/posts/2018-06-24-how-to-generate-convex-hull-polygons-and-centroids-from-kml.md': () => + import('/src/posts/2018-06-24-how-to-generate-convex-hull-polygons-and-centroids-from-kml.md'), + '/src/posts/2018-06-25-off-grid-file-sharing-with-samba-gl-inet.md': () => + import('/src/posts/2018-06-25-off-grid-file-sharing-with-samba-gl-inet.md'), + '/src/posts/2018-07-16-deploy-a-shiny-web-app-in-r-using-aws-ec2-red-hat.md': () => + import('/src/posts/2018-07-16-deploy-a-shiny-web-app-in-r-using-aws-ec2-red-hat.md'), + '/src/posts/2018-07-23-gdal-for-r-server-on-ubuntu-kml-spatial-libraries-and-more.md': () => + import('/src/posts/2018-07-23-gdal-for-r-server-on-ubuntu-kml-spatial-libraries-and-more.md'), + '/src/posts/2018-07-27-new-app-kml-search-and-convert.md': () => + import('/src/posts/2018-07-27-new-app-kml-search-and-convert.md'), + '/src/posts/2018-07-28-shiny-app-specific-repo-live.md': () => + import('/src/posts/2018-07-28-shiny-app-specific-repo-live.md'), + '/src/posts/2018-09-01-early-hardware-and-maker-projects.md': () => + import('/src/posts/2018-09-01-early-hardware-and-maker-projects.md'), + '/src/posts/2018-09-12-github-update-9-12-18-shiny-apps.md': () => + import('/src/posts/2018-09-12-github-update-9-12-18-shiny-apps.md'), + '/src/posts/2018-09-12-quick-fix-254-character-limit-in-esri-story-map.md': () => + import('/src/posts/2018-09-12-quick-fix-254-character-limit-in-esri-story-map.md'), + '/src/posts/2018-11-05-recycled-personal-cloud-computing-under-nat.md': () => + import('/src/posts/2018-11-05-recycled-personal-cloud-computing-under-nat.md'), + '/src/posts/2018-11-25-deploy-shiny-r-apps-along-node-js.md': () => + import('/src/posts/2018-11-25-deploy-shiny-r-apps-along-node-js.md'), + '/src/posts/2019-02-20-installing-chapel-language-on-mac-and-linux.md': () => + import('/src/posts/2019-02-20-installing-chapel-language-on-mac-and-linux.md'), + '/src/posts/2019-03-10-querying-kml-data-with-qgis-and-r.md': () => + import('/src/posts/2019-03-10-querying-kml-data-with-qgis-and-r.md'), + '/src/posts/2019-03-21-notes-on-a-free-and-open-source-notes-app-joplin.md': () => + import('/src/posts/2019-03-21-notes-on-a-free-and-open-source-notes-app-joplin.md'), + '/src/posts/2019-04-15-open-source-collaboration-and-higher-education.md': () => + import('/src/posts/2019-04-15-open-source-collaboration-and-higher-education.md'), + '/src/posts/2019-04-18-warbler-trillers-of-the-charles.md': () => + import('/src/posts/2019-04-18-warbler-trillers-of-the-charles.md'), + '/src/posts/2019-05-01-succession-warblers-and-gis.md': () => + import('/src/posts/2019-05-01-succession-warblers-and-gis.md'), + '/src/posts/2019-07-05-summer-2019-update.md': () => import('/src/posts/2019-07-05-summer-2019-update.md'), + '/src/posts/2019-08-04-persistent-live-ubuntu-for-college.md': () => + import('/src/posts/2019-08-04-persistent-live-ubuntu-for-college.md'), + '/src/posts/2019-09-30-decentralized-pi-video-monitoring-w-motioneye-batman.md': () => + import('/src/posts/2019-09-30-decentralized-pi-video-monitoring-w-motioneye-batman.md'), + '/src/posts/2019-10-07-gdal-for-gis-on-unix-using-a-mac-or-better-linux.md': () => + import('/src/posts/2019-10-07-gdal-for-gis-on-unix-using-a-mac-or-better-linux.md'), + '/src/posts/2019-10-30-qemu-for-raspian-arm.md': () => import('/src/posts/2019-10-30-qemu-for-raspian-arm.md'), + '/src/posts/2020-03-17-when-it-must-be-windows.md': () => import('/src/posts/2020-03-17-when-it-must-be-windows.md'), + '/src/posts/2020-03-23-1607.md': () => import('/src/posts/2020-03-23-1607.md'), + '/src/posts/2020-04-02-ppe-me.md': () => import('/src/posts/2020-04-02-ppe-me.md'), + '/src/posts/2020-04-05-dm-shields-fusion-360.md': () => import('/src/posts/2020-04-05-dm-shields-fusion-360.md'), + '/src/posts/2020-04-09-convert-heic-png.md': () => import('/src/posts/2020-04-09-convert-heic-png.md'), + '/src/posts/2020-04-25-install-adobe-applications-on-aws-workspaces.md': () => + import('/src/posts/2020-04-25-install-adobe-applications-on-aws-workspaces.md'), + '/src/posts/2020-04-27-oes.md': () => import('/src/posts/2020-04-27-oes.md'), + '/src/posts/2020-05-18-while-at-a-safe-distance.md': () => + import('/src/posts/2020-05-18-while-at-a-safe-distance.md'), + '/src/posts/2020-06-04-prius-printers.md': () => import('/src/posts/2020-06-04-prius-printers.md'), + '/src/posts/2020-06-14-the-ebird-api-regioncode.md': () => + import('/src/posts/2020-06-14-the-ebird-api-regioncode.md'), + '/src/posts/2020-06-24-1768.md': () => import('/src/posts/2020-06-24-1768.md'), + '/src/posts/2020-06-24-1820.md': () => import('/src/posts/2020-06-24-1820.md'), + '/src/posts/2020-06-29-osselc-monday.md': () => import('/src/posts/2020-06-29-osselc-monday.md'), + '/src/posts/2020-07-11-1784.md': () => import('/src/posts/2020-07-11-1784.md'), + '/src/posts/2020-07-15-this-that-7-15.md': () => import('/src/posts/2020-07-15-this-that-7-15.md'), + '/src/posts/2020-07-23-ever-tried-to-chrome-remote-ubuntu-xd.md': () => + import('/src/posts/2020-07-23-ever-tried-to-chrome-remote-ubuntu-xd.md'), + '/src/posts/2020-08-23-purple-prius-parts.md': () => import('/src/posts/2020-08-23-purple-prius-parts.md'), + '/src/posts/2020-09-05-kvm-does-fruit-xcode-from-qemu.md': () => + import('/src/posts/2020-09-05-kvm-does-fruit-xcode-from-qemu.md'), + '/src/posts/2020-09-06-1984.md': () => import('/src/posts/2020-09-06-1984.md'), + '/src/posts/2020-10-28-asynchronous-http-methods-from-typescript.md': () => + import('/src/posts/2020-10-28-asynchronous-http-methods-from-typescript.md'), + '/src/posts/2020-10-28-evening-metal-9-14-20.md': () => import('/src/posts/2020-10-28-evening-metal-9-14-20.md'), + '/src/posts/2020-12-23-a-boilerplate-for-flask-react-typescript-mongodb.md': () => + import('/src/posts/2020-12-23-a-boilerplate-for-flask-react-typescript-mongodb.md'), + '/src/posts/2021-01-02-naive-shenanigans-measuring-distance-to-roi-w-opencv.md': () => + import('/src/posts/2021-01-02-naive-shenanigans-measuring-distance-to-roi-w-opencv.md'), + '/src/posts/2021-01-23-chindogu-ascii-art-i-suppose.md': () => + import('/src/posts/2021-01-23-chindogu-ascii-art-i-suppose.md'), + '/src/posts/2021-02-17-tmpui-the-merlin-sound-id-project.md': () => + import('/src/posts/2021-02-17-tmpui-the-merlin-sound-id-project.md'), + '/src/posts/2021-03-18-bits-bobs-mushstools-toadrooms.md': () => + import('/src/posts/2021-03-18-bits-bobs-mushstools-toadrooms.md'), + '/src/posts/2021-04-23-morning-metal-4-22-21.md': () => import('/src/posts/2021-04-23-morning-metal-4-22-21.md'), + '/src/posts/2021-11-30-2393.md': () => import('/src/posts/2021-11-30-2393.md'), + '/src/posts/2022-01-01-datasets-plots-graphs-charts.md': () => + import('/src/posts/2022-01-01-datasets-plots-graphs-charts.md'), + '/src/posts/2022-02-06-makerspace-financial-reporting-w-ipython.md': () => + import('/src/posts/2022-02-06-makerspace-financial-reporting-w-ipython.md'), + '/src/posts/2022-05-15-running-cornells-dla-makerspace.md': () => + import('/src/posts/2022-05-15-running-cornells-dla-makerspace.md'), + '/src/posts/2022-05-23-this-and-that.md': () => import('/src/posts/2022-05-23-this-and-that.md'), + '/src/posts/2023-03-01-fusion-360-for-3d-printing-w-jess.md': () => + import('/src/posts/2023-03-01-fusion-360-for-3d-printing-w-jess.md'), + '/src/posts/2023-11-27-turkey-probe.md': () => import('/src/posts/2023-11-27-turkey-probe.md'), + '/src/posts/2023-12-18-lets-write-a-simple-efficient-and-fast-flask-based-image-server-in-an-afternoon.md': () => + import('/src/posts/2023-12-18-lets-write-a-simple-efficient-and-fast-flask-based-image-server-in-an-afternoon.md'), + '/src/posts/2024-01-02-accuwix.md': () => import('/src/posts/2024-01-02-accuwix.md'), + '/src/posts/2024-02-22-i-wrote-a-mutual-aid-mental-health-service.md': () => + import('/src/posts/2024-02-22-i-wrote-a-mutual-aid-mental-health-service.md'), + '/src/posts/2024-05-22-what-have-i-been-up-to-these-last-few-months.md': () => + import('/src/posts/2024-05-22-what-have-i-been-up-to-these-last-few-months.md'), + '/src/posts/2024-12-31-ligature-test-fixture.md': () => import('/src/posts/2024-12-31-ligature-test-fixture.md'), + '/src/posts/2026-02-26-aperture-tagged-devices-and-the-tsnet-escape-hatch.md': () => + import('/src/posts/2026-02-26-aperture-tagged-devices-and-the-tsnet-escape-hatch.md'), + '/src/posts/2026-03-04-from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life.md': () => + import('/src/posts/2026-03-04-from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life.md'), + '/src/posts/2026-03-04-week-notes-cyborgs-servers-and-sending.md': () => + import('/src/posts/2026-03-04-week-notes-cyborgs-servers-and-sending.md'), + '/src/posts/2026-03-04-xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives.md': () => + import('/src/posts/2026-03-04-xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives.md'), + '/src/posts/2026-03-12-updated-resume-and-cv.md': () => import('/src/posts/2026-03-12-updated-resume-and-cv.md'), + '/src/posts/2026-03-13-the-winrm-forkbomb-how-ansible-molecule-locks-you-out-of-active-directory.md': () => + import('/src/posts/2026-03-13-the-winrm-forkbomb-how-ansible-molecule-locks-you-out-of-active-directory.md'), + '/src/posts/2026-03-21-week-notes-indeterminism-passkeys-and-spring.md': () => + import('/src/posts/2026-03-21-week-notes-indeterminism-passkeys-and-spring.md'), + '/src/posts/2026-04-25-reproducible-host-probes-nix-chapel-dhall.md': () => + import('/src/posts/2026-04-25-reproducible-host-probes-nix-chapel-dhall.md'), + '/src/posts/2026-04-28-darwin-nic-docs-are-live.md': () => + import('/src/posts/2026-04-28-darwin-nic-docs-are-live.md'), + '/src/posts/2026-04-30-small-zig-libraries-for-apple-shaped-escape-hatches.md': () => + import('/src/posts/2026-04-30-small-zig-libraries-for-apple-shaped-escape-hatches.md'), + '/src/posts/2026-05-03-was110-fidium-gonetspeed-omci-personalities.md': () => + import('/src/posts/2026-05-03-was110-fidium-gonetspeed-omci-personalities.md'), + '/src/posts/2026-05-11-trashmonitor-tailnet-window-into-the-hardware-pile.md': () => + import('/src/posts/2026-05-11-trashmonitor-tailnet-window-into-the-hardware-pile.md'), + '/src/posts/2026-05-29-gingersnap-cookies.md': () => import('/src/posts/2026-05-29-gingersnap-cookies.md'), + '/src/posts/2026-06-08-glue-you-can-see-in-uv.md': () => import('/src/posts/2026-06-08-glue-you-can-see-in-uv.md'), + '/src/posts/2026-06-08-week-notes-scroll-wheels-and-chasing-the-sun.md': () => + import('/src/posts/2026-06-08-week-notes-scroll-wheels-and-chasing-the-sun.md'), + '/src/posts/2026-06-09-clearing-canon-5b00-lock-native-key-free-megatank-reset.md': () => + import('/src/posts/2026-06-09-clearing-canon-5b00-lock-native-key-free-megatank-reset.md'), +}; diff --git a/src/routes/blog/[slug]/+page.ts b/src/routes/blog/[slug]/+page.ts index 7ce7589..2c22fff 100644 --- a/src/routes/blog/[slug]/+page.ts +++ b/src/routes/blog/[slug]/+page.ts @@ -1,5 +1,6 @@ import type { PageLoad, EntryGenerator } from './$types'; import { error } from '@sveltejs/kit'; +import { publishedPostLoaders } from '$lib/data/blog-post-loaders.generated'; import searchIndexData from '../../../../static/search-index.json'; import publicationHoldsData from '../../../../static/blog-publication-holds.json'; @@ -34,7 +35,7 @@ function getSortedPosts(): { slug: string; title: string; date: string; tags: st slug: entry.slug, title: entry.title ?? entry.slug, date: entry.date ?? '', - tags: entry.tag_list ?? [] + tags: entry.tag_list ?? [], })) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); } @@ -42,7 +43,7 @@ function getSortedPosts(): { slug: string; title: string; date: string; tags: st function computeRelatedPosts( currentSlug: string, currentTags: Set, - posts: { slug: string; title: string; date: string; tags: string[] }[] + posts: { slug: string; title: string; date: string; tags: string[] }[], ): { slug: string; title: string; date: string }[] { if (currentTags.size === 0) { // No tags — return 3 most recent posts (excluding current) @@ -74,12 +75,14 @@ export const load: PageLoad = async ({ params }) => { throw error(404, 'Post not found'); } - const lazyModules = import.meta.glob('/src/posts/*.md'); const searchEntry = searchIndex.find((entry) => entry.slug === params.slug); const matchedPath = searchEntry?.source_file; + const postLoader = matchedPath + ? (publishedPostLoaders as Record Promise>)[matchedPath] + : undefined; - if (matchedPath && lazyModules[matchedPath]) { - const post = (await lazyModules[matchedPath]()) as { + if (matchedPath && postLoader) { + const post = (await postLoader()) as { default: import('svelte').Snippet; metadata: PostMeta; }; From 52e5ee3401ca3455afeccde9eb9bb8c376cc30c0 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Thu, 16 Jul 2026 23:53:17 -0400 Subject: [PATCH 4/6] fix(blog): enumerate published post modules for checks Keep source-file lookup keys while generating a statically analyzable Vite glob that excludes held posts and passes svelte-check in the GF Bazel sandbox. Refs TIN-2979 --- scripts/generate-search-index.mts | 11 +- src/lib/data/blog-post-loaders.generated.ts | 385 ++++++++------------ 2 files changed, 148 insertions(+), 248 deletions(-) diff --git a/scripts/generate-search-index.mts b/scripts/generate-search-index.mts index df3e03d..1a98ca2 100644 --- a/scripts/generate-search-index.mts +++ b/scripts/generate-search-index.mts @@ -90,11 +90,9 @@ async function main(): Promise { const publishedPostLoadersSource = [ '// Generated by scripts/generate-search-index.mts. Do not edit by hand.', - 'export const publishedPostLoaders = {', - ...publishedSourceFiles.map( - (sourceFile) => `\t${JSON.stringify(sourceFile)}: () => import(${JSON.stringify(sourceFile)}),`, - ), - '};', + 'export const publishedPostLoaders = import.meta.glob([', + ...publishedSourceFiles.map((sourceFile) => `\t${JSON.stringify(sourceFile)},`), + ']);', '', ].join('\n'); const publishedPostLoaders = await formatWithPrettier(publishedPostLoadersSource, { @@ -104,6 +102,9 @@ async function main(): Promise { trailingComma: 'all', useTabs: true, }); + if (publishedPostLoaders.includes("'/src/posts/*.md'")) { + throw new Error('Generated post loaders must enumerate published modules; broad globs can leak held posts'); + } await Promise.all([ writeFile(OUTPUT, JSON.stringify(index), 'utf-8'), diff --git a/src/lib/data/blog-post-loaders.generated.ts b/src/lib/data/blog-post-loaders.generated.ts index e448fe5..d66b08c 100644 --- a/src/lib/data/blog-post-loaders.generated.ts +++ b/src/lib/data/blog-post-loaders.generated.ts @@ -1,244 +1,143 @@ // Generated by scripts/generate-search-index.mts. Do not edit by hand. -export const publishedPostLoaders = { - '/src/posts/2016-12-13-example-photo-work.md': () => import('/src/posts/2016-12-13-example-photo-work.md'), - '/src/posts/2016-12-13-required-custom-tools.md': () => import('/src/posts/2016-12-13-required-custom-tools.md'), - '/src/posts/2016-12-13-the-audeasy-build.md': () => import('/src/posts/2016-12-13-the-audeasy-build.md'), - '/src/posts/2016-12-13-waxwing-audio.md': () => import('/src/posts/2016-12-13-waxwing-audio.md'), - '/src/posts/2016-12-14-opportunistic-birding-14122016.md': () => - import('/src/posts/2016-12-14-opportunistic-birding-14122016.md'), - '/src/posts/2016-12-15-beta-how-to-build-the-phone-charger.md': () => - import('/src/posts/2016-12-15-beta-how-to-build-the-phone-charger.md'), - '/src/posts/2016-12-15-building-electrostatic-earspeakers.md': () => - import('/src/posts/2016-12-15-building-electrostatic-earspeakers.md'), - '/src/posts/2016-12-15-diy-electrostatic-amp-options.md': () => - import('/src/posts/2016-12-15-diy-electrostatic-amp-options.md'), - '/src/posts/2016-12-15-music-sketches-1-playing-with-rhythm-division-and-jazzy-chords.md': () => - import('/src/posts/2016-12-15-music-sketches-1-playing-with-rhythm-division-and-jazzy-chords.md'), - '/src/posts/2016-12-16-music-sketches-2-rethinking-funtwos-guitar-tone-discipline.md': () => - import('/src/posts/2016-12-16-music-sketches-2-rethinking-funtwos-guitar-tone-discipline.md'), - '/src/posts/2016-12-21-develop-limitless-habitual-soithen_soicould_-ethos.md': () => - import('/src/posts/2016-12-21-develop-limitless-habitual-soithen_soicould_-ethos.md'), - '/src/posts/2017-02-20-wolf-pine-fox-park-1-bonus-winter-birds.md': () => - import('/src/posts/2017-02-20-wolf-pine-fox-park-1-bonus-winter-birds.md'), - '/src/posts/2017-03-02-wolf-pine-fox-park-2-22717.md': () => - import('/src/posts/2017-03-02-wolf-pine-fox-park-2-22717.md'), - '/src/posts/2017-03-04-wolf-pine-fox-park-3-3317-noon-ish-time.md': () => - import('/src/posts/2017-03-04-wolf-pine-fox-park-3-3317-noon-ish-time.md'), - '/src/posts/2017-03-05-wolf-pine-fox-park-4-3517-noon-ish-time.md': () => - import('/src/posts/2017-03-05-wolf-pine-fox-park-4-3517-noon-ish-time.md'), - '/src/posts/2017-03-11-wolf-pine-fox-park-6-31017-theoretical-owls-and-real-tree-bark.md': () => - import('/src/posts/2017-03-11-wolf-pine-fox-park-6-31017-theoretical-owls-and-real-tree-bark.md'), - '/src/posts/2017-03-12-wolf-pine-fox-park-7-31217-surprise-its-cold.md': () => - import('/src/posts/2017-03-12-wolf-pine-fox-park-7-31217-surprise-its-cold.md'), - '/src/posts/2017-03-13-sit-spot-n-1-the-gho-talk.md': () => - import('/src/posts/2017-03-13-sit-spot-n-1-the-gho-talk.md'), - '/src/posts/2017-03-17-wolf-pine-fox-park-8-31617-someone-bumped-the-snow-machine.md': () => - import('/src/posts/2017-03-17-wolf-pine-fox-park-8-31617-someone-bumped-the-snow-machine.md'), - '/src/posts/2017-03-20-boutique-everything-when-the-hobby-grows-up.md': () => - import('/src/posts/2017-03-20-boutique-everything-when-the-hobby-grows-up.md'), - '/src/posts/2017-03-28-wolf-pine-fox-park-9-32617-everyone-is-on-vacation.md': () => - import('/src/posts/2017-03-28-wolf-pine-fox-park-9-32617-everyone-is-on-vacation.md'), - '/src/posts/2017-03-30-beaver-dam-quincy-bog-1-33017-the-world-has-gone-mad.md': () => - import('/src/posts/2017-03-30-beaver-dam-quincy-bog-1-33017-the-world-has-gone-mad.md'), - '/src/posts/2017-03-31-parking-lotsit-spot-fox-park-10-33117-whoos-clues.md': () => - import('/src/posts/2017-03-31-parking-lotsit-spot-fox-park-10-33117-whoos-clues.md'), - '/src/posts/2017-04-02-wolf-pine-fox-park-10-4117-it-is-a-snow-show-debunking-the-melanistic-dogamount.md': () => - import('/src/posts/2017-04-02-wolf-pine-fox-park-10-4117-it-is-a-snow-show-debunking-the-melanistic-dogamount.md'), - '/src/posts/2017-04-03-parking-lotsit-spot-fox-park-12-4317-owling-2-hours-before-sunrise.md': () => - import('/src/posts/2017-04-03-parking-lotsit-spot-fox-park-12-4317-owling-2-hours-before-sunrise.md'), - '/src/posts/2017-04-04-603.md': () => import('/src/posts/2017-04-04-603.md'), - '/src/posts/2017-04-08-pt-1-reflecting-on-stunt-culture-theories-and-frisbee.md': () => - import('/src/posts/2017-04-08-pt-1-reflecting-on-stunt-culture-theories-and-frisbee.md'), - '/src/posts/2017-04-08-pt-2-reflecting-on-stunt-culture-scooters.md': () => - import('/src/posts/2017-04-08-pt-2-reflecting-on-stunt-culture-scooters.md'), - '/src/posts/2017-04-11-bird-observations-today-langdon-woods.md': () => - import('/src/posts/2017-04-11-bird-observations-today-langdon-woods.md'), - '/src/posts/2017-04-12-fox-park-14-41017-1-if-by-land-16-if-by-ear.md': () => - import('/src/posts/2017-04-12-fox-park-14-41017-1-if-by-land-16-if-by-ear.md'), - '/src/posts/2017-04-12-fox-park-15-41217-1-if-by-land-17-if-by-ear.md': () => - import('/src/posts/2017-04-12-fox-park-15-41217-1-if-by-land-17-if-by-ear.md'), - '/src/posts/2017-04-17-fox-park-16-and-17-41617big-toad-small-vireo-yay.md': () => - import('/src/posts/2017-04-17-fox-park-16-and-17-41617big-toad-small-vireo-yay.md'), - '/src/posts/2017-04-19-fox-park-18-41917-in-class-roamings.md': () => - import('/src/posts/2017-04-19-fox-park-18-41917-in-class-roamings.md'), - '/src/posts/2017-04-20-addendum-to-secret-beach-area-natural-history-class-walk.md': () => - import('/src/posts/2017-04-20-addendum-to-secret-beach-area-natural-history-class-walk.md'), - '/src/posts/2017-05-02-langdon-woods-with-kurt-plants.md': () => - import('/src/posts/2017-05-02-langdon-woods-with-kurt-plants.md'), - '/src/posts/2017-05-03-the-big-len-list.md': () => import('/src/posts/2017-05-03-the-big-len-list.md'), - '/src/posts/2017-05-04-early-morning-guitar-session-ala-laundry.md': () => - import('/src/posts/2017-05-04-early-morning-guitar-session-ala-laundry.md'), - '/src/posts/2017-05-04-fox-park-to-langdon-morning-birds.md': () => - import('/src/posts/2017-05-04-fox-park-to-langdon-morning-birds.md'), - '/src/posts/2017-05-05-new-photopromotional-media-business-pages-in-the-works.md': () => - import('/src/posts/2017-05-05-new-photopromotional-media-business-pages-in-the-works.md'), - '/src/posts/2017-05-07-hunting-for-trees-birds.md': () => import('/src/posts/2017-05-07-hunting-for-trees-birds.md'), - '/src/posts/2017-05-07-mega-langdon.md': () => import('/src/posts/2017-05-07-mega-langdon.md'), - '/src/posts/2017-05-08-new-business-site-up-and-running.md': () => - import('/src/posts/2017-05-08-new-business-site-up-and-running.md'), - '/src/posts/2017-05-08-pre-dawn-fox-park-lot-walk-birding-by-ear.md': () => - import('/src/posts/2017-05-08-pre-dawn-fox-park-lot-walk-birding-by-ear.md'), - '/src/posts/2017-05-12-wolf-pine-fox-park-silence.md': () => - import('/src/posts/2017-05-12-wolf-pine-fox-park-silence.md'), - '/src/posts/2017-05-17-rugby-morning-2.md': () => import('/src/posts/2017-05-17-rugby-morning-2.md'), - '/src/posts/2017-05-18-rugby-morning-3-6.md': () => import('/src/posts/2017-05-18-rugby-morning-3-6.md'), - '/src/posts/2017-07-03-birding-beyond-binos-bird-apps-and-the-guide.md': () => - import('/src/posts/2017-07-03-birding-beyond-binos-bird-apps-and-the-guide.md'), - '/src/posts/2017-07-25-mpcnc-it-moves.md': () => import('/src/posts/2017-07-25-mpcnc-it-moves.md'), - '/src/posts/2017-07-27-gallery-of-my-proper-photos-nh-warbler-research.md': () => - import('/src/posts/2017-07-27-gallery-of-my-proper-photos-nh-warbler-research.md'), - '/src/posts/2017-07-27-gallery-of-warblers-in-the-hand.md': () => - import('/src/posts/2017-07-27-gallery-of-warblers-in-the-hand.md'), - '/src/posts/2018-03-08-how-to-make-a-aws-r-server.md': () => - import('/src/posts/2018-03-08-how-to-make-a-aws-r-server.md'), - '/src/posts/2018-03-13-pages-fresh-from-the-book.md': () => - import('/src/posts/2018-03-13-pages-fresh-from-the-book.md'), - '/src/posts/2018-03-19-ebpp-my-epic-birding-prediction-project-in-r.md': () => - import('/src/posts/2018-03-19-ebpp-my-epic-birding-prediction-project-in-r.md'), - '/src/posts/2018-04-03-using-esri-arcgis-arcmap-on-mac-osx-2-methods.md': () => - import('/src/posts/2018-04-03-using-esri-arcgis-arcmap-on-mac-osx-2-methods.md'), - '/src/posts/2018-04-05-using-esri-arcgis-arcmap-in-the-aws-cloud.md': () => - import('/src/posts/2018-04-05-using-esri-arcgis-arcmap-in-the-aws-cloud.md'), - '/src/posts/2018-04-10-intro-to-the-aws-cloud-9-ide.md': () => - import('/src/posts/2018-04-10-intro-to-the-aws-cloud-9-ide.md'), - '/src/posts/2018-04-18-signs-of-thoughts-of-possible-spring.md': () => - import('/src/posts/2018-04-18-signs-of-thoughts-of-possible-spring.md'), - '/src/posts/2018-05-23-birding-at-plumb-island.md': () => import('/src/posts/2018-05-23-birding-at-plumb-island.md'), - '/src/posts/2018-05-24-840-watts-of-solar-power.md': () => - import('/src/posts/2018-05-24-840-watts-of-solar-power.md'), - '/src/posts/2018-05-28-gathering-point-data-using-compass-55-on-apple-ios.md': () => - import('/src/posts/2018-05-28-gathering-point-data-using-compass-55-on-apple-ios.md'), - '/src/posts/2018-05-31-research-year-two-three-photos.md': () => - import('/src/posts/2018-05-31-research-year-two-three-photos.md'), - '/src/posts/2018-05-31-solar-upgrades.md': () => import('/src/posts/2018-05-31-solar-upgrades.md'), - '/src/posts/2018-06-10-how-to-query-kml-point-data-as-csv-using-qgis-and-r.md': () => - import('/src/posts/2018-06-10-how-to-query-kml-point-data-as-csv-using-qgis-and-r.md'), - '/src/posts/2018-06-24-how-to-generate-convex-hull-polygons-and-centroids-from-kml.md': () => - import('/src/posts/2018-06-24-how-to-generate-convex-hull-polygons-and-centroids-from-kml.md'), - '/src/posts/2018-06-25-off-grid-file-sharing-with-samba-gl-inet.md': () => - import('/src/posts/2018-06-25-off-grid-file-sharing-with-samba-gl-inet.md'), - '/src/posts/2018-07-16-deploy-a-shiny-web-app-in-r-using-aws-ec2-red-hat.md': () => - import('/src/posts/2018-07-16-deploy-a-shiny-web-app-in-r-using-aws-ec2-red-hat.md'), - '/src/posts/2018-07-23-gdal-for-r-server-on-ubuntu-kml-spatial-libraries-and-more.md': () => - import('/src/posts/2018-07-23-gdal-for-r-server-on-ubuntu-kml-spatial-libraries-and-more.md'), - '/src/posts/2018-07-27-new-app-kml-search-and-convert.md': () => - import('/src/posts/2018-07-27-new-app-kml-search-and-convert.md'), - '/src/posts/2018-07-28-shiny-app-specific-repo-live.md': () => - import('/src/posts/2018-07-28-shiny-app-specific-repo-live.md'), - '/src/posts/2018-09-01-early-hardware-and-maker-projects.md': () => - import('/src/posts/2018-09-01-early-hardware-and-maker-projects.md'), - '/src/posts/2018-09-12-github-update-9-12-18-shiny-apps.md': () => - import('/src/posts/2018-09-12-github-update-9-12-18-shiny-apps.md'), - '/src/posts/2018-09-12-quick-fix-254-character-limit-in-esri-story-map.md': () => - import('/src/posts/2018-09-12-quick-fix-254-character-limit-in-esri-story-map.md'), - '/src/posts/2018-11-05-recycled-personal-cloud-computing-under-nat.md': () => - import('/src/posts/2018-11-05-recycled-personal-cloud-computing-under-nat.md'), - '/src/posts/2018-11-25-deploy-shiny-r-apps-along-node-js.md': () => - import('/src/posts/2018-11-25-deploy-shiny-r-apps-along-node-js.md'), - '/src/posts/2019-02-20-installing-chapel-language-on-mac-and-linux.md': () => - import('/src/posts/2019-02-20-installing-chapel-language-on-mac-and-linux.md'), - '/src/posts/2019-03-10-querying-kml-data-with-qgis-and-r.md': () => - import('/src/posts/2019-03-10-querying-kml-data-with-qgis-and-r.md'), - '/src/posts/2019-03-21-notes-on-a-free-and-open-source-notes-app-joplin.md': () => - import('/src/posts/2019-03-21-notes-on-a-free-and-open-source-notes-app-joplin.md'), - '/src/posts/2019-04-15-open-source-collaboration-and-higher-education.md': () => - import('/src/posts/2019-04-15-open-source-collaboration-and-higher-education.md'), - '/src/posts/2019-04-18-warbler-trillers-of-the-charles.md': () => - import('/src/posts/2019-04-18-warbler-trillers-of-the-charles.md'), - '/src/posts/2019-05-01-succession-warblers-and-gis.md': () => - import('/src/posts/2019-05-01-succession-warblers-and-gis.md'), - '/src/posts/2019-07-05-summer-2019-update.md': () => import('/src/posts/2019-07-05-summer-2019-update.md'), - '/src/posts/2019-08-04-persistent-live-ubuntu-for-college.md': () => - import('/src/posts/2019-08-04-persistent-live-ubuntu-for-college.md'), - '/src/posts/2019-09-30-decentralized-pi-video-monitoring-w-motioneye-batman.md': () => - import('/src/posts/2019-09-30-decentralized-pi-video-monitoring-w-motioneye-batman.md'), - '/src/posts/2019-10-07-gdal-for-gis-on-unix-using-a-mac-or-better-linux.md': () => - import('/src/posts/2019-10-07-gdal-for-gis-on-unix-using-a-mac-or-better-linux.md'), - '/src/posts/2019-10-30-qemu-for-raspian-arm.md': () => import('/src/posts/2019-10-30-qemu-for-raspian-arm.md'), - '/src/posts/2020-03-17-when-it-must-be-windows.md': () => import('/src/posts/2020-03-17-when-it-must-be-windows.md'), - '/src/posts/2020-03-23-1607.md': () => import('/src/posts/2020-03-23-1607.md'), - '/src/posts/2020-04-02-ppe-me.md': () => import('/src/posts/2020-04-02-ppe-me.md'), - '/src/posts/2020-04-05-dm-shields-fusion-360.md': () => import('/src/posts/2020-04-05-dm-shields-fusion-360.md'), - '/src/posts/2020-04-09-convert-heic-png.md': () => import('/src/posts/2020-04-09-convert-heic-png.md'), - '/src/posts/2020-04-25-install-adobe-applications-on-aws-workspaces.md': () => - import('/src/posts/2020-04-25-install-adobe-applications-on-aws-workspaces.md'), - '/src/posts/2020-04-27-oes.md': () => import('/src/posts/2020-04-27-oes.md'), - '/src/posts/2020-05-18-while-at-a-safe-distance.md': () => - import('/src/posts/2020-05-18-while-at-a-safe-distance.md'), - '/src/posts/2020-06-04-prius-printers.md': () => import('/src/posts/2020-06-04-prius-printers.md'), - '/src/posts/2020-06-14-the-ebird-api-regioncode.md': () => - import('/src/posts/2020-06-14-the-ebird-api-regioncode.md'), - '/src/posts/2020-06-24-1768.md': () => import('/src/posts/2020-06-24-1768.md'), - '/src/posts/2020-06-24-1820.md': () => import('/src/posts/2020-06-24-1820.md'), - '/src/posts/2020-06-29-osselc-monday.md': () => import('/src/posts/2020-06-29-osselc-monday.md'), - '/src/posts/2020-07-11-1784.md': () => import('/src/posts/2020-07-11-1784.md'), - '/src/posts/2020-07-15-this-that-7-15.md': () => import('/src/posts/2020-07-15-this-that-7-15.md'), - '/src/posts/2020-07-23-ever-tried-to-chrome-remote-ubuntu-xd.md': () => - import('/src/posts/2020-07-23-ever-tried-to-chrome-remote-ubuntu-xd.md'), - '/src/posts/2020-08-23-purple-prius-parts.md': () => import('/src/posts/2020-08-23-purple-prius-parts.md'), - '/src/posts/2020-09-05-kvm-does-fruit-xcode-from-qemu.md': () => - import('/src/posts/2020-09-05-kvm-does-fruit-xcode-from-qemu.md'), - '/src/posts/2020-09-06-1984.md': () => import('/src/posts/2020-09-06-1984.md'), - '/src/posts/2020-10-28-asynchronous-http-methods-from-typescript.md': () => - import('/src/posts/2020-10-28-asynchronous-http-methods-from-typescript.md'), - '/src/posts/2020-10-28-evening-metal-9-14-20.md': () => import('/src/posts/2020-10-28-evening-metal-9-14-20.md'), - '/src/posts/2020-12-23-a-boilerplate-for-flask-react-typescript-mongodb.md': () => - import('/src/posts/2020-12-23-a-boilerplate-for-flask-react-typescript-mongodb.md'), - '/src/posts/2021-01-02-naive-shenanigans-measuring-distance-to-roi-w-opencv.md': () => - import('/src/posts/2021-01-02-naive-shenanigans-measuring-distance-to-roi-w-opencv.md'), - '/src/posts/2021-01-23-chindogu-ascii-art-i-suppose.md': () => - import('/src/posts/2021-01-23-chindogu-ascii-art-i-suppose.md'), - '/src/posts/2021-02-17-tmpui-the-merlin-sound-id-project.md': () => - import('/src/posts/2021-02-17-tmpui-the-merlin-sound-id-project.md'), - '/src/posts/2021-03-18-bits-bobs-mushstools-toadrooms.md': () => - import('/src/posts/2021-03-18-bits-bobs-mushstools-toadrooms.md'), - '/src/posts/2021-04-23-morning-metal-4-22-21.md': () => import('/src/posts/2021-04-23-morning-metal-4-22-21.md'), - '/src/posts/2021-11-30-2393.md': () => import('/src/posts/2021-11-30-2393.md'), - '/src/posts/2022-01-01-datasets-plots-graphs-charts.md': () => - import('/src/posts/2022-01-01-datasets-plots-graphs-charts.md'), - '/src/posts/2022-02-06-makerspace-financial-reporting-w-ipython.md': () => - import('/src/posts/2022-02-06-makerspace-financial-reporting-w-ipython.md'), - '/src/posts/2022-05-15-running-cornells-dla-makerspace.md': () => - import('/src/posts/2022-05-15-running-cornells-dla-makerspace.md'), - '/src/posts/2022-05-23-this-and-that.md': () => import('/src/posts/2022-05-23-this-and-that.md'), - '/src/posts/2023-03-01-fusion-360-for-3d-printing-w-jess.md': () => - import('/src/posts/2023-03-01-fusion-360-for-3d-printing-w-jess.md'), - '/src/posts/2023-11-27-turkey-probe.md': () => import('/src/posts/2023-11-27-turkey-probe.md'), - '/src/posts/2023-12-18-lets-write-a-simple-efficient-and-fast-flask-based-image-server-in-an-afternoon.md': () => - import('/src/posts/2023-12-18-lets-write-a-simple-efficient-and-fast-flask-based-image-server-in-an-afternoon.md'), - '/src/posts/2024-01-02-accuwix.md': () => import('/src/posts/2024-01-02-accuwix.md'), - '/src/posts/2024-02-22-i-wrote-a-mutual-aid-mental-health-service.md': () => - import('/src/posts/2024-02-22-i-wrote-a-mutual-aid-mental-health-service.md'), - '/src/posts/2024-05-22-what-have-i-been-up-to-these-last-few-months.md': () => - import('/src/posts/2024-05-22-what-have-i-been-up-to-these-last-few-months.md'), - '/src/posts/2024-12-31-ligature-test-fixture.md': () => import('/src/posts/2024-12-31-ligature-test-fixture.md'), - '/src/posts/2026-02-26-aperture-tagged-devices-and-the-tsnet-escape-hatch.md': () => - import('/src/posts/2026-02-26-aperture-tagged-devices-and-the-tsnet-escape-hatch.md'), - '/src/posts/2026-03-04-from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life.md': () => - import('/src/posts/2026-03-04-from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life.md'), - '/src/posts/2026-03-04-week-notes-cyborgs-servers-and-sending.md': () => - import('/src/posts/2026-03-04-week-notes-cyborgs-servers-and-sending.md'), - '/src/posts/2026-03-04-xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives.md': () => - import('/src/posts/2026-03-04-xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives.md'), - '/src/posts/2026-03-12-updated-resume-and-cv.md': () => import('/src/posts/2026-03-12-updated-resume-and-cv.md'), - '/src/posts/2026-03-13-the-winrm-forkbomb-how-ansible-molecule-locks-you-out-of-active-directory.md': () => - import('/src/posts/2026-03-13-the-winrm-forkbomb-how-ansible-molecule-locks-you-out-of-active-directory.md'), - '/src/posts/2026-03-21-week-notes-indeterminism-passkeys-and-spring.md': () => - import('/src/posts/2026-03-21-week-notes-indeterminism-passkeys-and-spring.md'), - '/src/posts/2026-04-25-reproducible-host-probes-nix-chapel-dhall.md': () => - import('/src/posts/2026-04-25-reproducible-host-probes-nix-chapel-dhall.md'), - '/src/posts/2026-04-28-darwin-nic-docs-are-live.md': () => - import('/src/posts/2026-04-28-darwin-nic-docs-are-live.md'), - '/src/posts/2026-04-30-small-zig-libraries-for-apple-shaped-escape-hatches.md': () => - import('/src/posts/2026-04-30-small-zig-libraries-for-apple-shaped-escape-hatches.md'), - '/src/posts/2026-05-03-was110-fidium-gonetspeed-omci-personalities.md': () => - import('/src/posts/2026-05-03-was110-fidium-gonetspeed-omci-personalities.md'), - '/src/posts/2026-05-11-trashmonitor-tailnet-window-into-the-hardware-pile.md': () => - import('/src/posts/2026-05-11-trashmonitor-tailnet-window-into-the-hardware-pile.md'), - '/src/posts/2026-05-29-gingersnap-cookies.md': () => import('/src/posts/2026-05-29-gingersnap-cookies.md'), - '/src/posts/2026-06-08-glue-you-can-see-in-uv.md': () => import('/src/posts/2026-06-08-glue-you-can-see-in-uv.md'), - '/src/posts/2026-06-08-week-notes-scroll-wheels-and-chasing-the-sun.md': () => - import('/src/posts/2026-06-08-week-notes-scroll-wheels-and-chasing-the-sun.md'), - '/src/posts/2026-06-09-clearing-canon-5b00-lock-native-key-free-megatank-reset.md': () => - import('/src/posts/2026-06-09-clearing-canon-5b00-lock-native-key-free-megatank-reset.md'), -}; +export const publishedPostLoaders = import.meta.glob([ + '/src/posts/2016-12-13-example-photo-work.md', + '/src/posts/2016-12-13-required-custom-tools.md', + '/src/posts/2016-12-13-the-audeasy-build.md', + '/src/posts/2016-12-13-waxwing-audio.md', + '/src/posts/2016-12-14-opportunistic-birding-14122016.md', + '/src/posts/2016-12-15-beta-how-to-build-the-phone-charger.md', + '/src/posts/2016-12-15-building-electrostatic-earspeakers.md', + '/src/posts/2016-12-15-diy-electrostatic-amp-options.md', + '/src/posts/2016-12-15-music-sketches-1-playing-with-rhythm-division-and-jazzy-chords.md', + '/src/posts/2016-12-16-music-sketches-2-rethinking-funtwos-guitar-tone-discipline.md', + '/src/posts/2016-12-21-develop-limitless-habitual-soithen_soicould_-ethos.md', + '/src/posts/2017-02-20-wolf-pine-fox-park-1-bonus-winter-birds.md', + '/src/posts/2017-03-02-wolf-pine-fox-park-2-22717.md', + '/src/posts/2017-03-04-wolf-pine-fox-park-3-3317-noon-ish-time.md', + '/src/posts/2017-03-05-wolf-pine-fox-park-4-3517-noon-ish-time.md', + '/src/posts/2017-03-11-wolf-pine-fox-park-6-31017-theoretical-owls-and-real-tree-bark.md', + '/src/posts/2017-03-12-wolf-pine-fox-park-7-31217-surprise-its-cold.md', + '/src/posts/2017-03-13-sit-spot-n-1-the-gho-talk.md', + '/src/posts/2017-03-17-wolf-pine-fox-park-8-31617-someone-bumped-the-snow-machine.md', + '/src/posts/2017-03-20-boutique-everything-when-the-hobby-grows-up.md', + '/src/posts/2017-03-28-wolf-pine-fox-park-9-32617-everyone-is-on-vacation.md', + '/src/posts/2017-03-30-beaver-dam-quincy-bog-1-33017-the-world-has-gone-mad.md', + '/src/posts/2017-03-31-parking-lotsit-spot-fox-park-10-33117-whoos-clues.md', + '/src/posts/2017-04-02-wolf-pine-fox-park-10-4117-it-is-a-snow-show-debunking-the-melanistic-dogamount.md', + '/src/posts/2017-04-03-parking-lotsit-spot-fox-park-12-4317-owling-2-hours-before-sunrise.md', + '/src/posts/2017-04-04-603.md', + '/src/posts/2017-04-08-pt-1-reflecting-on-stunt-culture-theories-and-frisbee.md', + '/src/posts/2017-04-08-pt-2-reflecting-on-stunt-culture-scooters.md', + '/src/posts/2017-04-11-bird-observations-today-langdon-woods.md', + '/src/posts/2017-04-12-fox-park-14-41017-1-if-by-land-16-if-by-ear.md', + '/src/posts/2017-04-12-fox-park-15-41217-1-if-by-land-17-if-by-ear.md', + '/src/posts/2017-04-17-fox-park-16-and-17-41617big-toad-small-vireo-yay.md', + '/src/posts/2017-04-19-fox-park-18-41917-in-class-roamings.md', + '/src/posts/2017-04-20-addendum-to-secret-beach-area-natural-history-class-walk.md', + '/src/posts/2017-05-02-langdon-woods-with-kurt-plants.md', + '/src/posts/2017-05-03-the-big-len-list.md', + '/src/posts/2017-05-04-early-morning-guitar-session-ala-laundry.md', + '/src/posts/2017-05-04-fox-park-to-langdon-morning-birds.md', + '/src/posts/2017-05-05-new-photopromotional-media-business-pages-in-the-works.md', + '/src/posts/2017-05-07-hunting-for-trees-birds.md', + '/src/posts/2017-05-07-mega-langdon.md', + '/src/posts/2017-05-08-new-business-site-up-and-running.md', + '/src/posts/2017-05-08-pre-dawn-fox-park-lot-walk-birding-by-ear.md', + '/src/posts/2017-05-12-wolf-pine-fox-park-silence.md', + '/src/posts/2017-05-17-rugby-morning-2.md', + '/src/posts/2017-05-18-rugby-morning-3-6.md', + '/src/posts/2017-07-03-birding-beyond-binos-bird-apps-and-the-guide.md', + '/src/posts/2017-07-25-mpcnc-it-moves.md', + '/src/posts/2017-07-27-gallery-of-my-proper-photos-nh-warbler-research.md', + '/src/posts/2017-07-27-gallery-of-warblers-in-the-hand.md', + '/src/posts/2018-03-08-how-to-make-a-aws-r-server.md', + '/src/posts/2018-03-13-pages-fresh-from-the-book.md', + '/src/posts/2018-03-19-ebpp-my-epic-birding-prediction-project-in-r.md', + '/src/posts/2018-04-03-using-esri-arcgis-arcmap-on-mac-osx-2-methods.md', + '/src/posts/2018-04-05-using-esri-arcgis-arcmap-in-the-aws-cloud.md', + '/src/posts/2018-04-10-intro-to-the-aws-cloud-9-ide.md', + '/src/posts/2018-04-18-signs-of-thoughts-of-possible-spring.md', + '/src/posts/2018-05-23-birding-at-plumb-island.md', + '/src/posts/2018-05-24-840-watts-of-solar-power.md', + '/src/posts/2018-05-28-gathering-point-data-using-compass-55-on-apple-ios.md', + '/src/posts/2018-05-31-research-year-two-three-photos.md', + '/src/posts/2018-05-31-solar-upgrades.md', + '/src/posts/2018-06-10-how-to-query-kml-point-data-as-csv-using-qgis-and-r.md', + '/src/posts/2018-06-24-how-to-generate-convex-hull-polygons-and-centroids-from-kml.md', + '/src/posts/2018-06-25-off-grid-file-sharing-with-samba-gl-inet.md', + '/src/posts/2018-07-16-deploy-a-shiny-web-app-in-r-using-aws-ec2-red-hat.md', + '/src/posts/2018-07-23-gdal-for-r-server-on-ubuntu-kml-spatial-libraries-and-more.md', + '/src/posts/2018-07-27-new-app-kml-search-and-convert.md', + '/src/posts/2018-07-28-shiny-app-specific-repo-live.md', + '/src/posts/2018-09-01-early-hardware-and-maker-projects.md', + '/src/posts/2018-09-12-github-update-9-12-18-shiny-apps.md', + '/src/posts/2018-09-12-quick-fix-254-character-limit-in-esri-story-map.md', + '/src/posts/2018-11-05-recycled-personal-cloud-computing-under-nat.md', + '/src/posts/2018-11-25-deploy-shiny-r-apps-along-node-js.md', + '/src/posts/2019-02-20-installing-chapel-language-on-mac-and-linux.md', + '/src/posts/2019-03-10-querying-kml-data-with-qgis-and-r.md', + '/src/posts/2019-03-21-notes-on-a-free-and-open-source-notes-app-joplin.md', + '/src/posts/2019-04-15-open-source-collaboration-and-higher-education.md', + '/src/posts/2019-04-18-warbler-trillers-of-the-charles.md', + '/src/posts/2019-05-01-succession-warblers-and-gis.md', + '/src/posts/2019-07-05-summer-2019-update.md', + '/src/posts/2019-08-04-persistent-live-ubuntu-for-college.md', + '/src/posts/2019-09-30-decentralized-pi-video-monitoring-w-motioneye-batman.md', + '/src/posts/2019-10-07-gdal-for-gis-on-unix-using-a-mac-or-better-linux.md', + '/src/posts/2019-10-30-qemu-for-raspian-arm.md', + '/src/posts/2020-03-17-when-it-must-be-windows.md', + '/src/posts/2020-03-23-1607.md', + '/src/posts/2020-04-02-ppe-me.md', + '/src/posts/2020-04-05-dm-shields-fusion-360.md', + '/src/posts/2020-04-09-convert-heic-png.md', + '/src/posts/2020-04-25-install-adobe-applications-on-aws-workspaces.md', + '/src/posts/2020-04-27-oes.md', + '/src/posts/2020-05-18-while-at-a-safe-distance.md', + '/src/posts/2020-06-04-prius-printers.md', + '/src/posts/2020-06-14-the-ebird-api-regioncode.md', + '/src/posts/2020-06-24-1768.md', + '/src/posts/2020-06-24-1820.md', + '/src/posts/2020-06-29-osselc-monday.md', + '/src/posts/2020-07-11-1784.md', + '/src/posts/2020-07-15-this-that-7-15.md', + '/src/posts/2020-07-23-ever-tried-to-chrome-remote-ubuntu-xd.md', + '/src/posts/2020-08-23-purple-prius-parts.md', + '/src/posts/2020-09-05-kvm-does-fruit-xcode-from-qemu.md', + '/src/posts/2020-09-06-1984.md', + '/src/posts/2020-10-28-asynchronous-http-methods-from-typescript.md', + '/src/posts/2020-10-28-evening-metal-9-14-20.md', + '/src/posts/2020-12-23-a-boilerplate-for-flask-react-typescript-mongodb.md', + '/src/posts/2021-01-02-naive-shenanigans-measuring-distance-to-roi-w-opencv.md', + '/src/posts/2021-01-23-chindogu-ascii-art-i-suppose.md', + '/src/posts/2021-02-17-tmpui-the-merlin-sound-id-project.md', + '/src/posts/2021-03-18-bits-bobs-mushstools-toadrooms.md', + '/src/posts/2021-04-23-morning-metal-4-22-21.md', + '/src/posts/2021-11-30-2393.md', + '/src/posts/2022-01-01-datasets-plots-graphs-charts.md', + '/src/posts/2022-02-06-makerspace-financial-reporting-w-ipython.md', + '/src/posts/2022-05-15-running-cornells-dla-makerspace.md', + '/src/posts/2022-05-23-this-and-that.md', + '/src/posts/2023-03-01-fusion-360-for-3d-printing-w-jess.md', + '/src/posts/2023-11-27-turkey-probe.md', + '/src/posts/2023-12-18-lets-write-a-simple-efficient-and-fast-flask-based-image-server-in-an-afternoon.md', + '/src/posts/2024-01-02-accuwix.md', + '/src/posts/2024-02-22-i-wrote-a-mutual-aid-mental-health-service.md', + '/src/posts/2024-05-22-what-have-i-been-up-to-these-last-few-months.md', + '/src/posts/2024-12-31-ligature-test-fixture.md', + '/src/posts/2026-02-26-aperture-tagged-devices-and-the-tsnet-escape-hatch.md', + '/src/posts/2026-03-04-from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life.md', + '/src/posts/2026-03-04-week-notes-cyborgs-servers-and-sending.md', + '/src/posts/2026-03-04-xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives.md', + '/src/posts/2026-03-12-updated-resume-and-cv.md', + '/src/posts/2026-03-13-the-winrm-forkbomb-how-ansible-molecule-locks-you-out-of-active-directory.md', + '/src/posts/2026-03-21-week-notes-indeterminism-passkeys-and-spring.md', + '/src/posts/2026-04-25-reproducible-host-probes-nix-chapel-dhall.md', + '/src/posts/2026-04-28-darwin-nic-docs-are-live.md', + '/src/posts/2026-04-30-small-zig-libraries-for-apple-shaped-escape-hatches.md', + '/src/posts/2026-05-03-was110-fidium-gonetspeed-omci-personalities.md', + '/src/posts/2026-05-11-trashmonitor-tailnet-window-into-the-hardware-pile.md', + '/src/posts/2026-05-29-gingersnap-cookies.md', + '/src/posts/2026-06-08-glue-you-can-see-in-uv.md', + '/src/posts/2026-06-08-week-notes-scroll-wheels-and-chasing-the-sun.md', + '/src/posts/2026-06-09-clearing-canon-5b00-lock-native-key-free-megatank-reset.md', +]); From 563efd9e06fbed9f6d51e54d6c8ccb364facc0ad Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 17 Jul 2026 00:19:12 -0400 Subject: [PATCH 5/6] fix(blog): reject glob syntax in post paths Keep the generated published-module list literal and fail closed if a future filename could broaden Vite glob matching. Refs TIN-2979 --- scripts/generate-search-index.mts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/generate-search-index.mts b/scripts/generate-search-index.mts index 1a98ca2..1a8d447 100644 --- a/scripts/generate-search-index.mts +++ b/scripts/generate-search-index.mts @@ -87,6 +87,10 @@ async function main(): Promise { index.sort((a, b) => b.date.localeCompare(a.date)); publicationHolds.sort((a, b) => a.localeCompare(b)); publishedSourceFiles.sort((a, b) => a.localeCompare(b)); + const unsafeGlobPath = publishedSourceFiles.find((sourceFile) => /[!*?\[\]{}\\]/.test(sourceFile)); + if (unsafeGlobPath) { + throw new Error(`Published post path contains glob syntax and cannot be emitted safely: ${unsafeGlobPath}`); + } const publishedPostLoadersSource = [ '// Generated by scripts/generate-search-index.mts. Do not edit by hand.', @@ -102,9 +106,6 @@ async function main(): Promise { trailingComma: 'all', useTabs: true, }); - if (publishedPostLoaders.includes("'/src/posts/*.md'")) { - throw new Error('Generated post loaders must enumerate published modules; broad globs can leak held posts'); - } await Promise.all([ writeFile(OUTPUT, JSON.stringify(index), 'utf-8'), From f93c5a70885c43f76483c7a0c6f2860b56f1f089 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 17 Jul 2026 00:26:54 -0400 Subject: [PATCH 6/6] fix(blog): constrain generated loader paths --- scripts/generate-search-index.mts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/generate-search-index.mts b/scripts/generate-search-index.mts index 1a8d447..50fae1e 100644 --- a/scripts/generate-search-index.mts +++ b/scripts/generate-search-index.mts @@ -87,9 +87,11 @@ async function main(): Promise { index.sort((a, b) => b.date.localeCompare(a.date)); publicationHolds.sort((a, b) => a.localeCompare(b)); publishedSourceFiles.sort((a, b) => a.localeCompare(b)); - const unsafeGlobPath = publishedSourceFiles.find((sourceFile) => /[!*?\[\]{}\\]/.test(sourceFile)); - if (unsafeGlobPath) { - throw new Error(`Published post path contains glob syntax and cannot be emitted safely: ${unsafeGlobPath}`); + const unsafeLoaderPath = publishedSourceFiles.find( + (sourceFile) => !/^\/src\/posts\/[A-Za-z0-9._-]+\.md$/.test(sourceFile), + ); + if (unsafeLoaderPath) { + throw new Error(`Published post path cannot be emitted as an exact loader: ${unsafeLoaderPath}`); } const publishedPostLoadersSource = [