Skip to content

Fix author archive parity and pagination#738

Merged
justin808 merged 6 commits into
mainfrom
fix-author-archives
Apr 24, 2026
Merged

Fix author archive parity and pagination#738
justin808 merged 6 commits into
mainfrom
fix-author-archives

Conversation

@justin808

@justin808 justin808 commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

  • regenerate author-post-paths.json from the WordPress WXR exports plus local wordpressId metadata
  • add real /author/[slug]/page/[n]/ static routes with 12 posts per page to match WordPress author archives
  • normalize alias-route author inference so posts like /where-am-i-24-2/ still resolve to the correct author archive

Validation

  • yarn migrate:authors:map
  • yarn build
  • browser check against local build vs live WordPress confirmed:
    • /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/
  • current production baseline full-route audit before this branch: 667 routes audited, 662 clean, remaining flags concentrated in author archives and one heuristic post outlier

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 PaginationNav component, 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 as yarn migrate:authors:map) and regenerating src/data/author-post-paths.json for parity with WordPress exports.

Introduces src/lib/routePaths.ts + src/data/route-aliases.json to canonicalize known alias routes; integrates this into [...slug].astro for static path generation, related-post lookups, and author inference. Also broadens the wp-content URL matcher in build-wp-content-manifest.mjs and 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

    • Added pagination support to author archive pages, allowing readers to browse posts across multiple pages.
    • Introduced pagination navigation controls with previous/next links and page number selection.
  • Style

    • Added styling for pagination UI elements including page number buttons, current page indicator, and navigation links.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Walkthrough

Introduces 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

Cohort / File(s) Summary
Migration Infrastructure
package.json, scripts/build-author-post-paths.mjs
New npm script and CLI utility that generates author-post-paths.json by correlating WordPress XML exports with local markdown posts, filtering published entries and mapping creators to author slugs via aliases.
Data Generation
src/data/author-post-paths.json
Author-to-post-paths mapping completely regenerated; alan, tiffany, our-discount-desk, and our-travel-reporter arrays replaced with updated post routes and sort ordering.
Archive Library
src/lib/authorArchives.ts
New library exporting pagination constants, author profile lookups, archive entry retrieval, post slicing utilities, and canonical author page path builders for paginated author archives.
Pagination UI Component
src/components/PaginationNav.astro
New component rendering navigation links with boundary/nearby pages, ellipsis markers for gaps, previous/next buttons, and semantic ARIA attributes for accessible pagination.
Author Archive Pages
src/pages/author/[slug].astro, src/pages/author/[slug]/page/[page].astro
Modified base author page to source pagination-aware data via new archive library; new pagination template for additional author archive pages with dynamic route generation and post slicing.
Route Canonicalization
src/pages/[...slug].astro
Added path-canonicalization helper to normalize /where-am-i-24-2/ alias to canonical /where-am-i-24/ during route resolution.
Pagination Styling
src/styles/global.css
New CSS rules for .pagination-nav container, .page-numbers pill buttons with hover/current states, and .page-numbers--ellipsis variants with theme-driven colors and ARIA-hidden styling.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~40 minutes

Poem

🐰 Whiskers twitching with delight,
New pages paginated bright!
Authors' posts now neatly sorted,
WordPress routes expertly ported,
Hoppy hops through archives galore! 📄✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main objectives: regenerating author archives data and implementing pagination. It directly addresses the two primary changes (parity fix via regenerated data and pagination routing).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-author-archives

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 24, 2026

Copy link
Copy Markdown

Deploying blog-hichee-com with  Cloudflare Pages  Cloudflare Pages

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

View logs

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds static paginated author archive routes (/author/[slug]/page/[n]/) with 12 posts per page, regenerates author-post-paths.json from WordPress WXR exports (now sorted by date), and fixes alias-route author inference so /where-am-i-24-2/ correctly resolves to the tiffany archive. All three changes work together and are validated against live WordPress output. The remaining findings are all P2: a hardcoded aria-label in PaginationNav, a minor entity-ordering quirk in decodeXml, and a missing deduplication guard in getAuthorArchiveEntries for routes where both the alias path and canonical path land in the JSON.

Confidence Score: 5/5

Safe 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

