Skip to content

Commit 5a70bea

Browse files
committed
fix(landing): match microdata itemType and scope collection JSON-LD to visible posts
Address Greptile/Cursor review on PR #5638: - itemType now always resolves to BlogPosting (matching Greptile's exact suggested fix), since the JSON-LD graph already carries the richer TechArticle type via a multi-type array and the microdata path doesn't need to duplicate that distinction - buildCollectionPageJsonLd now receives the same tag-filtered/paginated post subset ContentIndexPage actually renders, instead of the full unfiltered catalog, via a new shared selectVisiblePosts/paginateContentPosts helper in lib/content/index-list.ts (also de-duplicates the pagination logic that previously lived only in ContentIndexPage)
1 parent fbd8e91 commit 5a70bea

5 files changed

Lines changed: 68 additions & 29 deletions

File tree

apps/sim/app/(landing)/blog/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from 'next'
22
import { getAllPostMeta } from '@/lib/blog/registry'
33
import { BLOG_SECTION, buildCollectionPageJsonLd, buildIndexMetadata } from '@/lib/blog/seo'
4+
import { selectVisiblePosts } from '@/lib/content/index-list'
45
import { ContentIndexPage } from '@/app/(landing)/components'
56

67
/**
@@ -35,7 +36,9 @@ export default async function BlogIndex({
3536
posts={posts}
3637
page={pageNum}
3738
tag={tag}
38-
collectionJsonLd={buildCollectionPageJsonLd(posts)}
39+
collectionJsonLd={buildCollectionPageJsonLd(
40+
selectVisiblePosts(posts, { tag, page: pageNum })
41+
)}
3942
/>
4043
)
4144
}

apps/sim/app/(landing)/components/content-index-page/content-index-page.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { ChipLink } from '@sim/emcn'
22
import Image from 'next/image'
33
import Link from 'next/link'
4+
import { paginateContentPosts } from '@/lib/content/index-list'
45
import type { ContentMeta } from '@/lib/content/schema'
56
import { Cta } from '@/app/(landing)/components/cta/cta'
67
import { JsonLd } from '@/app/(landing)/components/json-ld'
78

8-
const POSTS_PER_PAGE = 20
9-
const FEATURED_COUNT = 3
10-
119
interface ContentIndexPageProps {
1210
/** Route base path, e.g. `/blog` or `/library`. */
1311
basePath: string
@@ -36,25 +34,7 @@ export function ContentIndexPage({
3634
tag,
3735
collectionJsonLd,
3836
}: ContentIndexPageProps) {
39-
const filtered = tag ? posts.filter((p) => p.tags.includes(tag)) : posts
40-
const dateSorted = [...filtered].sort(
41-
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
42-
)
43-
44-
// Featured posts render once, in the page-1 featured row, regardless of
45-
// where their date would otherwise place them — so they're carved out of
46-
// the paginated pool up front rather than re-sorted to the front of page 1
47-
// (which left them still reachable, and duplicated, on a later page).
48-
const explicitlyFeatured = dateSorted.filter((p) => p.featured).slice(0, FEATURED_COUNT)
49-
const featuredPosts =
50-
explicitlyFeatured.length > 0 ? explicitlyFeatured : dateSorted.slice(0, FEATURED_COUNT)
51-
const featuredSlugs = new Set(featuredPosts.map((p) => p.slug))
52-
const paginated = dateSorted.filter((p) => !featuredSlugs.has(p.slug))
53-
54-
const totalPages = Math.max(1, Math.ceil(paginated.length / POSTS_PER_PAGE))
55-
const start = (page - 1) * POSTS_PER_PAGE
56-
const featured = page === 1 ? featuredPosts : []
57-
const remaining = paginated.slice(start, start + POSTS_PER_PAGE)
37+
const { featured, remaining, totalPages } = paginateContentPosts(posts, { tag, page })
5838

5939
const pageHref = (targetPage: number) =>
6040
`${basePath}?page=${targetPage}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`

apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,7 @@ export function ContentPostPage({
3434
const Article = post.Content
3535

3636
return (
37-
<article
38-
className='w-full bg-[var(--bg)]'
39-
itemScope
40-
itemType={`https://schema.org/${post.technical ? 'TechArticle' : 'BlogPosting'}`}
41-
>
37+
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/BlogPosting'>
4238
<JsonLd data={graphJsonLd} />
4339
<header className='mx-auto w-full max-w-[1460px] px-20 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
4440
<div className='mb-6'>

apps/sim/app/(landing)/library/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from 'next'
2+
import { selectVisiblePosts } from '@/lib/content/index-list'
23
import { getAllPostMeta } from '@/lib/library/registry'
34
import { buildCollectionPageJsonLd, buildIndexMetadata, LIBRARY_SECTION } from '@/lib/library/seo'
45
import { ContentIndexPage } from '@/app/(landing)/components'
@@ -35,7 +36,9 @@ export default async function LibraryIndex({
3536
posts={posts}
3637
page={pageNum}
3738
tag={tag}
38-
collectionJsonLd={buildCollectionPageJsonLd(posts)}
39+
collectionJsonLd={buildCollectionPageJsonLd(
40+
selectVisiblePosts(posts, { tag, page: pageNum })
41+
)}
3942
/>
4043
)
4144
}

apps/sim/lib/content/index-list.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { ContentMeta } from '@/lib/content/schema'
2+
3+
export const POSTS_PER_PAGE = 20
4+
export const FEATURED_COUNT = 3
5+
6+
interface PaginatedContentPosts {
7+
featured: ContentMeta[]
8+
remaining: ContentMeta[]
9+
totalPages: number
10+
}
11+
12+
/**
13+
* Reproduces `ContentIndexPage`'s render logic for a given `tag`/`page`
14+
* combination: tag-filtered, date-sorted, with up to `FEATURED_COUNT`
15+
* featured posts (explicit `post.featured` first, falling back to the most
16+
* recent) carved out of the paginated pool and returned only on page 1.
17+
* Shared by `ContentIndexPage` itself and `selectVisiblePosts` below, so
18+
* every consumer stays in lockstep with what's actually rendered.
19+
*/
20+
export function paginateContentPosts(
21+
posts: ContentMeta[],
22+
{ tag, page }: { tag?: string; page: number }
23+
): PaginatedContentPosts {
24+
const filtered = tag ? posts.filter((p) => p.tags.includes(tag)) : posts
25+
const dateSorted = [...filtered].sort(
26+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
27+
)
28+
29+
const explicitlyFeatured = dateSorted.filter((p) => p.featured).slice(0, FEATURED_COUNT)
30+
const featuredPosts =
31+
explicitlyFeatured.length > 0 ? explicitlyFeatured : dateSorted.slice(0, FEATURED_COUNT)
32+
const featuredSlugs = new Set(featuredPosts.map((p) => p.slug))
33+
const paginated = dateSorted.filter((p) => !featuredSlugs.has(p.slug))
34+
35+
const totalPages = Math.max(1, Math.ceil(paginated.length / POSTS_PER_PAGE))
36+
const start = (page - 1) * POSTS_PER_PAGE
37+
38+
return {
39+
featured: page === 1 ? featuredPosts : [],
40+
remaining: paginated.slice(start, start + POSTS_PER_PAGE),
41+
totalPages,
42+
}
43+
}
44+
45+
/**
46+
* Flat, render-order post list (featured then remaining) for a given
47+
* `tag`/`page` combination - used to keep `buildCollectionPageJsonLd`'s
48+
* `mainEntity` ItemList in sync with the posts actually visible on a
49+
* filtered or paginated index URL, instead of the full unfiltered catalog.
50+
*/
51+
export function selectVisiblePosts(
52+
posts: ContentMeta[],
53+
options: { tag?: string; page: number }
54+
): ContentMeta[] {
55+
const { featured, remaining } = paginateContentPosts(posts, options)
56+
return [...featured, ...remaining]
57+
}

0 commit comments

Comments
 (0)