From 36953f8cd3d9783e88aa5abe1726289db3deddb8 Mon Sep 17 00:00:00 2001 From: ahmedsadid Date: Tue, 23 Jun 2026 06:09:20 -0400 Subject: [PATCH 1/3] [docs] Fix SEO preview: use absolute URLs for social preview images Co-Authored-By: Claude Opus 4.8 --- packages/docs/src/lib/rss.ts | 8 ++++---- packages/docs/src/lib/site.ts | 12 ++++++++++++ packages/docs/src/pages/_layout.tsx | 16 ++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 packages/docs/src/lib/site.ts diff --git a/packages/docs/src/lib/rss.ts b/packages/docs/src/lib/rss.ts index eb9b8d68a..7652214cd 100644 --- a/packages/docs/src/lib/rss.ts +++ b/packages/docs/src/lib/rss.ts @@ -6,13 +6,14 @@ */ import { Feed } from 'feed'; import { blogSource } from '@/lib/source'; +import { SITE_URL } from '@/lib/site'; import { marked } from 'marked'; import type { InferPageType } from 'fumadocs-core/source'; const logoUrl = '/stylex-logo-small.svg'; const faviconUrl = '/favicon.svg'; -const baseUrl = 'https://stylexjs.com'; +const baseUrl = SITE_URL; const AUTHORS: Record = { mellyeliu: { @@ -44,8 +45,8 @@ async function createFeed(): Promise { const feed = new Feed({ title: 'StyleX Blog', description: 'The latest news and updates about StyleX.', - id: `https://stylexjs.com/blog`, - link: `https://stylexjs.com/blog`, + id: `${baseUrl}/blog`, + link: `${baseUrl}/blog`, language: 'en', image: `${baseUrl}${logoUrl}`, favicon: `${baseUrl}${faviconUrl}`, @@ -107,4 +108,3 @@ export async function getAtom(): Promise { const feed = await getFeed(); return feed.atom1(); } - diff --git a/packages/docs/src/lib/site.ts b/packages/docs/src/lib/site.ts new file mode 100644 index 000000000..f358f93a3 --- /dev/null +++ b/packages/docs/src/lib/site.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Canonical origin for the site, used to build absolute URLs. Single source of +// truth shared by the SEO/social meta tags and the RSS feed. Crawlers (Open +// Graph, Twitter/X cards) require absolute image and page URLs — a bundler +// relative path like "/assets/…png" does not resolve in link previews. +export const SITE_URL = 'https://stylexjs.com'; diff --git a/packages/docs/src/pages/_layout.tsx b/packages/docs/src/pages/_layout.tsx index 7c7194f23..185bdacff 100644 --- a/packages/docs/src/pages/_layout.tsx +++ b/packages/docs/src/pages/_layout.tsx @@ -9,6 +9,7 @@ import { Provider } from '@/components/provider'; import '@/styles/globals.css'; import DevStyleXHMR from '@/components/DevStyleXHMR'; import { SidebarProvider } from '@/contexts/SidebarContext'; +import { SITE_URL } from '@/lib/site'; import coverImageUrl from '@/static/img/stylex-cover-photo.png'; const faviconUrl = '/favicon.svg'; @@ -16,6 +17,11 @@ const faviconUrl = '/favicon.svg'; const DEFAULT_TITLE = 'StyleX — The styling system for ambitious interfaces'; const DEFAULT_DESCRIPTION = 'The styling system that powers Meta.'; +// `coverImageUrl` is a root-relative bundler path; crawlers need an absolute URL. +const coverImageAbsoluteUrl = `${SITE_URL}${coverImageUrl}`; +const COVER_IMAGE_WIDTH = '1034'; +const COVER_IMAGE_HEIGHT = '548'; + export default function RootLayout({ children }: { children: ReactNode }) { return ( <> @@ -26,11 +32,17 @@ export default function RootLayout({ children }: { children: ReactNode }) { - + + + + + + - + + From c9adf7b80e55c30e4c0f363924100536046a6bd9 Mon Sep 17 00:00:00 2001 From: ahmedsadid Date: Tue, 23 Jun 2026 06:44:28 -0400 Subject: [PATCH 2/3] [docs] Fix SEO preview: add page-specific social card previews Co-Authored-By: Claude Opus 4.8 --- packages/docs/src/components/Seo.tsx | 31 +++++++++++++++++++ packages/docs/src/lib/site.ts | 7 +++++ packages/docs/src/pages/(home)/index.tsx | 2 ++ .../src/pages/(playground)/playground.tsx | 5 +++ packages/docs/src/pages/_layout.tsx | 15 +++------ packages/docs/src/pages/blog/[...slugs].tsx | 5 +++ packages/docs/src/pages/docs/[...slugs].tsx | 5 +++ 7 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 packages/docs/src/components/Seo.tsx diff --git a/packages/docs/src/components/Seo.tsx b/packages/docs/src/components/Seo.tsx new file mode 100644 index 000000000..b6d1ab991 --- /dev/null +++ b/packages/docs/src/components/Seo.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { DEFAULT_TITLE, DEFAULT_DESCRIPTION } from '@/lib/site'; + +// Per-page title/description for SEO and social cards. React hoists these +// tags into . They must be set per-page (not globally in the root layout), +// because React does not de-duplicate meta tags — a global default plus a +// per-page tag would emit two conflicting `og:title`s. Invariant tags +// (og:type, og:image, twitter:card, …) stay in the root layout. +export function Seo({ + title = DEFAULT_TITLE, + description = DEFAULT_DESCRIPTION, +}: { + title?: string; + description?: string; +}) { + return ( + <> + + + + + + + ); +} diff --git a/packages/docs/src/lib/site.ts b/packages/docs/src/lib/site.ts index f358f93a3..291ee10c1 100644 --- a/packages/docs/src/lib/site.ts +++ b/packages/docs/src/lib/site.ts @@ -10,3 +10,10 @@ // Graph, Twitter/X cards) require absolute image and page URLs — a bundler // relative path like "/assets/…png" does not resolve in link previews. export const SITE_URL = 'https://stylexjs.com'; + +export const SITE_NAME = 'StyleX'; + +// Fallback title/description for pages that don't provide their own. +export const DEFAULT_TITLE = + 'StyleX — The styling system for ambitious interfaces'; +export const DEFAULT_DESCRIPTION = 'The styling system that powers Meta.'; diff --git a/packages/docs/src/pages/(home)/index.tsx b/packages/docs/src/pages/(home)/index.tsx index 4c8b5f581..7493cac2e 100644 --- a/packages/docs/src/pages/(home)/index.tsx +++ b/packages/docs/src/pages/(home)/index.tsx @@ -10,12 +10,14 @@ import StylexAnimatedLogo from '@/components/StylexAnimatedLogo'; import CtaButton from '@/components/CtaButton'; import TypingWord from '@/components/TypingWord'; import Footer from '@/components/Footer'; +import { Seo } from '@/components/Seo'; import { vars } from '@/theming/vars.stylex'; export default function Home() { return ( <> StyleX — styling system for ambitious interfaces +

diff --git a/packages/docs/src/pages/(playground)/playground.tsx b/packages/docs/src/pages/(playground)/playground.tsx index a0c7c9c75..a0c089432 100644 --- a/packages/docs/src/pages/(playground)/playground.tsx +++ b/packages/docs/src/pages/(playground)/playground.tsx @@ -8,12 +8,17 @@ import * as stylex from '@stylexjs/stylex'; import { Playground } from '@/components/Playground/DynamicPlayground'; import Footer from '@/components/Footer'; +import { Seo } from '@/components/Seo'; import { vars } from '@/theming/vars.stylex'; export default function PlaygroundPage() { return ( <> Playground | StyleX +
diff --git a/packages/docs/src/pages/_layout.tsx b/packages/docs/src/pages/_layout.tsx index 185bdacff..42869852c 100644 --- a/packages/docs/src/pages/_layout.tsx +++ b/packages/docs/src/pages/_layout.tsx @@ -9,38 +9,33 @@ import { Provider } from '@/components/provider'; import '@/styles/globals.css'; import DevStyleXHMR from '@/components/DevStyleXHMR'; import { SidebarProvider } from '@/contexts/SidebarContext'; -import { SITE_URL } from '@/lib/site'; +import { SITE_URL, SITE_NAME, DEFAULT_TITLE } from '@/lib/site'; import coverImageUrl from '@/static/img/stylex-cover-photo.png'; const faviconUrl = '/favicon.svg'; -const DEFAULT_TITLE = 'StyleX — The styling system for ambitious interfaces'; -const DEFAULT_DESCRIPTION = 'The styling system that powers Meta.'; - // `coverImageUrl` is a root-relative bundler path; crawlers need an absolute URL. const coverImageAbsoluteUrl = `${SITE_URL}${coverImageUrl}`; const COVER_IMAGE_WIDTH = '1034'; const COVER_IMAGE_HEIGHT = '548'; +// Only site-wide invariant tags live here. Per-page title/description tags +// (description, og:title, og:description, twitter:title/description) are rendered +// per-page via the component, since React does not de-duplicate meta tags. export default function RootLayout({ children }: { children: ReactNode }) { return ( <> - - - - + - - diff --git a/packages/docs/src/pages/blog/[...slugs].tsx b/packages/docs/src/pages/blog/[...slugs].tsx index 6089bb0f6..fcd92bf15 100644 --- a/packages/docs/src/pages/blog/[...slugs].tsx +++ b/packages/docs/src/pages/blog/[...slugs].tsx @@ -14,6 +14,7 @@ import { DocsTitle, } from '@/components/layout/page'; import { baseOptions } from '@/lib/layout.shared'; +import { Seo } from '@/components/Seo'; import { mdxComponents } from '@/components/mdx'; import nmnImage from '@/static/img/nmn.jpg'; import necolasImage from '@/static/img/necolas.jpg'; @@ -52,6 +53,10 @@ export default function BlogPage({ slugs }: PageProps<'/blog/[...slugs]'>) { // breadcrumb={{ enabled: true }} > {`${page.data.title} | StyleX`} + {page.data.title}{' '} {/* diff --git a/packages/docs/src/pages/docs/[...slugs].tsx b/packages/docs/src/pages/docs/[...slugs].tsx index 43e5914e0..5ad352056 100644 --- a/packages/docs/src/pages/docs/[...slugs].tsx +++ b/packages/docs/src/pages/docs/[...slugs].tsx @@ -13,6 +13,7 @@ import { DocsTitle, } from '@/components/layout/page'; import { mdxComponents } from '@/components/mdx'; +import { Seo } from '@/components/Seo'; import * as stylex from '@stylexjs/stylex'; import { vars } from '@/theming/vars.stylex'; @@ -34,6 +35,10 @@ export default function DocPage({ slugs }: PageProps<'/docs/[...slugs]'>) { return ( {`${page.data.title} | StyleX`} + {slugs.length > 1 && slugs[0] === 'api' ? ( {page.data.title} From 3da092889ae7d97031aafaa623dbe4459ac8b920 Mon Sep 17 00:00:00 2001 From: ahmedsadid Date: Tue, 23 Jun 2026 06:57:46 -0400 Subject: [PATCH 3/3] [docs] Fix SEO preview: derive blog post descriptions from content Co-Authored-By: Claude Opus 4.8 --- packages/docs/src/lib/excerpt.ts | 41 +++++++++++++++++++ .../src/pages/(playground)/playground.tsx | 2 +- packages/docs/src/pages/blog/[...slugs].tsx | 13 +++--- packages/docs/src/pages/docs/[...slugs].tsx | 2 +- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 packages/docs/src/lib/excerpt.ts diff --git a/packages/docs/src/lib/excerpt.ts b/packages/docs/src/lib/excerpt.ts new file mode 100644 index 000000000..b9d75cb23 --- /dev/null +++ b/packages/docs/src/lib/excerpt.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Derive a plain-text excerpt from markdown, for use as a meta/social +// description when a page has no explicit `description`. Accumulates the leading +// content paragraphs (skipping headings and code blocks) until the length limit, +// strips common markdown syntax, and truncates on a word boundary. +export function getExcerpt(markdown: string, maxLength = 200): string { + const paragraphs = markdown + .split(/\n\s*\n/) + .map((block) => block.trim()) + .filter( + (block) => + block.length > 0 && !block.startsWith('#') && !block.startsWith('```'), + ); + + let collected = ''; + for (const paragraph of paragraphs) { + collected = collected ? `${collected} ${paragraph}` : paragraph; + if (collected.length >= maxLength) break; + } + + const plain = collected + .replace(/`([^`]+)`/g, '$1') // inline code + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // links -> link text + .replace(/[*_~>#]/g, '') // emphasis / blockquote / heading markers + .replace(/\s+/g, ' ') + .trim(); + + if (plain.length <= maxLength) return plain; + return ( + plain + .slice(0, maxLength) + .replace(/\s+\S*$/, '') + .trimEnd() + '…' + ); +} diff --git a/packages/docs/src/pages/(playground)/playground.tsx b/packages/docs/src/pages/(playground)/playground.tsx index a0c089432..e87820c54 100644 --- a/packages/docs/src/pages/(playground)/playground.tsx +++ b/packages/docs/src/pages/(playground)/playground.tsx @@ -16,8 +16,8 @@ export default function PlaygroundPage() { <> Playground | StyleX
diff --git a/packages/docs/src/pages/blog/[...slugs].tsx b/packages/docs/src/pages/blog/[...slugs].tsx index fcd92bf15..c8c9d9ab1 100644 --- a/packages/docs/src/pages/blog/[...slugs].tsx +++ b/packages/docs/src/pages/blog/[...slugs].tsx @@ -14,6 +14,7 @@ import { DocsTitle, } from '@/components/layout/page'; import { baseOptions } from '@/lib/layout.shared'; +import { getExcerpt } from '@/lib/excerpt'; import { Seo } from '@/components/Seo'; import { mdxComponents } from '@/components/mdx'; import nmnImage from '@/static/img/nmn.jpg'; @@ -22,7 +23,9 @@ import mellyeliuImage from '@/static/img/mellyeliu.jpg'; import vincentriemerImage from '@/static/img/vincentriemer.png'; import { vars } from '@/theming/vars.stylex'; -export default function BlogPage({ slugs }: PageProps<'/blog/[...slugs]'>) { +export default async function BlogPage({ + slugs, +}: PageProps<'/blog/[...slugs]'>) { const pages = blogSource.getPages(); const slug = slugs[0]; @@ -44,6 +47,9 @@ export default function BlogPage({ slugs }: PageProps<'/blog/[...slugs]'>) { const authors = page.data.authors.map( (author) => AUTHORS[author as keyof typeof AUTHORS], ); + // Blog posts have no `description` frontmatter; derive one from the content. + const description = + page.data.description || getExcerpt(await page.data.getText('processed')); return ( ) { // breadcrumb={{ enabled: true }} > {`${page.data.title} | StyleX`} - + {page.data.title}{' '} {/* diff --git a/packages/docs/src/pages/docs/[...slugs].tsx b/packages/docs/src/pages/docs/[...slugs].tsx index 5ad352056..b778d12fc 100644 --- a/packages/docs/src/pages/docs/[...slugs].tsx +++ b/packages/docs/src/pages/docs/[...slugs].tsx @@ -36,8 +36,8 @@ export default function DocPage({ slugs }: PageProps<'/docs/[...slugs]'>) { {`${page.data.title} | StyleX`} {slugs.length > 1 && slugs[0] === 'api' ? (