Skip to content

Commit fbd8e91

Browse files
committed
fix(landing): complete Organization schema, add CollectionPage/BlogPosting JSON-LD, fix TechArticle rich-result eligibility
- Organization schema (site-structured-data.tsx): add brand and contactPoint.url (verified against the real /contact route); all other fields were already correct and verified against the Footer's sameAs links. foundingDate/legalName/address omitted — not verifiable from anything in this repo. - CollectionPage (blog + library index): buildCollectionPageJsonLd now takes the real posts list (same getAllPostMeta() the index page already renders from) and emits a mainEntity ItemList of BlogPosting stubs instead of omitting mainEntity entirely. - BlogPosting + TechArticle (post detail template): Google's Article rich-result eligibility only recognizes Article/NewsArticle/BlogPosting — a bare TechArticle type isn't in that allowlist. buildArticleJsonLd now emits a multi-type @type array (["BlogPosting","TechArticle"]) for genuinely technical posts, and "BlogPosting" alone for posts that are general announcements, via a new `technical` frontmatter flag (defaults true; set to false on the series-a funding-announcement post, the one post with no technical content). Also fixed a real markup/schema mismatch: the article's `speakable.cssSelector` referenced `[itemprop="description"]`, but no element carried that itemProp — added it to the description paragraph. TechArticle/BlogPosting image URLs are now made absolute (previously relative paths, invalid for crawlers). - FAQPage: audited every LandingFAQ usage (/models, /models/[provider], /models/[provider]/[model], /integrations, /integrations/[slug], /comparison, /comparison/[provider]) — all already emit matching FAQPage JSON-LD sourced from the same data passed to LandingFAQ; no gaps found, no changes needed.
1 parent 0640c0b commit fbd8e91

10 files changed

Lines changed: 72 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default async function BlogIndex({
3535
posts={posts}
3636
page={pageNum}
3737
tag={tag}
38-
collectionJsonLd={buildCollectionPageJsonLd()}
38+
collectionJsonLd={buildCollectionPageJsonLd(posts)}
3939
/>
4040
)
4141
}

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

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

