Skip to content

Commit 360c326

Browse files
committed
improvement(library): single JSON-LD source, Markdown articles, and IndexNow pings
1 parent 772fcf7 commit 360c326

16 files changed

Lines changed: 484 additions & 53 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,42 @@ jobs:
636636
fi
637637
bunx trigger.dev@4.5.16 promote "$VERSION" "${TARGET_ARGS[@]}"
638638
639+
# Production only, once the new app serves traffic: submit the library and comparison URLs this
640+
# push changed to IndexNow. Best effort: nothing depends on it, and a failure never fails the run.
641+
indexnow:
642+
name: indexnow
643+
needs: [promote, trigger-promote]
644+
if: >-
645+
!cancelled() &&
646+
github.event_name == 'push' &&
647+
github.ref == 'refs/heads/main' &&
648+
needs.promote.outputs.promoted == 'true' &&
649+
needs.trigger-promote.result == 'success'
650+
runs-on: *runner-2vcpu
651+
timeout-minutes: 5
652+
continue-on-error: true
653+
permissions:
654+
contents: read
655+
steps:
656+
- name: Checkout code
657+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
658+
659+
- name: Setup Bun
660+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
661+
with:
662+
bun-version: 1.4.4
663+
664+
# The diff needs only the two trees, so the previous head is fetched alone. A push that
665+
# created the branch has an all-zero `before`, which the script skips.
666+
- name: Submit changed URLs to IndexNow
667+
env:
668+
BASE_SHA: ${{ github.event.before }}
669+
run: |
670+
if [[ ! "$BASE_SHA" =~ ^0+$ ]]; then
671+
git fetch --no-tags --depth=1 origin "$BASE_SHA"
672+
fi
673+
bun scripts/ping-indexnow.ts "$BASE_SHA" "$GITHUB_SHA"
674+
639675
# Build ARM64 images for GHCR (main branch only, runs in parallel with
640676
# tests). Pushes only the immutable sha tag — latest-arm64/version-arm64
641677
# are applied by ghcr after the gate, so a failing run

