From 5dc8d8f097c27888971eb8fbc54f85a61e36e6b8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 13 Jul 2026 18:02:07 -0700 Subject: [PATCH] fix(landing): align author-page UI with the site shell and link bylines to it (#5655) * fix(landing): align author-page UI with the site shell and link bylines to it Rebuilds ContentAuthorPage/ContentAuthorLoading to match the standard page-shell pattern used by ContentIndexPage/ContentPostPage (max-w-[1460px] header, BackLink, framed post-list rows with hover states) instead of the previous bare max-w-[900px] container with a 2-column image grid. Adds a styled not-found fallback matching the comparisons/integrations pattern. The byline author Link in ContentPostPage now points at the internal author page (/{basePath}/authors/{id}) instead of the author's external X/GitHub profile, giving the author page a real navigation entry point for the first time - previously it was only reachable via the sitemap. Also fixes both author-page route files to match on any of a post's authors (post.authors.some(a => a.id === id)) instead of only the primary author (post.author.id === id), so posts with co-authors show up on every listed author's page, consistent with the sitemap's own author enumeration. * fix(landing): address review findings on author-page UI rebuild - Not-found fallback no longer renders a nested
- blog/layout.tsx and library/layout.tsx already provide that landmark, so the fallback now uses
like the rest of the component. - Byline author link now encodeURIComponent()s the author id before using it as a route segment. - Loading skeleton's post-list wrapper now matches the real page's mx-auto max-w-[1460px] shell (was mx-20/max-lg:mx-8, which stretches past the header on wide viewports). * fix(landing): make author Person.image an absolute URL in JSON-LD Same class of bug as the earlier article-image fix: author.avatarUrl is a site-relative path, so Person.image was emitting an invalid relative URL for crawlers. Prefixes with SITE_URL, matching buildArticleJsonLd's existing pattern. * fix(landing): align author not-found fallback width with the site convention max-w-[1446px]/px-12 was copied from comparisons/not-found.tsx, which turns out to be the outlier - models/not-found.tsx and integrations/not-found.tsx both use max-w-[1460px]/px-20, matching the rest of the site's shell and this component's own success-state header. * fix(landing): encode author id consistently in every author URL builder The byline link encoded the author id, but buildAuthorMetadata's canonical URL, buildAuthorGraphJsonLd's Person.url/BreadcrumbList item, and sitemap.ts's buildAuthorPages URL still interpolated the raw id - a mismatch could point sitemap/canonical/structured-data URLs at a different route than the actual page. All four now encodeURIComponent() consistently. --- .../app/(landing)/blog/authors/[id]/page.tsx | 10 +- .../content-author-loading.tsx | 40 +++-- .../content-author-page.tsx | 143 +++++++++++++----- .../content-post-page/content-post-page.tsx | 5 +- .../(landing)/library/authors/[id]/page.tsx | 10 +- apps/sim/app/sitemap.ts | 2 +- apps/sim/lib/content/seo.ts | 10 +- 7 files changed, 149 insertions(+), 71 deletions(-) diff --git a/apps/sim/app/(landing)/blog/authors/[id]/page.tsx b/apps/sim/app/(landing)/blog/authors/[id]/page.tsx index 302b843cae..6d2869eeca 100644 --- a/apps/sim/app/(landing)/blog/authors/[id]/page.tsx +++ b/apps/sim/app/(landing)/blog/authors/[id]/page.tsx @@ -11,18 +11,20 @@ export async function generateMetadata({ params: Promise<{ id: string }> }): Promise { const { id } = await params - const posts = (await getAllPostMeta()).filter((p) => p.author.id === id) - return buildAuthorMetadata(id, posts[0]?.author) + const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id)) + const author = posts[0]?.authors.find((a) => a.id === id) + return buildAuthorMetadata(id, author) } export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params - const posts = (await getAllPostMeta()).filter((p) => p.author.id === id) - const author = posts[0]?.author + const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id)) + const author = posts[0]?.authors.find((a) => a.id === id) return ( -
- - +
+
+ +
+ + +
-
- {Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => ( -
- -
- - + +
+ +
+
+ {Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => ( +
+
+ +
+ + +
+ +
+
-
- ))} + ))} +
-
+ ) } diff --git a/apps/sim/app/(landing)/components/content-author-page/content-author-page.tsx b/apps/sim/app/(landing)/components/content-author-page/content-author-page.tsx index e57ad9e3c6..43225eb95c 100644 --- a/apps/sim/app/(landing)/components/content-author-page/content-author-page.tsx +++ b/apps/sim/app/(landing)/components/content-author-page/content-author-page.tsx @@ -1,11 +1,16 @@ +import { ChipLink } from '@sim/emcn' import Image from 'next/image' import Link from 'next/link' import type { ContentMeta } from '@/lib/content/schema' +import { BackLink } from '@/app/(landing)/components/back-link' +import { Cta } from '@/app/(landing)/components/cta/cta' import { JsonLd } from '@/app/(landing)/components/json-ld' interface ContentAuthorPageProps { /** Route base path, e.g. `/blog` or `/library`. */ basePath: string + /** Section label used in the not-found fallback, e.g. "Blog" or "Library". */ + sectionName: string authorName?: string authorAvatarUrl?: string /** Posts already filtered down to this author. */ @@ -13,9 +18,15 @@ interface ContentAuthorPageProps { graphJsonLd?: Record } -/** Shared author-profile layout for a content section. */ +/** + * Shared author-profile layout for a content section: standard page-shell + * header (matching `ContentIndexPage`/`ContentPostPage`) with avatar + name, + * followed by the author's posts in the same framed-list card style used + * everywhere else on the site. + */ export function ContentAuthorPage({ basePath, + sectionName, authorName, authorAvatarUrl, posts, @@ -23,54 +34,104 @@ export function ContentAuthorPage({ }: ContentAuthorPageProps) { if (!authorName) { return ( -
-

Author not found

-
+
+

+ Author not found +

+

+ The author you're looking for doesn't exist or has been moved. +

+ + Browse {sectionName} + +
) } return ( -
- {graphJsonLd && } -
- {authorAvatarUrl ? ( - {authorName} - ) : null} -

{authorName}

-
-
- {posts.map((p) => ( - -
+ <> +
+ {graphJsonLd && } + +
+
+ +
+ +
+ {authorAvatarUrl ? ( {p.title} -
-
- {new Date(p.date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} -
-
{p.title}
+ ) : null} +

+ {authorName} +

+
+
+ +
+ +
+
+ {posts.map((p) => ( +
+ + + {new Date(p.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + +
+ + {new Date(p.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + +

+ {p.title} +

+

+ {p.description} +

+
+ +
+ {p.title} +
+ +
-
- - ))} + ))} +
+
+ +
+
+ +
+
-
+ ) } diff --git a/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx b/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx index e7ab328e01..fccff3158d 100644 --- a/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx +++ b/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx @@ -96,9 +96,8 @@ export function ContentPostPage({ ) : null} }): Promise { const { id } = await params - const posts = (await getAllPostMeta()).filter((p) => p.author.id === id) - return buildAuthorMetadata(id, posts[0]?.author) + const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id)) + const author = posts[0]?.authors.find((a) => a.id === id) + return buildAuthorMetadata(id, author) } export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params - const posts = (await getAllPostMeta()).filter((p) => p.author.id === id) - const author = posts[0]?.author + const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id)) + const author = posts[0]?.authors.find((a) => a.id === id) return ( ({ - url: `${SITE_URL}${basePath}/authors/${id}`, + url: `${SITE_URL}${basePath}/authors/${encodeURIComponent(id)}`, lastModified: date, })) } diff --git a/apps/sim/lib/content/seo.ts b/apps/sim/lib/content/seo.ts index df224f9196..dcad1a7adf 100644 --- a/apps/sim/lib/content/seo.ts +++ b/apps/sim/lib/content/seo.ts @@ -285,7 +285,7 @@ export function buildAuthorMetadata( author?: Author ): Metadata { const name = author?.name ?? 'Author' - const canonical = `${SITE_URL}${section.basePath}/authors/${id}` + const canonical = `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(id)}` const description = `Read articles by ${name} on the Sim ${section.name.toLowerCase()}.` return { title: `${name} | Sim ${section.name}`, @@ -318,9 +318,11 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author) { '@type': 'Person', name: author.name, - url: `${SITE_URL}${section.basePath}/authors/${author.id}`, + url: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`, sameAs: author.url ? [author.url] : [], - image: author.avatarUrl, + image: author.avatarUrl?.startsWith('http') + ? author.avatarUrl + : author.avatarUrl && `${SITE_URL}${author.avatarUrl}`, worksFor: { '@type': 'Organization', name: 'Sim', @@ -341,7 +343,7 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author) '@type': 'ListItem', position: 3, name: author.name, - item: `${SITE_URL}${section.basePath}/authors/${author.id}`, + item: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`, }, ], },