From af2df8d72fb95c4fd9a862cca93202edc2da5a36 Mon Sep 17 00:00:00 2001 From: kyleamueller <82664864+kyleamueller@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:26:30 -0600 Subject: [PATCH 1/2] perf: cache MDX file reads to reduce redundant I/O MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add module-level Map caches for getFunctionBySlug, getConceptBySlug, and getPatternBySlug so each file is read and parsed only once. Reduces test suite time by ~35% (mdx.test 941ms→613ms, content-integrity 1171ms→764ms). Co-Authored-By: Claude Opus 4.6 --- src/lib/mdx.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/lib/mdx.ts b/src/lib/mdx.ts index 04b122e..9c1915e 100644 --- a/src/lib/mdx.ts +++ b/src/lib/mdx.ts @@ -7,14 +7,17 @@ import { functionSynonyms } from "@/data/search-synonyms"; const CONTENT_DIR = path.join(process.cwd(), "src/content/functions"); const CONCEPTS_DIR = path.join(process.cwd(), "src/content/concepts"); +const functionCache = new Map(); + export function getFunctionBySlug(slug: string) { + const cached = functionCache.get(slug); + if (cached) return cached; const filePath = path.join(CONTENT_DIR, `${slug}.mdx`); const raw = fs.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n"); const { data, content } = matter(raw); - return { - frontmatter: data as FunctionFrontmatter, - content, - }; + const result = { frontmatter: data as FunctionFrontmatter, content }; + functionCache.set(slug, result); + return result; } export function getAllFunctionSlugs(): string[] { @@ -57,14 +60,17 @@ export function buildSearchIndex(): FunctionIndexEntry[] { // Concept utilities +const conceptCache = new Map(); + export function getConceptBySlug(slug: string) { + const cached = conceptCache.get(slug); + if (cached) return cached; const filePath = path.join(CONCEPTS_DIR, `${slug}.mdx`); const raw = fs.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n"); const { data, content } = matter(raw); - return { - frontmatter: data as ConceptFrontmatter, - content, - }; + const result = { frontmatter: data as ConceptFrontmatter, content }; + conceptCache.set(slug, result); + return result; } export function getAllConceptSlugs(): string[] { @@ -87,14 +93,17 @@ export function getAllConcepts(): (ConceptFrontmatter & { slug: string })[] { const PATTERNS_DIR = path.join(process.cwd(), "src/content/patterns"); +const patternCache = new Map(); + export function getPatternBySlug(slug: string) { + const cached = patternCache.get(slug); + if (cached) return cached; const filePath = path.join(PATTERNS_DIR, `${slug}.mdx`); const raw = fs.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n"); const { data, content } = matter(raw); - return { - frontmatter: data as PatternFrontmatter, - content, - }; + const result = { frontmatter: data as PatternFrontmatter, content }; + patternCache.set(slug, result); + return result; } export function getAllPatternSlugs(): string[] { From 37853def888bd3c0da4768e061785aaab1a9694e Mon Sep 17 00:00:00 2001 From: kyleamueller <82664864+kyleamueller@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:42:15 -0600 Subject: [PATCH 2/2] feat: replace "Recently Added" with "Recently Edited" on home page Replace the git-dependent getRecentlyAdded() with a static JSON data file (src/data/recently-edited.json) that supports all content types (functions, concepts, patterns). Add 6 content-integrity tests including a 45-day freshness gate that fails the pre-push hook if the list goes stale. Document maintenance workflow in CLAUDE.md. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 8 +++ src/__tests__/content-integrity.test.ts | 78 ++++++++++++++++++++++ src/app/page.tsx | 88 +++++++++++++++---------- src/data/recently-edited.json | 32 +++++++++ src/lib/types.ts | 7 ++ 5 files changed, 178 insertions(+), 35 deletions(-) create mode 100644 src/data/recently-edited.json diff --git a/CLAUDE.md b/CLAUDE.md index 2f50752..1dec6cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,3 +24,11 @@ ## Windows gotcha MDX files on Windows have CRLF line endings. All file readers in `src/lib/mdx.ts` normalize with `.replace(/\r\n/g, "\n")` — do not remove this. + +## Recently Edited section + +The home page "Recently Edited" section is driven by `src/data/recently-edited.json`. +When editing content files, update this JSON: +1. Add a new entry at the top with the slug, type, today's date, and a short change description. +2. Keep the array at 6 entries max — remove the oldest if needed. +3. A content-integrity test enforces that the most recent entry is within the last 45 days. diff --git a/src/__tests__/content-integrity.test.ts b/src/__tests__/content-integrity.test.ts index 2097235..fce78c5 100644 --- a/src/__tests__/content-integrity.test.ts +++ b/src/__tests__/content-integrity.test.ts @@ -8,6 +8,8 @@ import { getPatternBySlug, } from "@/lib/mdx"; import officialSpecSlugs from "./fixtures/official-spec-slugs.json"; +import recentlyEditedData from "@/data/recently-edited.json"; +import type { RecentlyEditedEntry } from "@/lib/types"; // All 661 official spec slugs are now documented — relatedFunctions must // reference existing documented slugs only (no forward-reference fallback). @@ -162,3 +164,79 @@ describe("Pattern MDX integrity", () => { } }); }); + +// --------------------------------------------------------------------------- +// Recently Edited +// --------------------------------------------------------------------------- + +describe("Recently Edited data integrity", () => { + const entries = recentlyEditedData as RecentlyEditedEntry[]; + + it("has between 1 and 6 entries", () => { + expect(entries.length).toBeGreaterThanOrEqual(1); + expect(entries.length).toBeLessThanOrEqual(6); + }); + + it("every entry has valid schema fields", () => { + const validTypes = ["function", "concept", "pattern"]; + for (const entry of entries) { + expect(typeof entry.slug, `slug type for ${entry.slug}`).toBe("string"); + expect(entry.slug.length, `slug empty`).toBeGreaterThan(0); + expect(validTypes, `invalid type "${entry.type}"`).toContain(entry.type); + expect(typeof entry.date, `date type for ${entry.slug}`).toBe("string"); + expect( + /^\d{4}-\d{2}-\d{2}$/.test(entry.date), + `invalid date format "${entry.date}" for ${entry.slug}` + ).toBe(true); + expect( + isNaN(Date.parse(entry.date)), + `unparseable date "${entry.date}" for ${entry.slug}` + ).toBe(false); + expect(typeof entry.description, `description type for ${entry.slug}`).toBe("string"); + expect(entry.description.length, `empty description for ${entry.slug}`).toBeGreaterThan(0); + } + }); + + it("every entry slug references an existing content file", () => { + const functionSlugs = new Set(getAllFunctionSlugs()); + const conceptSlugs = new Set(getAllConceptSlugs()); + const patternSlugs = new Set(getAllPatternSlugs()); + for (const entry of entries) { + switch (entry.type) { + case "function": + expect(functionSlugs.has(entry.slug), `function slug "${entry.slug}" does not exist`).toBe(true); + break; + case "concept": + expect(conceptSlugs.has(entry.slug), `concept slug "${entry.slug}" does not exist`).toBe(true); + break; + case "pattern": + expect(patternSlugs.has(entry.slug), `pattern slug "${entry.slug}" does not exist`).toBe(true); + break; + } + } + }); + + it("has no duplicate slug+type combinations", () => { + const keys = entries.map((e) => `${e.type}/${e.slug}`); + expect(new Set(keys).size, "duplicate entries found").toBe(keys.length); + }); + + it("entries are sorted newest-first", () => { + for (let i = 1; i < entries.length; i++) { + expect( + entries[i - 1].date >= entries[i].date, + `entries[${i - 1}] (${entries[i - 1].date}) should be >= entries[${i}] (${entries[i].date})` + ).toBe(true); + } + }); + + it("most recent entry is within the last 45 days", () => { + const now = new Date(); + const newest = new Date(entries[0].date); + const diffDays = (now.getTime() - newest.getTime()) / (1000 * 60 * 60 * 24); + expect( + diffDays, + `most recent entry is ${Math.round(diffDays)} days old — update recently-edited.json` + ).toBeLessThanOrEqual(45); + }); +}); diff --git a/src/app/page.tsx b/src/app/page.tsx index fa2959d..f5e91d5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,9 @@ import Link from "next/link"; -import path from "path"; -import { execSync } from "child_process"; import { categories } from "@/data/categories"; -import { getAllFunctions, getFunctionBySlug, getAllPatterns } from "@/lib/mdx"; +import recentlyEditedData from "@/data/recently-edited.json"; +import { getAllFunctions, getFunctionBySlug, getConceptBySlug, getPatternBySlug, getAllPatterns } from "@/lib/mdx"; import { PatternDifficulty } from "@/lib/types"; +import type { RecentlyEditedEntry } from "@/lib/types"; import type { Metadata } from "next"; export const metadata: Metadata = { @@ -19,30 +19,35 @@ export const metadata: Metadata = { }, }; -function getRecentlyAdded(limit = 6) { - try { - const output = execSync( - "git log --format=\"\" --diff-filter=A --name-only -- src/content/functions/", - { cwd: process.cwd() } - ).toString().trim(); - - const slugs = output - .split("\n") - .filter((f) => f.endsWith(".mdx")) - .slice(0, limit) - .map((f) => path.basename(f, ".mdx")); - - return slugs.flatMap((slug) => { - try { - const { frontmatter } = getFunctionBySlug(slug); - return [{ slug, title: frontmatter.title, category: frontmatter.category, description: frontmatter.description }]; - } catch { - return []; +function getRecentlyEdited() { + return (recentlyEditedData as RecentlyEditedEntry[]).flatMap((entry) => { + try { + let title: string, contentDesc: string; + switch (entry.type) { + case "function": { + const { frontmatter } = getFunctionBySlug(entry.slug); + title = frontmatter.title; + contentDesc = frontmatter.description; + break; + } + case "concept": { + const { frontmatter } = getConceptBySlug(entry.slug); + title = frontmatter.title; + contentDesc = frontmatter.description; + break; + } + case "pattern": { + const { frontmatter } = getPatternBySlug(entry.slug); + title = frontmatter.title; + contentDesc = frontmatter.description; + break; + } } - }); - } catch { - return []; - } + return [{ ...entry, title, contentDescription: contentDesc }]; + } catch { + return []; + } + }); } const DIFFICULTY_COLOR: Record = { @@ -59,7 +64,7 @@ const DIFFICULTY_LABEL: Record = { export default function Home() { const allFunctions = getAllFunctions(); - const recentlyAdded = getRecentlyAdded(); + const recentlyEdited = getRecentlyEdited(); const allPatterns = getAllPatterns(); // Show beginner patterns first, then fill with intermediate up to 4 total const featuredPatterns = [ @@ -137,16 +142,16 @@ export default function Home() { - {recentlyAdded.length > 0 && ( + {recentlyEdited.length > 0 && (

- Recently Added + Recently Edited

- {recentlyAdded.map((fn) => ( + {recentlyEdited.map((item) => ( -
- {fn.title} +
+ + {item.title} + + + {item.type} +
- {fn.description} + {item.contentDescription} +
+
+ {item.date}
))} diff --git a/src/data/recently-edited.json b/src/data/recently-edited.json new file mode 100644 index 0000000..52707ec --- /dev/null +++ b/src/data/recently-edited.json @@ -0,0 +1,32 @@ +[ + { + "slug": "web-contents", + "type": "function", + "date": "2026-04-07", + "description": "Added RelativePath, ManualStatusHandling options and 3 real examples" + }, + { + "slug": "api-authentication", + "type": "pattern", + "date": "2026-04-07", + "description": "Added 6 new Web.Contents options, RelativePath/ManualStatusHandling sections" + }, + { + "slug": "power-query-environments", + "type": "concept", + "date": "2026-04-03", + "description": "April 2026 freshness review updates" + }, + { + "slug": "m-paradigm", + "type": "concept", + "date": "2026-03-11", + "description": "Overhauled for UI-to-M beginner learning path" + }, + { + "slug": "table-join", + "type": "function", + "date": "2026-03-08", + "description": "Corrected examples and added output comments" + } +] diff --git a/src/lib/types.ts b/src/lib/types.ts index 7edbdc2..8d6a3fa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -120,3 +120,10 @@ export interface PatternIndexEntry { description: string; difficulty: PatternDifficulty; } + +export interface RecentlyEditedEntry { + slug: string; + type: SearchResultType; + date: string; + description: string; +}