3636
return (
37-
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/TechArticle'>
37+
<article
38+
className='w-full bg-[var(--bg)]'
39+
itemScope
40+
itemType={`https://schema.org/${post.technical ? 'TechArticle' : 'BlogPosting'}`}
41+
>
3842
<JsonLd data={graphJsonLd} />
3943
<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'>
4044
<div className='mb-6'>
@@ -66,7 +70,10 @@ export function ContentPostPage({
6670
>
6771
{post.title}
6872
</h1>
69-
<p className='mt-4 text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
73+
<p
74+
className='mt-4 text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'
75+
itemProp='description'
76+
>
7077
{post.description}
7178
</p>
7279
</div>

apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const SITE_JSON_LD = {
2222
caption: 'Sim Logo',
2323
},
2424
image: { '@id': `${SITE_URL}#logo` },
25+
brand: { '@type': 'Brand', name: 'Sim' },
2526
sameAs: [
2627
'https://x.com/simdotai',
2728
'https://github.com/simstudioai/sim',
@@ -31,6 +32,7 @@ const SITE_JSON_LD = {
3132
contactPoint: {
3233
'@type': 'ContactPoint',
3334
contactType: 'customer support',
35+
url: `${SITE_URL}/contact`,
3436
availableLanguage: ['en'],
3537
},
3638
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default async function LibraryIndex({
3535
posts={posts}
3636
page={pageNum}
3737
tag={tag}
38-
collectionJsonLd={buildCollectionPageJsonLd()}
38+
collectionJsonLd={buildCollectionPageJsonLd(posts)}
3939
/>
4040
)
4141
}

apps/sim/content/blog/series-a/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ timeRequired: PT4M
1616
canonical: https://www.sim.ai/blog/series-a
1717
featured: true
1818
draft: false
19+
technical: false
1920
---
2021

2122
![Sim team photo](/blog/series-a/team.jpg)

apps/sim/lib/blog/seo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export function buildPostGraphJsonLd(post: ContentMeta) {
2525
return buildPostGraphJsonLdGeneric(post, BLOG_SECTION)
2626
}
2727

28-
export function buildCollectionPageJsonLd() {
29-
return buildCollectionPageJsonLdGeneric(BLOG_SECTION)
28+
export function buildCollectionPageJsonLd(posts: ContentMeta[]) {
29+
return buildCollectionPageJsonLdGeneric(BLOG_SECTION, posts)
3030
}
3131

3232
export function buildIndexMetadata(input: { tag?: string; pageNum: number }) {

apps/sim/lib/content/registry-factory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
161161
wordCount,
162162
draft: fm.draft,
163163
featured: fm.featured ?? false,
164+
technical: fm.technical,
164165
}
165166
})
166167
)

apps/sim/lib/content/schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export const ContentFrontmatterSchema = z
4242
canonical: z.string().url(),
4343
draft: z.boolean().default(false),
4444
featured: z.boolean().default(false),
45+
/**
46+
* Whether this post covers technical/developer content (architecture,
47+
* implementation, how-tos). Drives whether `TechArticle` is included
48+
* alongside `BlogPosting` in the post's JSON-LD `@type` — general
49+
* announcements (funding, company news) should set this to `false`.
50+
*/
51+
technical: z.boolean().default(true),
4552
})
4653
.strict()
4754

@@ -70,6 +77,7 @@ export interface ContentMeta {
7077
canonical: string
7178
draft: boolean
7279
featured: boolean
80+
technical: boolean
7381
}
7482

7583
export interface ContentPost extends ContentMeta {

apps/sim/lib/content/seo.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,25 @@ export function buildPostMetadata(post: ContentMeta): Metadata {
7373
}
7474
}
7575

76+
/**
77+
* Google's Article rich-result eligibility only recognizes `Article`,
78+
* `NewsArticle`, and `BlogPosting` — a bare `TechArticle` type is not in that
79+
* allowlist, so it silently loses rich-result eligibility. `BlogPosting` is
80+
* therefore always included; `TechArticle` is layered on via a multi-type
81+
* `@type` array (the standard schema.org way to say "this is both") only for
82+
* posts that are genuinely technical/developer content (`post.technical`) —
83+
* general announcements (funding, company news) get `BlogPosting` alone.
84+
*/
7685
export function buildArticleJsonLd(post: ContentMeta) {
7786
return {
78-
'@type': 'TechArticle',
87+
'@type': post.technical ? ['BlogPosting', 'TechArticle'] : 'BlogPosting',
7988
url: post.canonical,
8089
headline: post.title,
8190
description: post.description,
8291
image: [
8392
{
8493
'@type': 'ImageObject',
85-
url: post.ogImage,
94+
url: post.ogImage.startsWith('http') ? post.ogImage : `${SITE_URL}${post.ogImage}`,
8695
width: post.ogImageWidth ?? 1200,
8796
height: post.ogImageHeight ?? 630,
8897
caption: post.ogAlt || post.title,
@@ -91,7 +100,7 @@ export function buildArticleJsonLd(post: ContentMeta) {
91100
datePublished: post.date,
92101
dateModified: post.updated ?? post.date,
93102
wordCount: post.wordCount,
94-
proficiencyLevel: 'Beginner',
103+
...(post.technical ? { proficiencyLevel: 'Beginner' } : {}),
95104
author: (post.authors && post.authors.length > 0 ? post.authors : [post.author]).map((a) => ({
96105
'@type': 'Person',
97106
name: a.name,
@@ -340,7 +349,17 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author)
340349
}
341350
}
342351

343-
export function buildCollectionPageJsonLd(section: ContentSection) {
352+
/**
353+
* `mainEntity` lists the section's real, currently-published posts (sourced
354+
* from the same `getAllPostMeta()` list the index page renders from — never
355+
* a hardcoded list) so the collection's JSON-LD stays in sync with what's
356+
* actually on the page.
357+
*/
358+
export function buildCollectionPageJsonLd(section: ContentSection, posts: ContentMeta[]) {
359+
const dateSorted = [...posts].sort(
360+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
361+
)
362+
344363
return {
345364
'@context': 'https://schema.org',
346365
'@type': 'CollectionPage',
@@ -362,5 +381,27 @@ export function buildCollectionPageJsonLd(section: ContentSection) {
362381
name: 'Sim',
363382
url: SITE_URL,
364383
},
384+
mainEntity: {
385+
'@type': 'ItemList',
386+
itemListElement: dateSorted.map((post, index) => ({
387+
'@type': 'ListItem',
388+
position: index + 1,
389+
url: post.canonical,
390+
item: {
391+
'@type': 'BlogPosting',
392+
headline: post.title,
393+
description: post.description,
394+
url: post.canonical,
395+
datePublished: post.date,
396+
dateModified: post.updated ?? post.date,
397+
image: post.ogImage.startsWith('http') ? post.ogImage : `${SITE_URL}${post.ogImage}`,
398+
author: {
399+
'@type': 'Person',
400+
name: post.author.name,
401+
...(post.author.url ? { url: post.author.url } : {}),
402+
},
403+
},
404+
})),
405+
},
365406
}
366407
}

apps/sim/lib/library/seo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export function buildPostGraphJsonLd(post: ContentMeta) {
2626
return buildPostGraphJsonLdGeneric(post, LIBRARY_SECTION)
2727
}
2828

29-
export function buildCollectionPageJsonLd() {
30-
return buildCollectionPageJsonLdGeneric(LIBRARY_SECTION)
29+
export function buildCollectionPageJsonLd(posts: ContentMeta[]) {
30+
return buildCollectionPageJsonLdGeneric(LIBRARY_SECTION, posts)
3131
}
3232

3333
export function buildIndexMetadata(input: { tag?: string; pageNum: number }) {

0 commit comments

Comments
 (0)