diff --git a/src/middleware.ts b/src/middleware.ts index b652986..7092a80 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,13 +1,41 @@ import { defineMiddleware } from "astro:middleware"; import { getCollection } from "astro:content"; -import { Code } from "@/utils/code"; +import { Code, KIND_TO_COLLECTION, type Kind, type Collection } from "@/utils/code"; export const onRequest = defineMiddleware(async (context, next) => { const url = new URL(context.request.url); + // Handle short code URLs like /b232e -> /blog/b232e/slug/ + const kindChars = Object.keys(KIND_TO_COLLECTION).join("").toLowerCase(); + const shortCodePattern = new RegExp(`^\\/([${kindChars}][0-9a-f]{4})\\/?$`, "i"); + const shortCodeMatch = url.pathname.match(shortCodePattern); + if (shortCodeMatch) { + const shortCode = shortCodeMatch[1].toLowerCase(); + const kindChar = shortCode[0].toUpperCase() as Kind; + const collection = KIND_TO_COLLECTION[kindChar]; + + const entry = ( + await getCollection(collection, ({ id }) => { + return shortCode === id.slice(0, 5).toLowerCase(); + }) + ).at(0); + + if (entry) { + const { code, slug } = Code.parseId(entry.id); + const newUrl = new URL(url); + newUrl.pathname = `/${collection}/${code}/${slug}/`; + return context.redirect(newUrl.toString(), 301); + } + + // No match, continue to 404 + return next(); + } + // Handle /blog/*, /projects/*, /research/*, and /talks/* paths - const match = url.pathname.match(/^\/(blog|projects|research|talks)\//); - const matchedType = match?.[1] as "blog" | "projects" | "research" | "talks" | undefined; + const collections = Object.values(KIND_TO_COLLECTION).join("|"); + const collectionPattern = new RegExp(`^\\/(${collections})\\/`); + const match = url.pathname.match(collectionPattern); + const matchedType = match?.[1] as Collection | undefined; if (!matchedType) { return next(); diff --git a/src/utils/code.ts b/src/utils/code.ts index f667cd7..a168114 100644 --- a/src/utils/code.ts +++ b/src/utils/code.ts @@ -2,7 +2,15 @@ const EPOCH = Date.UTC(2000, 0, 1); // January 1, 2000 UTC const BASE16_CHARS = "0123456789ABCDEF" as const; export const VALID_KINDS = ["B", "R", "P", "T"] as const; -export type Kind = typeof VALID_KINDS[number]; +export type Kind = (typeof VALID_KINDS)[number]; + +export const KIND_TO_COLLECTION = { + B: "blog", + R: "research", + P: "projects", + T: "talks", +} as const; +export type Collection = (typeof KIND_TO_COLLECTION)[Kind]; function isValidKind(char: string): char is Kind { return VALID_KINDS.includes(char as Kind); @@ -155,6 +163,13 @@ export class Code { return this.toString(); } + /** + * Get the collection name for this code's kind + */ + getCollection(): Collection { + return KIND_TO_COLLECTION[this.kind]; + } + /** * Helper to generate getStaticPaths for Astro routes * @param collection - The collection to get entries from @@ -275,7 +290,7 @@ if (import.meta.vitest) { }); it("should reject codes with invalid kind", () => { - expect(() => Code.fromCode("X2391")).toThrow("Invalid kind 'X'. Must be one of: B, R, P"); + expect(() => Code.fromCode("X2391")).toThrow(`Invalid kind 'X'. Must be one of: ${VALID_KINDS.join(", ")}`); }); it("should reject codes with non-hex characters in date portion", () => { @@ -400,4 +415,22 @@ if (import.meta.vitest) { expect(url).toBe("/research/r24e5/parsing-techniques/"); }); }); + + describe("Code.getCollection()", () => { + it("should return 'blog' for kind B", () => { + expect(Code.fromCode("B2392").getCollection()).toBe("blog"); + }); + + it("should return 'research' for kind R", () => { + expect(Code.fromCode("R2392").getCollection()).toBe("research"); + }); + + it("should return 'projects' for kind P", () => { + expect(Code.fromCode("P2392").getCollection()).toBe("projects"); + }); + + it("should return 'talks' for kind T", () => { + expect(Code.fromCode("T2392").getCollection()).toBe("talks"); + }); + }); }