‎apps/sim/app/(landing)/blog/[slug]/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function generateMetadata({
2424
const { slug } = await params
2525
const post = await getPostBySlug(slug)
2626
if (!post || post.draft) return {}
27-
return buildPostMetadata(post)
27+
return buildPostMetadata(post, BLOG_SECTION)
2828
}
2929

3030
export const revalidate = 86400

‎apps/sim/app/(landing)/changelog/[slug]/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function generateStaticParams() {
1919
export async function generateMetadata({ params }: ChangelogEntryPageProps): Promise<Metadata> {
2020
const { slug } = await params
2121
const entry = await getEntryBySlug(slug)
22-
return entry ? buildPostMetadata(entry) : {}
22+
return entry ? buildPostMetadata(entry, CHANGELOG_SECTION) : {}
2323
}
2424

2525
export default async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {

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

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function ContentPostPage({
4040
const showUpdated = modifiedIso.slice(0, 10) !== post.date.slice(0, 10)
4141

4242
return (
43-
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/BlogPosting'>
43+
<article className='w-full bg-[var(--bg)]'>
4444
<JsonLd data={graphJsonLd} />
4545
<header className='mx-auto w-full max-w-[1728px] px-10 pt-[112px] max-sm:pt-20 max-md:px-7 max-lg:px-8 max-xl:px-9'>
4646
<div className='mb-6'>
@@ -59,7 +59,6 @@ export function ContentPostPage({
5959
sizes='(max-width: 1023px) calc(100vw - 64px), 450px'
6060
priority
6161
fetchPriority='high'
62-
itemProp='image'
6362
unoptimized
6463
/>
6564
</div>
@@ -68,13 +67,13 @@ export function ContentPostPage({
6867
<div>
6968
<h1
7069
className='text-balance text-[28px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
71-
itemProp='headline'
70+
data-speakable='headline'
7271
>
7372
{post.title}
7473
</h1>
7574
<p
7675
className='mt-4 text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg sm:leading-7'
77-
itemProp='description'
76+
data-speakable='description'
7877
>
7978
{post.description}
8079
</p>
@@ -84,25 +83,21 @@ export function ContentPostPage({
8483
<time
8584
className='text-[var(--text-secondary)] text-xs uppercase tracking-[0.1em]'
8685
dateTime={post.date}
87-
itemProp='datePublished'
8886
>
8987
{formatPostDate(post.date)}
9088
</time>
91-
{showUpdated ? (
89+
{showUpdated && (
9290
<>
9391
<span aria-hidden='true' className='text-[var(--text-secondary)] text-xs'>
9492
·
9593
</span>
9694
<time
9795
className='text-[var(--text-secondary)] text-xs uppercase tracking-[0.1em]'
9896
dateTime={modifiedIso}
99-
itemProp='dateModified'
10097
>
10198
Updated {formatPostDate(modifiedIso)}
10299
</time>
103100
</>
104-
) : (
105-
<meta itemProp='dateModified' content={modifiedIso} />
106101
)}
107102
</div>
108103
<div className='flex items-center gap-3'>
@@ -118,11 +113,8 @@ export function ContentPostPage({
118113
href={`${basePath}/authors/${encodeURIComponent(a?.id ?? '')}`}
119114
rel='author'
120115
className='text-[var(--text-secondary)] text-xs uppercase tracking-[0.1em] hover:text-[var(--text-primary)]'
121-
itemProp='author'
122-
itemScope
123-
itemType='https://schema.org/Person'
124116
>
125-
<span itemProp='name'>{a?.name}</span>
117+
{a?.name}
126118
</Link>
127119
</div>
128120
))}
@@ -139,7 +131,7 @@ export function ContentPostPage({
139131

140132
<div className='mx-auto w-full max-w-[1728px] px-10 max-md:px-7 max-lg:px-8 max-xl:px-9'>
141133
<div className='border-[var(--border)] border-x'>
142-
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
134+
<div className='mx-auto max-w-[900px] px-6 py-16'>
143135
<div
144136
className={cn(
145137
'prose prose-lg max-w-none prose-headings:font-season prose-headings:tracking-[-0.02em]',
@@ -169,11 +161,6 @@ export function ContentPostPage({
169161
</div>
170162

171163
<div className='-mt-px h-px w-full bg-[var(--border)]' />
172-
173-
<meta itemProp='publisher' content='Sim' />
174-
<meta itemProp='inLanguage' content='en-US' />
175-
<meta itemProp='keywords' content={post.tags.join(', ')} />
176-
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
177164
</article>
178165
)
179166
}

‎apps/sim/app/(landing)/customers/[slug]/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export async function generateMetadata({ params }: CustomerPageProps): Promise<M
2121
const { slug } = await params
2222
if (!CUSTOMER_STORIES.some((story) => story.slug === slug)) return {}
2323
const post = await getCustomerStoryBySlug(slug)
24-
return post ? buildPostMetadata(post) : {}
24+
return post ? buildPostMetadata(post, CUSTOMER_SECTION) : {}
2525
}
2626

2727
export default async function Page({ params }: CustomerPageProps) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { NextRequest } from 'next/server'
2+
import type { ContentMeta } from '@/lib/content/schema'
3+
import { toSiteUrl } from '@/lib/core/utils/urls'
4+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
5+
import { getAllPostMeta, getPostSource } from '@/lib/library/registry'
6+
import { LIBRARY_SECTION } from '@/lib/library/seo'
7+
import { markdownResponse, section, toLlmsMarkdown } from '@/app/llms.txt/llms'
8+
9+
type RouteContext = { params: Promise<{ slug: string }> }
10+
11+
const NOT_FOUND_MARKDOWN = `# Article not found
12+
13+
There is no library article at this address. Browse the [library](${toSiteUrl(LIBRARY_SECTION.basePath)}) or the [llms.txt](${toSiteUrl('/llms.txt')}) index.
14+
`
15+
16+
function postMarkdownBlocks(post: ContentMeta, source: string): string[] {
17+
const authors = (post.authors.length > 0 ? post.authors : [post.author]).map((a) => a.name)
18+
const published = post.date.slice(0, 10)
19+
const updated = (post.updated ?? post.date).slice(0, 10)
20+
return [
21+
`# ${post.title}`,
22+
`> ${post.description}`,
23+
[
24+
`URL: ${post.canonical}`,
25+
`By ${authors.join(', ')} · Published ${published}${updated !== published ? ` · Updated ${updated}` : ''}`,
26+
].join('\n'),
27+
toLlmsMarkdown(source, 0),
28+
section(
29+
'FAQ',
30+
(post.faq ?? []).map((item) => `### ${item.q}\n\n${item.a}`),
31+
'\n\n'
32+
),
33+
]
34+
}
35+
36+
/**
37+
* A library article as Markdown, at `/library/<slug>.md` (and at `/library/<slug>` for a client
38+
* whose `Accept` names `text/markdown`), via the rewrites in `next.config.ts`. Drafts and unknown
39+
* slugs are a Markdown 404. Uncached, like the HTML post page: the same body answers at the post's
40+
* own URL, so a cache that ignores `Vary` must never keep it.
41+
*/
42+
export const GET = withRouteHandler(async (_request: NextRequest, context: RouteContext) => {
43+
const { slug } = await context.params
44+
const [posts, source] = await Promise.all([getAllPostMeta(), getPostSource(slug)])
45+
const post = posts.find((candidate) => candidate.slug === slug)
46+
const response =
47+
post && source !== null
48+
? markdownResponse(postMarkdownBlocks(post, source), 0)
49+
: new Response(NOT_FOUND_MARKDOWN, {
50+
status: 404,
51+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
52+
})
53+
response.headers.set('Cache-Control', 'no-store')
54+
response.headers.set('Vary', 'Accept')
55+
if (post) response.headers.set('Link', `<${post.canonical}>; rel="canonical"`)
56+
return response
57+
})

‎apps/sim/app/llms.txt/route.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export async function GET() {
7979
),
8080
]),
8181
section('Library', [
82+
'Each article is also served as Markdown at its URL with `.md` appended.',
83+
'',
8284
linkLine('Library', '/library', 'Comparisons, how-tos, and roundups on building AI agents'),
8385
...libraryPosts.map((p) => linkLine(p.title, p.canonical, p.description)),
8486
]),

‎apps/sim/lib/blog/seo.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const BLOG_SECTION: ContentSection = {
1717
name: 'Blog',
1818
basePath: '/blog',
1919
feedPath: '/blog/rss.xml',
20+
hasAuthorPages: true,
2021
description: 'Announcements, insights, and guides for building AI agents.',
2122
}
2223

‎apps/sim/lib/content/faq.tsx‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
export function FAQ({ items }: { items: { q: string; a: string }[] }) {
22
if (!items || items.length === 0) return null
33
return (
4-
<section className='mt-12' itemScope itemType='https://schema.org/FAQPage'>
4+
<section className='mt-12'>
55
<h2 className='mb-4 font-medium text-[24px] text-[var(--text-primary)]'>FAQ</h2>
66
<div className='space-y-6'>
77
{items.map((it, i) => (
8-
<div key={i} itemScope itemType='https://schema.org/Question' itemProp='mainEntity'>
9-
<h3 className='mb-2 font-medium text-[20px] text-[var(--text-primary)]' itemProp='name'>
10-
{it.q}
11-
</h3>
12-
<div itemScope itemType='https://schema.org/Answer' itemProp='acceptedAnswer'>
13-
<p className='text-[19px] text-[var(--text-subtle)] leading-relaxed' itemProp='text'>
14-
{it.a}
15-
</p>
16-
</div>
8+
<div key={i}>
9+
<h3 className='mb-2 font-medium text-[20px] text-[var(--text-primary)]'>{it.q}</h3>
10+
<p className='text-[19px] text-[var(--text-subtle)] leading-relaxed'>{it.a}</p>
1711
</div>
1812
))}
1913
</div>

0 commit comments

Comments
 (0)