Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
78 changes: 78 additions & 0 deletions src/__tests__/content-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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);
});
});
88 changes: 53 additions & 35 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<PatternDifficulty, string> = {
Expand All @@ -59,7 +64,7 @@ const DIFFICULTY_LABEL: Record<PatternDifficulty, string> = {

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 = [
Expand Down Expand Up @@ -137,16 +142,16 @@ export default function Home() {
</div>
</div>

{recentlyAdded.length > 0 && (
{recentlyEdited.length > 0 && (
<div style={{ marginBottom: 40 }}>
<h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12, color: "var(--text-secondary)", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Recently Added
Recently Edited
</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 8 }}>
{recentlyAdded.map((fn) => (
{recentlyEdited.map((item) => (
<Link
key={fn.slug}
href={`/functions/${fn.slug}`}
key={`${item.type}-${item.slug}`}
href={`/${item.type}s/${item.slug}`}
style={{
display: "block",
background: "var(--bg-secondary)",
Expand All @@ -156,11 +161,24 @@ export default function Home() {
textDecoration: "none",
}}
>
<div style={{ fontSize: 13, fontWeight: 600, color: "var(--accent)", marginBottom: 2, fontFamily: "var(--font-mono)" }}>
{fn.title}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 2 }}>
<span style={{
fontSize: 13,
fontWeight: 600,
color: "var(--accent)",
fontFamily: item.type === "function" ? "var(--font-mono)" : undefined,
}}>
{item.title}
</span>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", color: "var(--text-muted)", flexShrink: 0, marginLeft: 8 }}>
{item.type}
</span>
</div>
<div style={{ fontSize: 12, color: "var(--text-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{fn.description}
{item.contentDescription}
</div>
<div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4, opacity: 0.7 }}>
{item.date}
</div>
</Link>
))}
Expand Down
32 changes: 32 additions & 0 deletions src/data/recently-edited.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
33 changes: 21 additions & 12 deletions src/lib/mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { frontmatter: FunctionFrontmatter; content: string }>();

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[] {
Expand Down Expand Up @@ -57,14 +60,17 @@ export function buildSearchIndex(): FunctionIndexEntry[] {

// Concept utilities

const conceptCache = new Map<string, { frontmatter: ConceptFrontmatter; content: string }>();

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[] {
Expand All @@ -87,14 +93,17 @@ export function getAllConcepts(): (ConceptFrontmatter & { slug: string })[] {

const PATTERNS_DIR = path.join(process.cwd(), "src/content/patterns");

const patternCache = new Map<string, { frontmatter: PatternFrontmatter; content: string }>();

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[] {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,10 @@ export interface PatternIndexEntry {
description: string;
difficulty: PatternDifficulty;
}

export interface RecentlyEditedEntry {
slug: string;
type: SearchResultType;
date: string;
description: string;
}
Loading