Validate and canonicalize generated toolkit routes#1065
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Use one safe slug contract across route discovery, integration cards, and the sitemap so invalid or duplicate entries cannot publish broken links. Co-authored-by: Cursor <cursoragent@cursor.com>
8fb9dfe to
35ecefc
Compare
Keep a malformed index entry from changing the source of the entire route set, and prevent redirecting fallback-category URLs from entering the sitemap. Co-authored-by: Cursor <cursoragent@cursor.com>
| const isRecord = (value: unknown): value is Record<string, unknown> => | ||
| typeof value === "object" && value !== null; | ||
|
|
||
| const isValidToolkitData = (parsed: unknown): parsed is ToolkitData => | ||
| typeof parsed === "object" && | ||
| parsed !== null && | ||
| isRecord(parsed) && | ||
| "id" in parsed && | ||
| ("label" in parsed || "name" in parsed) && | ||
| "metadata" in parsed && | ||
| typeof (parsed as Record<string, unknown>).metadata === "object" && | ||
| (parsed as Record<string, unknown>).metadata !== null; | ||
| isRecord(parsed.metadata); | ||
|
|
||
| const isToolkitIndexEntry = (value: unknown): value is ToolkitIndexEntry => { | ||
| if (!isRecord(value)) { | ||
| return false; | ||
| } | ||
|
|
||
| return ( | ||
| typeof value.id === "string" && | ||
| typeof value.label === "string" && | ||
| typeof value.version === "string" && | ||
| typeof value.category === "string" && | ||
| (value.type === undefined || typeof value.type === "string") && | ||
| typeof value.toolCount === "number" && | ||
| Number.isInteger(value.toolCount) && | ||
| value.toolCount >= 0 && | ||
| typeof value.authType === "string" | ||
| ); | ||
| }; | ||
|
|
||
| const isToolkitIndexEnvelope = ( | ||
| value: unknown | ||
| ): value is ToolkitIndexEnvelope => | ||
| isRecord(value) && | ||
| typeof value.generatedAt === "string" && | ||
| typeof value.version === "string" && | ||
| Array.isArray(value.toolkits); |
There was a problem hiding this comment.
cc @teallarson would these be better off as zod schemas?
There was a problem hiding this comment.
100%
The generator already has a schema... we can't import that here because of module resolution BUT we can define one in app/_lib/toolkit-data.ts.
Something like:
// isToolkitIndexEntry becomes
const ToolkitIndexEntrySchema = z.object({
id: z.string(),
label: z.string(),
version: z.string(),
category: z.string(),
type: z.string().optional(),
toolCount: z.number().int().nonnegative(),
authType: z.string(),
});
...
// isToolkitIndexEnvelope becomes
const ToolkitIndexEnvelopeSchema = z.object({
generatedAt: z.string(),
version: z.string(),
toolkits: z.array(z.unknown()),
});
...
// isValidToolkitData becomes:
const ToolkitDataSchema = z
.object({
id: z.string(),
label: z.string().optional(),
name: z.string().optional(),
metadata: z.object({}).passthrough(),
})
.passthrough()
.refine((v) => v.label !== undefined || v.name !== undefined);
//etc.
cc: @jottakka !
There was a problem hiding this comment.
Implemented the app-local Zod schemas as sketched (envelope + per-entry filter). See also the later note about how defensive this is — open to simplifying if you’d prefer slug-skip only.
fix: keep generated sidebar links aligned with toolkit routes Reuse the canonical toolkit slug and category validation when rendering sidebar metadata, including safely escaped labels and keys. Co-authored-by: Cursor <cursoragent@cursor.com>
Use app-local Zod schemas for toolkit data/index parsing, align the sitemap test with the others-category exclusion, and reuse the shared normalizeCategory helper in sidebar sync. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the Zod / sitemap / shared- Remaining card↔route category contract gaps (raw Re-requesting review for the Zod change on this PR. |
There was a problem hiding this comment.
When a toolkit can’t produce a valid URL, this code throws an error in one place and catches it in another. That works, but it’s harder to follow than needed. Simpler approach: if we can’t build a URL, just skip that toolkit and move on — no throw/catch needed.
There was a problem hiding this comment.
Also: Sidebar and canonical URLs now sanitize categories (so bad values like ../../outside get mapped to others), but integration card links don’t seem to do that yet. Should those use the same category cleanup so cards and pages always match?
There was a problem hiding this comment.
Done — toIntegrationLink now returns null for bad slugs, and resolveIndexToolkits just skips those entries (no throw/catch).
There was a problem hiding this comment.
Agreed — cards now use the same client-safe normalizeCategory helper as routes/sidebar (app/_lib/toolkit-category.ts), so unknown/empty categories map to others consistently.
| if (!slug) { | ||
| throw new Error(`Cannot build a canonical path for toolkit: ${toolkit.id}`); | ||
| } |
There was a problem hiding this comment.
similar to the comment above: invalid slugs throw errors that get caught elsewhere. Would be cleaner if “bad slug” just meant “don’t include this page” everywhere, consistently.
There was a problem hiding this comment.
Done — getToolkitCanonicalPath now returns null instead of throwing; callers skip / omit canonical when there is no safe slug.
There was a problem hiding this comment.
This adds a fair amount of validation code. Is that solving a real problem we’ve hit (bad rows in index.json), or is it defensive?
If we haven’t seen bad data in practice, we might get most of the benefit from just skipping toolkits with invalid slugs.
There was a problem hiding this comment.
Mostly defensive + consistency with the earlier request to use Zod here (generator already validates with Zod; we can’t import those schemas into app/_lib).
Practical benefit we kept: envelope validates, then filters bad index rows instead of dropping the whole catalog. Happy to slim this further to “skip invalid slugs only” if you’d rather — the slug skip path is now consistent everywhere either way.
Extract a client-safe toolkit-category helper so integration index links and catalog enrichment use the same category allow-list as static routes, instead of raw catalog categories that can miss pages or 404. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep catalog enrichment aligned with route generation: toolkit JSON wins for docsLink, and category values are normalized before cards/filters use them. Co-authored-by: Cursor <cursoragent@cursor.com>
Return null instead of throw/catch for invalid integration/canonical URLs, reuse listValidIntegrationLinks in the sitemap (excluding others), fold in client-safe category normalization for cards, and trim overlapping sitemap and sidebar tests. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed Teal’s latest review comments in the newest commits:
#1088 can be closed as superseded. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit baee0a0. Configure here.
| ...toolkit, | ||
| category, | ||
| ...(docsLink ? { docsLink } : {}), | ||
| }; |
There was a problem hiding this comment.
Empty docsLink fails to override
Medium Severity
Explicit empty docsLink values from toolkit JSON are not correctly applied in getToolkitsWithDocsLinks. The docsLink ? { docsLink } : {} spread condition treats empty strings as falsy, allowing existing design-system docsLinks to persist. This can cause card slugs to diverge from generated routes, which correctly interpret an empty docsLink as a fallback to the ID slug.
Reviewed by Cursor Bugbot for commit baee0a0. Configure here.
normalizeCategory can return docs-only "others", which is not in the design-system ToolkitCategory union — widen the docs toolkit type and filter helper so the Next build typecheck passes. Co-authored-by: Cursor <cursoragent@cursor.com>
Only drop a bare name when another catalog toolkit owns the -api URL; otherwise remap the card onto that page and store the resolved link on the index entry so the client does not recompute a dead bare href. Co-authored-by: Cursor <cursoragent@cursor.com>


