Fix author archive parity and pagination#738
Conversation
WalkthroughIntroduces author archive pagination functionality with a new migration script to map WordPress exports to local posts, updated author-post mappings, pagination library utilities, a PaginationNav component, and paginated author archive pages. Additionally refactors existing author pages to use the new pagination infrastructure and adds route canonicalization. Changes
Sequence DiagramsequenceDiagram
participant Build as Build System
participant ArchiveLib as Archive Library
participant PostsCol as Posts Collection
participant AuthData as author-post-paths.json
participant AuthPage as Author Page Template
participant PaginNav as PaginationNav Component
Build->>ArchiveLib: getAuthorArchiveEntries()
ArchiveLib->>PostsCol: getCollection('posts')
PostsCol-->>ArchiveLib: published posts
ArchiveLib->>AuthData: load author-to-paths mapping
AuthData-->>ArchiveLib: author slug → [post paths]
ArchiveLib->>ArchiveLib: normalize routes, match posts to authors
ArchiveLib-->>Build: AuthorArchiveEntry[] (with totalPages)
Build->>Build: for each entry: getStaticPaths()
Build->>ArchiveLib: paginateAuthorPosts(posts, page)
ArchiveLib-->>Build: sliced posts for page
Build->>AuthPage: pass slug, profile, posts, currentPage, totalPages
AuthPage->>AuthPage: render author hero + post grid
AuthPage->>PaginNav: pass basePath, currentPage, totalPages
PaginNav->>PaginNav: buildItems() - compute page links & ellipsis
PaginNav-->>AuthPage: navigation UI
AuthPage-->>Build: static HTML page
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~40 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying blog-hichee-com with
|
| Latest commit: |
9d8f796
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://46d992cf.blog-hichee-com-git.pages.dev |
| Branch Preview URL: | https://fix-author-archives.blog-hichee-com-git.pages.dev |
Greptile SummaryThis PR adds static paginated author archive routes ( Confidence Score: 5/5Safe to merge; all findings are P2 quality-and-reusability suggestions with no blocking correctness issues. The pagination logic, alias resolution, and data regeneration are all well-structured and author-validated against live WordPress output. The three open comments are non-blocking P2 items that do not affect current production correctness. src/lib/authorArchives.ts — review the alias+canonical deduplication assumption; src/data/author-post-paths.json — the presence of both /where-am-i-24-2/ and /where-am-i-24/ should be intentional. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[author-post-paths.json\nroutes per author slug] -->|imported by| B[authorArchives.ts\ngetAuthorArchiveEntries]
B -->|getCollection posts| C[Astro content layer]
B -->|resolveArchivePost\nalias expansion| D[AuthorArchivePost list]
D -->|slice page 1| E["[slug].astro\n/author/alan/"]
D -->|paginateAuthorPosts\npages 2..N| F["[page].astro\n/author/alan/page/7/"]
E --> G[PaginationNav\nbasePath + totalPages]
F --> G
H[build-author-post-paths.mjs\nyarn migrate:authors:map] -->|reads WXR + local posts| A
I["[...slug].astro\npost detail"] -->|inferAuthorSlugFromPath\n+ canonicalizeAliasPath| J[Author link\n/author/tiffany/]
|
| .replace(/\uFFFC/g, '') | ||
| .trim(); | ||
|
|
||
| if (!raw) return ''; | ||
|
|
||
| const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`; | ||
| return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; | ||
| } | ||
|
|
||
| function toTimestamp(value) { | ||
| if (value instanceof Date) return value.getTime(); | ||
| const parsed = Date.parse(String(value || '')); | ||
| return Number.isFinite(parsed) ? parsed : 0; |
There was a problem hiding this comment.
Entity ordering can cause double-decode for
&
& is the numeric character reference for &, identical to &. Replacing it first converts & → & → &, which double-decodes a literal & entity. WordPress WXR files can contain doubly-escaped content (e.g. creator display names with ampersands). Merging both patterns into one avoids this:
.replace(/&|&/g, '&')|
|
||
| return Object.entries(authorPostPaths).map(([slug, paths]) => { | ||
| const authorPosts = (paths as string[]) | ||
| .map((entryPath) => resolveArchivePost(entryPath, postsByPath)) | ||
| .filter((post): post is AuthorArchivePost => Boolean(post)) | ||
| .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); |
There was a problem hiding this comment.
No deduplication after alias expansion can yield duplicate post cards
author-post-paths.json stores both /where-am-i-24-2/ and /where-am-i-24/ for the same author. resolveArchivePost resolves both to the same underlying post (with different synthetic IDs), and the filter+sort pipeline has no deduplication guard. If the WordPress parity count is intentional (two separate WP posts), a deduplication step by base post ID would make that assumption explicit and prevent future regressions.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (9)
src/pages/[...slug].astro (2)
10-17: Consider extracting alias canonicalization into a shared module to prevent drift.The same
/where-am-i-24-2/→/where-am-i-24/rule is duplicated insrc/lib/authorArchives.ts(asROUTE_ALIASES, applied as a post-lookup fallback insideresolveArchivePost), while here it is applied upfront insidenormalizeRoutePath. The two files will diverge as aliases grow, and they already use different application strategies. Extracting a single helper (e.g.,src/lib/routeCanonicalization.tsexportingcanonicalizeAliasPathand/or a sharednormalizeRoutePath) and consuming it from both files would keep the mapping authoritative in one place and make the alias-resolution behavior consistent across the catch-all page and archive resolution.Also, a
Mapmirrors thealiasPathToCanonicalPathMap already used ingetStaticPaths/pickRelatedPostsin this same file and scales better than aswitchonce more than one alias lands.♻️ Sketch of a shared helper
// src/lib/routeCanonicalization.ts const ROUTE_ALIASES = new Map<string, string>([ ['/where-am-i-24-2/', '/where-am-i-24/'], ]); export function canonicalizeAliasPath(route: string): string { return ROUTE_ALIASES.get(route) ?? route; } export function normalizeRoutePath(inputPath: string): string { const raw = String(inputPath || '').replace(/%ef%bf%bc/gi, '').replace(/\uFFFC/g, ''); if (!raw) return '/'; const withStart = raw.startsWith('/') ? raw : `/${raw}`; const normalized = withStart.endsWith('/') ? withStart : `${withStart}/`; return canonicalizeAliasPath(normalized); }Then import from both
src/pages/[...slug].astroandsrc/lib/authorArchives.ts(replacing the localROUTE_ALIASES+ fallback inresolveArchivePost).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/`[...slug].astro around lines 10 - 17, Extract the alias mapping and logic into a single shared helper module that exports canonicalizeAliasPath and normalizeRoutePath, replace the inline switch in canonicalizeAliasPath with a Map-backed ROUTE_ALIASES, and update callers (the current normalizeRoutePath usage in this file plus resolveArchivePost in authorArchives.ts which currently uses its own ROUTE_ALIASES fallback) to import and use the shared canonicalizeAliasPath/normalizeRoutePath so alias resolution is authoritative and consistent; ensure getStaticPaths and pickRelatedPosts continue to use the same Map (or import it) and that normalizeRoutePath still normalizes path formatting before applying canonicalizeAliasPath.
573-579: Downstream simplification opportunity (non-blocking).Now that
normalizeRoutePathcanonicalizes/where-am-i-24-2/upfront, the/where-am-i-24-2/entry in the localaliasPathToCanonicalPathinsidepickRelatedPosts(lines 603–617) becomes unreachable —normalizeRoutePath(relatedPath)at line 626 has already mapped it to/where-am-i-24/before the Map lookup at line 627. Similarly, the\uFFFCentries in that Map are unreachable because the regex at line 574 strips\uFFFCfromraw. Worth pruning in a follow-up pass so the alias surface is expressed in exactly one place. Not introduced by this PR, flagging because the new canonicalization makes the dead branches visible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/`[...slug].astro around lines 573 - 579, normalizeRoutePath already canonicalizes paths and strips \uFFFC, so redundant alias keys like "/where-am-i-24-2/" and any entries containing \uFFFC in the aliasPathToCanonicalPath Map inside pickRelatedPosts are unreachable; update pickRelatedPosts to either (a) remove those dead alias entries from the Map, or (b) build the Map using normalizeRoutePath on each alias key so only normalized keys are stored (ensuring lookup at normalizeRoutePath(relatedPath) matches), and remove any explicit "\uFFFC" variants to centralize canonicalization in normalizeRoutePath.src/components/PaginationNav.astro (2)
48-48: Hardcodedaria-label="Author pages"limits reusability.The component is written as a generic
PaginationNavbut the aria-label assumes the author-archive context. Consider accepting it as a prop (with a sensible default like"Pagination") so the component remains reusable if you later add paginated category/tag/search pages.♻️ Proposed change
interface Props { basePath: string; currentPage: number; totalPages: number; + label?: string; } -const { basePath, currentPage, totalPages } = Astro.props; +const { basePath, currentPage, totalPages, label = 'Pagination' } = Astro.props; ... - <nav class="pagination-nav" aria-label="Author pages"> + <nav class="pagination-nav" aria-label={label}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/PaginationNav.astro` at line 48, The nav element currently hardcodes aria-label="Author pages" which makes PaginationNav non-reusable; change the component to accept a prop (e.g., export let ariaLabel = "Pagination") in the frontmatter and replace the hardcoded string with the prop (aria-label={ariaLabel}); ensure the prop has a sensible default ("Pagination") so existing uses keep the same behavior unless they pass a custom label.
52-54: Add screen-reader context to Previous/Next links."Previous" and "Next" alone are ambiguous for assistive tech users when multiple nav controls are on the page. Consider adding
aria-labelindicating the target page number.♻️ Proposed change
- <a class="page-numbers page-numbers--nav" href={previousHref} rel="prev"> + <a + class="page-numbers page-numbers--nav" + href={previousHref} + rel="prev" + aria-label={`Previous page, page ${currentPage - 1}`} + > Previous </a> ... - <a class="page-numbers page-numbers--nav" href={nextHref} rel="next"> + <a + class="page-numbers page-numbers--nav" + href={nextHref} + rel="next" + aria-label={`Next page, page ${currentPage + 1}`} + > Next </a>Also applies to: 78-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/PaginationNav.astro` around lines 52 - 54, Pagination "Previous" and "Next" anchors in the PaginationNav component are ambiguous to screen readers; update the <a class="page-numbers page-numbers--nav" href={previousHref} rel="prev"> and the corresponding next link (href={nextHref}) to include an aria-label that indicates the target page (e.g., "Previous page, page X" or "Next page, page Y"). Compute or derive the page number from the href or from existing props (previousHref/nextHref or any previousPage/nextPage values passed into PaginationNav) and inject that value into the aria-label so assistive tech users get clear context; apply the same change to the next link block as noted in the review.src/lib/authorArchives.ts (2)
66-71: Avoid computingtitleCaseSlug(slug)twice in the fallback.Minor, but the fallback branch calls
titleCaseSlug(slug)for bothnameandbio.♻️ Proposed change
export function getAuthorProfile(slug: string): AuthorProfile { - return authorProfiles[slug] ?? { - name: titleCaseSlug(slug), - bio: `Posts written by ${titleCaseSlug(slug)}.` - }; + const existing = authorProfiles[slug]; + if (existing) return existing; + const name = titleCaseSlug(slug); + return { name, bio: `Posts written by ${name}.` }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/authorArchives.ts` around lines 66 - 71, In getAuthorProfile, avoid calling titleCaseSlug(slug) twice in the fallback object; compute it once into a local variable (e.g., const displayName = titleCaseSlug(slug)) and use displayName for both the name and inside the bio string, keeping the existing behavior but reducing redundant computation; update references in getAuthorProfile and ensure return still matches AuthorProfile shape and preserves authorProfiles[slug] when present.
16-21: ExportAuthorArchiveEntryfor downstream typing.
getAuthorArchiveEntries()is public and returnsPromise<AuthorArchiveEntry[]>, but the type itself is not exported, so callers (e.g., the two author pages) cannot name the return element type. Exporting it lets consumers replaceposts: any[]with precise typing.♻️ Proposed change
-type AuthorArchiveEntry = { +export type AuthorArchiveEntry = { slug: string; profile: AuthorProfile; posts: AuthorArchivePost[]; totalPages: number; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/authorArchives.ts` around lines 16 - 21, The type AuthorArchiveEntry is declared but not exported, preventing callers of getAuthorArchiveEntries from using it for typing; export the AuthorArchiveEntry type (e.g., add an export keyword to the type declaration for AuthorArchiveEntry) so downstream modules can import and use it to strongly type the return values of getAuthorArchiveEntries and replace posts: any[] with the precise AuthorArchiveEntry.posts type.scripts/build-author-post-paths.mjs (1)
110-113: SubtlerecordedRouteexpression — please add a clarifying comment.The ternary condition
preferred || fallback || !aliasResolved ? resolved.route : sourceRouteencodes the intent "prefer the local canonical path, except when we only matched via aROUTE_ALIASESentry — in which case persist the alias URL so the author archive lists/where-am-i-24-2/(as WordPress does)". That is non-obvious on first read and is the core behavior described in the PR. A short comment here will save future readers a diff spelunking.📝 Proposed comment
- const recordedRoute = - preferred || fallback || !aliasResolved ? resolved.route : sourceRoute; + // When we resolved the post only via a ROUTE_ALIASES entry (alias → canonical), + // record the alias URL (sourceRoute) so the author archive lists the same path + // WordPress shows. Otherwise use the local canonical route. + const recordedRoute = + preferred || fallback || !aliasResolved ? resolved.route : sourceRoute;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/build-author-post-paths.mjs` around lines 110 - 113, Add a short clarifying comment above the recordedRoute assignment explaining the intent: that the ternary (preferred || fallback || !aliasResolved ? resolved.route : sourceRoute) prefers the local canonical path (resolved.route) but, when the match came only from a ROUTE_ALIASES entry (aliasResolved is true and neither preferred nor fallback), it intentionally persists the alias URL (sourceRoute) so author archive listings show the aliased path (e.g., /where-am-i-24-2/) like WordPress; keep the assignment using recordedRoute and then call pathsBySlug.get(slug).set(recordedRoute, resolved.date) as before.src/pages/author/[slug].astro (1)
28-35: Sameany[]typing aspage/[page].astro.For consistency with the sister paginated route, please switch
posts: any[]toAuthorArchivePost[](exported fromsrc/lib/authorArchives). See the sibling page's comment for the proposed diff.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/author/`[slug].astro around lines 28 - 35, Update the posts prop type in the Astro props destructuring to use the exported AuthorArchivePost type instead of any[]: change the posts declaration in the const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.props as { ... } to use posts: AuthorArchivePost[] (import AuthorArchivePost from the module that exports it) so the author archive page uses the same AuthorArchivePost type as the sibling paginated route.src/pages/author/[slug]/page/[page].astro (1)
34-41: Use the sharedAuthorArchivePosttype instead ofany[].
src/lib/authorArchives.tsalready exportsAuthorArchivePost. Usingany[]here forgoes the type-safety guarantees that the helpers provide and makes downstream usage (e.g.,posts.map((post) => <PostCard post={post} />)) unchecked.♻️ Proposed change
-import { - buildAuthorPagePath, - getAuthorArchiveEntries, - paginateAuthorPosts, -} from '../../../../lib/authorArchives'; +import { + buildAuthorPagePath, + getAuthorArchiveEntries, + paginateAuthorPosts, + type AuthorArchivePost, +} from '../../../../lib/authorArchives'; ... -const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.props as { - slug: string; - profile: { name: string; bio: string }; - posts: any[]; - totalPosts: number; - currentPage: number; - totalPages: number; -}; +const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.props as { + slug: string; + profile: { name: string; bio: string }; + posts: AuthorArchivePost[]; + totalPosts: number; + currentPage: number; + totalPages: number; +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/author/`[slug]/page/[page].astro around lines 34 - 41, The props typing uses posts: any[] which loses type-safety; import the exported AuthorArchivePost type (named AuthorArchivePost) and change the destructured props signature to posts: AuthorArchivePost[] so downstream usage (e.g., posts.map and <PostCard post={post} />) is typed; add a top-level import type { AuthorArchivePost } from the module that exports it and update the Astro.props cast to use AuthorArchivePost[].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/build-author-post-paths.mjs`:
- Around line 141-149: extractCdata currently returns empty when a tag exists
without <![CDATA[]]>, causing fields like status/postType/dc:creator to be blank
and items filtered out; fix by adding a fallback to plain-tag extraction: where
extractCdata(...) is used (e.g., extracting wp:status, wp:post_type,
dc:creator), change calls to use extractCdata(source, tagName) ||
extractTag(source, tagName) (or modify extractCdata to internally try extractTag
on no-match) so values are captured whether wrapped in CDATA or not.
---
Nitpick comments:
In `@scripts/build-author-post-paths.mjs`:
- Around line 110-113: Add a short clarifying comment above the recordedRoute
assignment explaining the intent: that the ternary (preferred || fallback ||
!aliasResolved ? resolved.route : sourceRoute) prefers the local canonical path
(resolved.route) but, when the match came only from a ROUTE_ALIASES entry
(aliasResolved is true and neither preferred nor fallback), it intentionally
persists the alias URL (sourceRoute) so author archive listings show the aliased
path (e.g., /where-am-i-24-2/) like WordPress; keep the assignment using
recordedRoute and then call pathsBySlug.get(slug).set(recordedRoute,
resolved.date) as before.
In `@src/components/PaginationNav.astro`:
- Line 48: The nav element currently hardcodes aria-label="Author pages" which
makes PaginationNav non-reusable; change the component to accept a prop (e.g.,
export let ariaLabel = "Pagination") in the frontmatter and replace the
hardcoded string with the prop (aria-label={ariaLabel}); ensure the prop has a
sensible default ("Pagination") so existing uses keep the same behavior unless
they pass a custom label.
- Around line 52-54: Pagination "Previous" and "Next" anchors in the
PaginationNav component are ambiguous to screen readers; update the <a
class="page-numbers page-numbers--nav" href={previousHref} rel="prev"> and the
corresponding next link (href={nextHref}) to include an aria-label that
indicates the target page (e.g., "Previous page, page X" or "Next page, page
Y"). Compute or derive the page number from the href or from existing props
(previousHref/nextHref or any previousPage/nextPage values passed into
PaginationNav) and inject that value into the aria-label so assistive tech users
get clear context; apply the same change to the next link block as noted in the
review.
In `@src/lib/authorArchives.ts`:
- Around line 66-71: In getAuthorProfile, avoid calling titleCaseSlug(slug)
twice in the fallback object; compute it once into a local variable (e.g., const
displayName = titleCaseSlug(slug)) and use displayName for both the name and
inside the bio string, keeping the existing behavior but reducing redundant
computation; update references in getAuthorProfile and ensure return still
matches AuthorProfile shape and preserves authorProfiles[slug] when present.
- Around line 16-21: The type AuthorArchiveEntry is declared but not exported,
preventing callers of getAuthorArchiveEntries from using it for typing; export
the AuthorArchiveEntry type (e.g., add an export keyword to the type declaration
for AuthorArchiveEntry) so downstream modules can import and use it to strongly
type the return values of getAuthorArchiveEntries and replace posts: any[] with
the precise AuthorArchiveEntry.posts type.
In `@src/pages/`[...slug].astro:
- Around line 10-17: Extract the alias mapping and logic into a single shared
helper module that exports canonicalizeAliasPath and normalizeRoutePath, replace
the inline switch in canonicalizeAliasPath with a Map-backed ROUTE_ALIASES, and
update callers (the current normalizeRoutePath usage in this file plus
resolveArchivePost in authorArchives.ts which currently uses its own
ROUTE_ALIASES fallback) to import and use the shared
canonicalizeAliasPath/normalizeRoutePath so alias resolution is authoritative
and consistent; ensure getStaticPaths and pickRelatedPosts continue to use the
same Map (or import it) and that normalizeRoutePath still normalizes path
formatting before applying canonicalizeAliasPath.
- Around line 573-579: normalizeRoutePath already canonicalizes paths and strips
\uFFFC, so redundant alias keys like "/where-am-i-24-2/" and any entries
containing \uFFFC in the aliasPathToCanonicalPath Map inside pickRelatedPosts
are unreachable; update pickRelatedPosts to either (a) remove those dead alias
entries from the Map, or (b) build the Map using normalizeRoutePath on each
alias key so only normalized keys are stored (ensuring lookup at
normalizeRoutePath(relatedPath) matches), and remove any explicit "\uFFFC"
variants to centralize canonicalization in normalizeRoutePath.
In `@src/pages/author/`[slug].astro:
- Around line 28-35: Update the posts prop type in the Astro props destructuring
to use the exported AuthorArchivePost type instead of any[]: change the posts
declaration in the const { slug, profile, posts, totalPosts, currentPage,
totalPages } = Astro.props as { ... } to use posts: AuthorArchivePost[] (import
AuthorArchivePost from the module that exports it) so the author archive page
uses the same AuthorArchivePost type as the sibling paginated route.
In `@src/pages/author/`[slug]/page/[page].astro:
- Around line 34-41: The props typing uses posts: any[] which loses type-safety;
import the exported AuthorArchivePost type (named AuthorArchivePost) and change
the destructured props signature to posts: AuthorArchivePost[] so downstream
usage (e.g., posts.map and <PostCard post={post} />) is typed; add a top-level
import type { AuthorArchivePost } from the module that exports it and update the
Astro.props cast to use AuthorArchivePost[].
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5fc7c9be-e91a-43b5-9737-3092e611e362
📒 Files selected for processing (9)
package.jsonscripts/build-author-post-paths.mjssrc/components/PaginationNav.astrosrc/data/author-post-paths.jsonsrc/lib/authorArchives.tssrc/pages/[...slug].astrosrc/pages/author/[slug].astrosrc/pages/author/[slug]/page/[page].astrosrc/styles/global.css
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Route alias mapping duplicated across four locations
- I centralized all route aliases in
src/data/route-aliases.jsonand updated the archive library, slug page, and author-path build script to read from that shared source so alias behavior stays consistent.
- I centralized all route aliases in
Or push these changes by commenting:
@cursor push f2153aece0
Preview (f2153aece0)
diff --git a/scripts/build-author-post-paths.mjs b/scripts/build-author-post-paths.mjs
--- a/scripts/build-author-post-paths.mjs
+++ b/scripts/build-author-post-paths.mjs
@@ -3,6 +3,7 @@
import path from 'node:path';
import process from 'node:process';
import matter from 'gray-matter';
+import routeAliasesRaw from '../src/data/route-aliases.json' with { type: 'json' };
const args = parseArgs(process.argv.slice(2));
const outPath = path.resolve(args.out ?? 'src/data/author-post-paths.json');
@@ -16,9 +17,7 @@
'Our Travel Reporter': 'our-travel-reporter',
};
-const ROUTE_ALIASES = new Map([
- ['/where-am-i-24-2/', '/where-am-i-24/'],
-]);
+const ROUTE_ALIASES = new Map(Object.entries(routeAliasesRaw));
const localPosts = await loadLocalPosts(postsDir);
const authorPathMap = await buildAuthorPathMap(wxrDir, localPosts);
diff --git a/src/data/route-aliases.json b/src/data/route-aliases.json
new file mode 100644
--- /dev/null
+++ b/src/data/route-aliases.json
@@ -1,0 +1,6 @@
+{
+ "/where-am-i-24-2/": "/where-am-i-24/",
+ "/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022\uFFFC/": "/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022/",
+ "/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo\uFFFC/": "/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo/",
+ "/youll-lose-thousands-hosting-on-airbnb-if-you-do-this\uFFFC/": "/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/"
+}
diff --git a/src/lib/authorArchives.ts b/src/lib/authorArchives.ts
--- a/src/lib/authorArchives.ts
+++ b/src/lib/authorArchives.ts
@@ -1,10 +1,11 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import authorPostPaths from '../data/author-post-paths.json';
+import routeAliasesRaw from '../data/route-aliases.json';
export const AUTHOR_PAGE_SIZE = 12;
-const ROUTE_ALIASES = new Map([
- ['/where-am-i-24-2/', '/where-am-i-24/'],
-]);
+const ROUTE_ALIASES = new Map<string, string>(
+ Object.entries(routeAliasesRaw as Record<string, string>)
+);
type AuthorProfile = {
name: string;
diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro
--- a/src/pages/[...slug].astro
+++ b/src/pages/[...slug].astro
@@ -5,40 +5,23 @@
import legacySimilarBlocksByRoute from '../data/legacy-similar-blocks.json';
import legacyRelatedPostsByRouteRaw from '../data/legacy-related-posts.json';
import authorPostPaths from '../data/author-post-paths.json';
+import routeAliasesRaw from '../data/route-aliases.json';
import { normalizeWpUploadUrl, rewriteWpUploadsInHtml } from '../lib/media';
-function canonicalizeAliasPath(route: string): string {
- switch (route) {
- case '/where-am-i-24-2/':
- return '/where-am-i-24/';
- default:
- return route;
- }
-}
+const aliasPathToCanonicalPath = new Map<string, string>(
+ Object.entries(routeAliasesRaw as Record<string, string>)
+);
export async function getStaticPaths() {
+ const aliasPathToCanonicalPath = new Map<string, string>(
+ Object.entries(routeAliasesRaw as Record<string, string>)
+ );
const allEntries = [
...(await getCollection('posts')).filter((entry) => !entry.data.draft && entry.data.status === 'publish'),
...(await getCollection('pages')).filter((entry) => !entry.data.draft && entry.data.status === 'publish')
];
const byPath = new Map(allEntries.map((entry) => [entry.data.path, entry] as const));
- const aliasPathToCanonicalPath = new Map<string, string>([
- ['/where-am-i-24-2/', '/where-am-i-24/'],
- [
- '/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022\uFFFC/',
- '/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022/'
- ],
- [
- '/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo\uFFFC/',
- '/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo/'
- ],
- [
- '/youll-lose-thousands-hosting-on-airbnb-if-you-do-this\uFFFC/',
- '/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/'
- ]
- ]);
-
const contentPaths = allEntries
.filter((entry) => entry.data.path !== '/')
.map((entry) => ({
@@ -575,7 +558,7 @@
if (!raw) return '/';
const withStart = raw.startsWith('/') ? raw : `/${raw}`;
const normalized = withStart.endsWith('/') ? withStart : `${withStart}/`;
- return canonicalizeAliasPath(normalized);
+ return aliasPathToCanonicalPath.get(normalized) ?? normalized;
}
function pickRelatedPosts<
@@ -600,21 +583,6 @@
legacyRelatedByRoute: Record<string, string[]>;
}): { post: T; href: string }[] {
const byPath = new Map(allPosts.map((post) => [normalizeRoutePath(post.data.path), post] as const));
- const aliasPathToCanonicalPath = new Map<string, string>([
- ['/where-am-i-24-2/', '/where-am-i-24/'],
- [
- '/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022\uFFFC/',
- '/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022/'
- ],
- [
- '/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo\uFFFC/',
- '/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo/'
- ],
- [
- '/youll-lose-thousands-hosting-on-airbnb-if-you-do-this\uFFFC/',
- '/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/'
- ]
- ]);
const route = normalizeRoutePath(currentPath);
const legacyPaths = legacyRelatedByRoute[route] ?? [];
const targetCount = legacyPaths.length > 0 ? Math.min(6, legacyPaths.length) : 3;You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Route alias keys with
\uFFFCare unreachable for canonicalization- I normalized alias keys when building the canonicalization map by stripping
\uFFFC, so lookups now align with pre-normalized input paths.
- I normalized alias keys when building the canonicalization map by stripping
Or push these changes by commenting:
@cursor push 652d74f6ba
Preview (652d74f6ba)
diff --git a/src/lib/routePaths.ts b/src/lib/routePaths.ts
--- a/src/lib/routePaths.ts
+++ b/src/lib/routePaths.ts
@@ -1,7 +1,9 @@
import routeAliasesRaw from '../data/route-aliases.json';
const routeAliases = routeAliasesRaw as Record<string, string>;
-const routeAliasMap = new Map<string, string>(Object.entries(routeAliases));
+const routeAliasMap = new Map<string, string>(
+ Object.entries(routeAliases).map(([alias, canonical]) => [alias.replace(/\uFFFC/g, ''), canonical])
+);
type NormalizeRoutePathOptions = {
canonicalize?: boolean;You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 4380cc0. Configure here.


Summary
author-post-paths.jsonfrom the WordPress WXR exports plus localwordpressIdmetadata/author/[slug]/page/[n]/static routes with 12 posts per page to match WordPress author archives/where-am-i-24-2/still resolve to the correct author archiveValidation
yarn migrate:authors:mapyarn build/author/alan/= 12 cards, page 1 of 7 on both/author/alan/page/7/= 5 cards on both/author/tiffany/= 12 cards, page 1 of 41 on both/author/tiffany/page/41/= 10 cards on both/where-am-i-24-2/author link resolves to/author/tiffany/Note
Medium Risk
Medium risk because it changes static route generation and author-archive post resolution (including alias canonicalization), which can affect SEO/canonical URLs and content discoverability if mappings are wrong.
Overview
Adds paginated author archive pages with a shared
PaginationNavcomponent, generating real static routes at/author/[slug]/page/[n]/(12 posts per page) and updating metadata/canonical URLs accordingly.Rebuilds and centralizes author→post path mapping by introducing
scripts/build-author-post-paths.mjs(wired asyarn migrate:authors:map) and regeneratingsrc/data/author-post-paths.jsonfor parity with WordPress exports.Introduces
src/lib/routePaths.ts+src/data/route-aliases.jsonto canonicalize known alias routes; integrates this into[...slug].astrofor static path generation, related-post lookups, and author inference. Also broadens thewp-contentURL matcher inbuild-wp-content-manifest.mjsand adds several new redirect rules.Reviewed by Cursor Bugbot for commit 9d8f796. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Style