Skip to content

Commit 5f55520

Browse files
just-be-devclaude
andcommitted
fix(micro): address code review issues
- Validate cursor param in micro.astro; redirect to /micro on NaN input - Add id attribute to article elements for anchor URL support - Guard JSON.parse of syndicated_to with try/catch and type validation - Validate D1 API response structure before accessing result rows - Fix middleware to handle hash fragments in redirect targets without URL-encoding the '#' via URL.pathname setter - Create /micro/[id] route that redirects to the correct paginated page with anchor, so RSS links resolve even for non-first-page posts - Restore rss.xml.ts link to /micro/${post.id} path style - Remove unused skippedCount variable in gen-url-manifest.ts - Add 'M' kind coverage to code.test.ts (fromDate, getKind, getCollection) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 43fe1d0 commit 5f55520

6 files changed

Lines changed: 84 additions & 9 deletions

File tree

scripts/gen-url-manifest.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ async function processMicroCollection(
8282
const results = data.result[0]?.results || [];
8383

8484
let processedCount = 0;
85-
let skippedCount = 0;
8685
let errorCount = 0;
8786

8887
for (const post of results) {
@@ -145,7 +144,7 @@ async function processMicroCollection(
145144
console.log();
146145
return {
147146
processed: processedCount,
148-
skipped: skippedCount,
147+
skipped: 0,
149148
errors: errorCount,
150149
};
151150
} catch (error) {

src/loaders/micro-loader.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,42 @@ export function microLoader(options: MicroLoaderOptions): Loader {
5656
throw new Error(`D1 query failed: ${JSON.stringify(data.errors)}`);
5757
}
5858

59-
const results = data.result[0]?.results || [];
59+
if (!Array.isArray(data.result) || data.result.length === 0) {
60+
logger.warn("D1 query returned unexpected result structure");
61+
return;
62+
}
63+
64+
const resultRows = data.result[0]?.results;
65+
if (!Array.isArray(resultRows)) {
66+
logger.warn("D1 query results is not an array");
67+
return;
68+
}
69+
70+
const results = resultRows as MicroPost[];
6071

6172
// Store each post in the collection
62-
for (const post of results as MicroPost[]) {
73+
for (const post of results) {
74+
let syndicatedTo: string[] = [];
75+
if (post.syndicated_to) {
76+
try {
77+
const parsed = JSON.parse(post.syndicated_to);
78+
if (Array.isArray(parsed) && parsed.every((s) => typeof s === "string")) {
79+
syndicatedTo = parsed;
80+
} else {
81+
logger.warn(`Invalid syndicated_to value for post ${post.id}, defaulting to []`);
82+
}
83+
} catch {
84+
logger.warn(`Failed to parse syndicated_to for post ${post.id}, defaulting to []`);
85+
}
86+
}
87+
6388
store.set({
6489
id: String(post.id),
6590
data: {
6691
content: post.content,
6792
createdAt: new Date(post.created_at * 1000),
6893
updatedAt: new Date(post.updated_at * 1000),
69-
syndicatedTo: post.syndicated_to ? JSON.parse(post.syndicated_to) : [],
94+
syndicatedTo,
7095
},
7196
});
7297
}

src/middleware.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,17 @@ export const onRequest = defineMiddleware(async (context, next) => {
4242
const matchingEntry = entries.find((entry) => entry.data.code === code);
4343

4444
if (matchingEntry) {
45-
// The manifest ID is the canonical URL path (e.g., "/blog/slug")
45+
// The manifest ID is the canonical URL path (e.g., "/blog/slug" or "/micro#123")
46+
// Handle hash fragments explicitly — setting URL.pathname encodes '#' as '%23'
47+
const targetPath = matchingEntry.id;
4648
const newUrl = new URL(url);
47-
newUrl.pathname = matchingEntry.id;
49+
const hashIdx = targetPath.indexOf("#");
50+
if (hashIdx !== -1) {
51+
newUrl.pathname = targetPath.slice(0, hashIdx);
52+
newUrl.hash = targetPath.slice(hashIdx);
53+
} else {
54+
newUrl.pathname = targetPath;
55+
}
4856
return context.redirect(newUrl.toString(), 301);
4957
}
5058
}

src/pages/micro.astro

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const PAGE_SIZE = 20;
99
const cursorParam = Astro.url.searchParams.get("cursor");
1010
const direction = Astro.url.searchParams.get("dir") || "next";
1111
12+
// Validate cursor before using it
13+
if (cursorParam !== null && !Number.isFinite(Number(cursorParam))) {
14+
return Astro.redirect("/micro", 302);
15+
}
16+
1217
// Get micro posts from D1 via content collection
1318
const allPosts = (await getCollection("micro")).sort(
1419
(a, b) => b.data.createdAt.valueOf() - a.data.createdAt.valueOf()
@@ -28,7 +33,7 @@ if (!cursorParam) {
2833
nextCursor = posts[posts.length - 1].data.createdAt.valueOf();
2934
}
3035
} else {
31-
const cursor = parseInt(cursorParam);
36+
const cursor = Number(cursorParam);
3237
3338
if (direction === "prev") {
3439
// Previous page - get posts newer than cursor
@@ -96,7 +101,7 @@ function formatDate(date: Date): string {
96101
<div class="space-y-2">
97102
{
98103
posts.map((post) => (
99-
<article class="border-fg-2 border p-2">
104+
<article id={post.id} class="border-fg-2 border p-2">
100105
<p class="whitespace-pre-wrap break-words">{post.data.content}</p>
101106
<time class="text-fg-2 mt-1 block text-sm" datetime={post.data.createdAt.toISOString()}>
102107
{formatDate(post.data.createdAt)}

src/pages/micro/[id].astro

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
import { getCollection } from "astro:content";
3+
4+
export const prerender = false;
5+
6+
const PAGE_SIZE = 20;
7+
const { id } = Astro.params;
8+
9+
const allPosts = (await getCollection("micro")).sort(
10+
(a, b) => b.data.createdAt.valueOf() - a.data.createdAt.valueOf()
11+
);
12+
13+
const idx = allPosts.findIndex((p) => p.id === id);
14+
15+
if (idx === -1) {
16+
return new Response(null, { status: 404 });
17+
}
18+
19+
const pageNum = Math.floor(idx / PAGE_SIZE);
20+
21+
if (pageNum === 0) {
22+
// Post is on the first page — no cursor needed
23+
return Astro.redirect(`/micro#${id}`, 302);
24+
} else {
25+
// Cursor is the createdAt of the last post on the preceding page,
26+
// which is the boundary that makes dir=next load this page.
27+
const cursorPost = allPosts[pageNum * PAGE_SIZE - 1];
28+
const cursor = cursorPost.data.createdAt.valueOf();
29+
return Astro.redirect(`/micro?cursor=${cursor}&dir=next#${id}`, 302);
30+
}
31+
---

src/utils/code.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe("Code.fromDate()", () => {
3535
expect(Code.fromDate(date, "R").toString()).toBe("R2392");
3636
expect(Code.fromDate(date, "P").toString()).toBe("P2392");
3737
expect(Code.fromDate(date, "T").toString()).toBe("T2392");
38+
expect(Code.fromDate(date, "M").toString()).toBe("M2392");
3839
});
3940
});
4041

@@ -113,6 +114,7 @@ describe("Code.fromCode()", () => {
113114
expect(Code.fromCode("R2391").getKind()).toBe("R");
114115
expect(Code.fromCode("P2391").getKind()).toBe("P");
115116
expect(Code.fromCode("T2391").getKind()).toBe("T");
117+
expect(Code.fromCode("M2391").getKind()).toBe("M");
116118
});
117119
});
118120

@@ -180,6 +182,7 @@ describe("getters", () => {
180182
expect(Code.fromCode("R2392").getKind()).toBe("R");
181183
expect(Code.fromCode("P2392").getKind()).toBe("P");
182184
expect(Code.fromCode("T2392").getKind()).toBe("T");
185+
expect(Code.fromCode("M2392").getKind()).toBe("M");
183186
});
184187

185188
it("should return date code via getDateCode()", () => {
@@ -242,4 +245,8 @@ describe("Code.getCollection()", () => {
242245
it("should return 'talks' for kind T", () => {
243246
expect(Code.fromCode("T2392").getCollection()).toBe("talks");
244247
});
248+
249+
it("should return 'micro' for kind M", () => {
250+
expect(Code.fromCode("M2392").getCollection()).toBe("micro");
251+
});
245252
});

0 commit comments

Comments
 (0)