Summary
Test plan
pnpm testNote
Medium Risk
Touches integration routing, index link resolution, and sitemap/canonical SEO across many entry points; behavior is defensive and heavily tested but mis-slugging could hide or mis-link catalog entries.
Overview
Unifies how integration categories, slugs, and card URLs are derived so the catalog, static routes, sidebar, sitemap, and canonical metadata stay in sync.
Slug safety:
getToolkitSlugnow rejects unsafe path segments and returnsnullwhen no valid slug exists; route listing, index resolution, canonical paths, andllms.txtskip those entries instead of emitting bad URLs.Shared contract:
normalizeCategoryand the category allow-list move to a client-safetoolkit-categorymodule; catalog enrichment prefers toolkit JSON over design-system metadata fordocsLinkandcategory(matching route helpers).Integrations index:
resolveIndexToolkitsexposes a finallink(including lone bare-name →-apiremaps when no sibling owns the page), drops unsluggable catalog rows, and cards use thatlinkrather than recomputing on the client.Data loading: Toolkit JSON and
index.jsonare validated with Zod; malformed index rows are filtered without dropping the whole index.SEO / discovery: The sitemap merges authored MDX URLs with
listValidIntegrationLinks, dedupes by URL, and omitsothersintegration paths that redirect; toolkit pages only setalternates.canonicalwhen a canonical path exists.Generator: Sidebar sync reuses shared slug/category helpers and safer JSON escaping for generated
_meta.tsxfiles.Reviewed by Cursor Bugbot for commit bac2dce. Bugbot is set up for automated code reviews on this repo. Configure here.