Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/docs/src/components/Seo.tsx
Original file line number Diff line number Diff line change
@@ -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 <meta>
// tags into <head>. 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 (
<>
<meta content={description} name="description" />
<meta content={title} property="og:title" />
<meta content={description} property="og:description" />
<meta content={title} name="twitter:title" />
<meta content={description} name="twitter:description" />
</>
);
}
41 changes: 41 additions & 0 deletions packages/docs/src/lib/excerpt.ts
Original file line number Diff line number Diff line change
@@ -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() + '…'
);
}
8 changes: 4 additions & 4 deletions packages/docs/src/lib/rss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { name: string; link: string }> = {
mellyeliu: {
Expand Down Expand Up @@ -44,8 +45,8 @@ async function createFeed(): Promise<Feed> {
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}`,
Expand Down Expand Up @@ -107,4 +108,3 @@ export async function getAtom(): Promise<string> {
const feed = await getFeed();
return feed.atom1();
}

19 changes: 19 additions & 0 deletions packages/docs/src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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';

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.';
2 changes: 2 additions & 0 deletions packages/docs/src/pages/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<title>StyleX — styling system for ambitious interfaces</title>
<Seo />
<main {...stylex.props(styles.main)}>
<section {...stylex.props(styles.hero)}>
<h1 {...stylex.props(styles.title)}>
Expand Down
5 changes: 5 additions & 0 deletions packages/docs/src/pages/(playground)/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<title>Playground | StyleX</title>
<Seo
description="Try StyleX in your browser — edit styles and see the compiled output live."
title="Playground"
/>
<main {...stylex.props(styles.main)}>
<Playground />
</main>
Expand Down
25 changes: 16 additions & 9 deletions packages/docs/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,35 @@ import { Provider } from '@/components/provider';
import '@/styles/globals.css';
import DevStyleXHMR from '@/components/DevStyleXHMR';
import { SidebarProvider } from '@/contexts/SidebarContext';
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 <Seo> component, since React does not de-duplicate meta tags.
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<>
<head>
<meta charSet="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta content={DEFAULT_DESCRIPTION} name="description" />
<meta content={DEFAULT_TITLE} property="og:title" />
<meta content={DEFAULT_DESCRIPTION} property="og:description" />
<meta content="website" property="og:type" />
<meta content={coverImageUrl} property="og:image" />
<meta content={SITE_URL} property="og:url" />
<meta content={SITE_NAME} property="og:site_name" />
<meta content={coverImageAbsoluteUrl} property="og:image" />
<meta content={COVER_IMAGE_WIDTH} property="og:image:width" />
<meta content={COVER_IMAGE_HEIGHT} property="og:image:height" />
<meta content={DEFAULT_TITLE} property="og:image:alt" />
<meta content="summary_large_image" name="twitter:card" />
<meta content={DEFAULT_TITLE} name="twitter:title" />
<meta content={DEFAULT_DESCRIPTION} name="twitter:description" />
<meta content={coverImageUrl} name="twitter:image" />
<meta content={coverImageAbsoluteUrl} name="twitter:image" />
<meta content={DEFAULT_TITLE} name="twitter:image:alt" />
<link href={faviconUrl} rel="icon" sizes="any" />
<link href={faviconUrl} rel="icon" type="image/svg+xml" />
<link href={faviconUrl} rel="shortcut icon" />
Expand Down
10 changes: 9 additions & 1 deletion packages/docs/src/pages/blog/[...slugs].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ 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';
import necolasImage from '@/static/img/necolas.jpg';
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];
Expand All @@ -43,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 (
<DocsPage
Expand All @@ -52,6 +59,7 @@ export default function BlogPage({ slugs }: PageProps<'/blog/[...slugs]'>) {
// breadcrumb={{ enabled: true }}
>
<title>{`${page.data.title} | StyleX`}</title>
<Seo description={description} title={page.data.title} />
<DocsTitle>
{page.data.title}{' '}
{/* <code>
Expand Down
5 changes: 5 additions & 0 deletions packages/docs/src/pages/docs/[...slugs].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -34,6 +35,10 @@ export default function DocPage({ slugs }: PageProps<'/docs/[...slugs]'>) {
return (
<DocsPage toc={page.data.toc}>
<title>{`${page.data.title} | StyleX`}</title>
<Seo
description={page.data.description || undefined}
title={page.data.title}
/>
<DocsTitle>
{slugs.length > 1 && slugs[0] === 'api' ? (
<code {...stylex.props(styles.codeTitle)}>{page.data.title}</code>
Expand Down
Loading