Filename Overview
scripts/build-author-post-paths.mjs New build script that reads WXR exports and local post metadata to regenerate author-post-paths.json; minor entity-ordering issue in decodeXml could double-decode ampersands in author names.
src/lib/authorArchives.ts New library extracting author archive logic; alias-route expansion is correct but lacks deduplication, leaving a theoretical duplicate-card scenario when both alias and canonical routes land in the JSON.
src/components/PaginationNav.astro New reusable pagination component; works correctly but hardcodes aria-label as 'Author pages', limiting reusability.
src/pages/author/[slug]/page/[page].astro New static route for paginated author archives (pages 2+); correctly generates paths for each author and page, passes the right slice of posts, and renders with PaginationNav.
src/pages/author/[slug].astro Updated to use new authorArchives library; page-1 slice is correct and pagination nav is wired up properly.
src/pages/[...slug].astro Updated inferAuthorSlugFromPath to use canonicalized alias-aware normalizeRoutePath, correctly resolving alias routes like /where-am-i-24-2/ to the tiffany author archive.
src/data/author-post-paths.json Regenerated from WXR exports; posts now ordered by date descending. Contains both /where-am-i-24-2/ and /where-am-i-24/ for the same author — intentional for alias parity but could produce duplicate cards without a deduplication guard.

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/]
Loading

Comments Outside Diff (1)

  1. src/components/PaginationNav.astro, line 258 (link)

    P2 Hardcoded aria-label reduces component reusability

    aria-label="Author pages" is baked in, so if PaginationNav is ever reused for category or tag archives the label will be semantically wrong for screen readers. Consider accepting it as a prop with a sensible default.

Reviews (1): Last reviewed commit: "Fix author archive parity and pagination" | Re-trigger Greptile

Comment on lines +173 to +185
.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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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, '&')

Comment thread src/lib/authorArchives.ts
Comment on lines +50 to +55

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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in src/lib/authorArchives.ts (as ROUTE_ALIASES, applied as a post-lookup fallback inside resolveArchivePost), while here it is applied upfront inside normalizeRoutePath. The two files will diverge as aliases grow, and they already use different application strategies. Extracting a single helper (e.g., src/lib/routeCanonicalization.ts exporting canonicalizeAliasPath and/or a shared normalizeRoutePath) 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 Map mirrors the aliasPathToCanonicalPath Map already used in getStaticPaths/pickRelatedPosts in this same file and scales better than a switch once 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].astro and src/lib/authorArchives.ts (replacing the local ROUTE_ALIASES + fallback in resolveArchivePost).

🤖 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 normalizeRoutePath canonicalizes /where-am-i-24-2/ upfront, the /where-am-i-24-2/ entry in the local aliasPathToCanonicalPath inside pickRelatedPosts (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 \uFFFC entries in that Map are unreachable because the regex at line 574 strips \uFFFC from raw. 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: Hardcoded aria-label="Author pages" limits reusability.

The component is written as a generic PaginationNav but 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-label indicating 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 computing titleCaseSlug(slug) twice in the fallback.

Minor, but the fallback branch calls titleCaseSlug(slug) for both name and bio.

♻️ 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: Export AuthorArchiveEntry for downstream typing.

getAuthorArchiveEntries() is public and returns Promise<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 replace posts: 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: Subtle recordedRoute expression — please add a clarifying comment.

The ternary condition preferred || fallback || !aliasResolved ? resolved.route : sourceRoute encodes the intent "prefer the local canonical path, except when we only matched via a ROUTE_ALIASES entry — 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: Same any[] typing as page/[page].astro.

For consistency with the sister paginated route, please switch posts: any[] to AuthorArchivePost[] (exported from src/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 shared AuthorArchivePost type instead of any[].

src/lib/authorArchives.ts already exports AuthorArchivePost. Using any[] 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

📥 Commits

Reviewing files that changed from the base of the PR and between 37ebe81 and d1cb5d9.

📒 Files selected for processing (9)
  • package.json
  • scripts/build-author-post-paths.mjs
  • src/components/PaginationNav.astro
  • src/data/author-post-paths.json
  • src/lib/authorArchives.ts
  • src/pages/[...slug].astro
  • src/pages/author/[slug].astro
  • src/pages/author/[slug]/page/[page].astro
  • src/styles/global.css

Comment thread scripts/build-author-post-paths.mjs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json and updated the archive library, slug page, and author-path build script to read from that shared source so alias behavior stays consistent.

Create PR

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.

Comment thread src/lib/authorArchives.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Route alias keys with \uFFFC are unreachable for canonicalization
    • I normalized alias keys when building the canonicalization map by stripping \uFFFC, so lookups now align with pre-normalized input paths.

Create PR

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.

Comment thread src/data/route-aliases.json
@justin808 justin808 merged commit def69eb into main Apr 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant