feat(site): framework-specific SEO metadata for docs (#541)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-02-18 11:21:15 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 78e8e3a8e1
commit e936ea4e44
22 changed files with 160 additions and 30 deletions
+1
View File
@@ -37,6 +37,7 @@ This skill adds writing guidance on top of those. When in doubt, the source file
| Package README from scratch | `templates/readme.md` |
| Code example conventions | `patterns/code-examples.md` |
| Error documentation patterns | `patterns/error-docs.md` |
| SEO metadata and keyword targeting | `references/seo.md` |
| Review workflow (multi-agent) | `review/workflow.md` |
| Review checklist (single-agent) | `review/checklist.md` |
| Review agent prompts | `review/agents.md` |
+111
View File
@@ -0,0 +1,111 @@
# SEO Metadata Conventions
Conventions for page titles, meta descriptions, and structured data across the Video.js site.
## Target keywords
These are the search terms we optimize for. Work them into titles, descriptions, body content, and anchor text where they fit naturally.
| Priority | Keyword | Where it appears |
| -------- | ----------------------------- | -------------------------------------------------------------- |
| High | **open source video player** | Every page title suffix, homepage |
| High | **React video player** | React docs title suffix, installation intro, meta descriptions |
| High | **HTML video player** | HTML docs title suffix, installation intro, meta descriptions |
| Medium | **video player** | Site description, body content, anchor text |
| Medium | **video player components** | Installation page, component reference descriptions |
| Medium | **accessible video player** | Component descriptions, installation intro |
| Medium | **video player performance** | Site description, architecture page, installation description |
| Medium | **lightweight video player** | Site description, installation intro |
| Medium | **fast video player** | Architecture page, performance-related content |
| Low | **streaming** | Site description, installation description |
| Low | **customizable video player** | Component descriptions, skins pages |
**Framework-specific keywords** are the highest priority for docs pages because each framework gets its own URL (`/docs/framework/react/...` and `/docs/framework/html/...`), creating natural keyword-targeting opportunities.
**Performance keywords** are natural fits — v10 is built around minimal bundle sizes, CSS-driven animations, and smooth rendering. Use "performance", "lightweight", and "fast" in descriptions for architecture, installation, and component pages.
## Page titles
Pattern: `PAGE | Video.js | Open Source [Framework] Video Player`
| Context | Format | Example |
| ------------ | ---------------------------------------------------- | ---------------------------------------------------------------- |
| Docs (React) | `Page \| Video.js \| Open Source React Video Player` | `PlayButton \| Video.js \| Open Source React Video Player` |
| Docs (HTML) | `Page \| Video.js \| Open Source HTML Video Player` | `media-play-button \| Video.js \| Open Source HTML Video Player` |
| Non-docs | `Page \| Video.js \| Open Source Video Player` | `Blog \| Video.js \| Open Source Video Player` |
| Homepage | `Video.js \| Open Source Video Player` | — |
### How it works
- `SITE_TITLE` (`Video.js`) — brand segment, always present
- `SEO_SUFFIX` (`Open Source Video Player`) — default suffix for non-docs pages
- `Base.astro` accepts an optional `suffix` prop (defaults to `SEO_SUFFIX`)
- `Docs.astro` overrides suffix with `Open Source ${FRAMEWORK_LABELS[framework]} Video Player`
- Title construction: `pageTitle | SITE_TITLE | suffix` (or just `SITE_TITLE | suffix` for homepage)
### Guidelines
- Keep total title under ~60 characters — Google truncates beyond that
- Target keywords should appear before the truncation point
- Don't add "Video.js" or "video player" to the page-specific segment — the suffix handles it
## Meta descriptions
### Docs pages
Docs descriptions come from MDX frontmatter `description` + an automatic framework suffix appended by `Docs.astro`:
```
${description} — Video.js ${Framework} Video Player.
```
**Writing good descriptions:**
- Describe the benefit, not just the feature: "Accessible play/pause button with keyboard support" not "A button component for playing and pausing"
- Don't include "Video.js" or version numbers — the suffix handles branding
- Keep the frontmatter description under ~120 chars so the full description (with suffix) stays under ~160 chars
- Use action words: "Accessible", "Customizable", "Composable", "Lightweight", "Fast", "Performant"
### Non-docs pages
Set directly in the page or layout. Include "video player" and relevant keywords naturally.
## Body content keywords
The installation page (`how-to/installation.mdx`) uses `<FrameworkCase>` to show framework-specific intro text with target keywords:
- **React:** "React video player component library"
- **HTML:** "HTML video player built on custom elements"
When adding new high-traffic landing pages, include framework-specific keywords in body content — not just metadata.
## Structured data (JSON-LD)
`createTechArticleSchema` in `site/src/utils/jsonLd/schemas.ts` generates JSON-LD for docs pages. It includes:
```ts
about: {
'@type': 'SoftwareApplication',
name: 'Video.js',
applicationCategory: 'MultimediaApplication',
operatingSystem: 'Web',
}
```
This helps search engines categorize docs as documentation for a specific software product.
## Internal linking
Use keyword-rich anchor text for internal links instead of generic text:
| Instead of | Use |
| -------------------- | --------------------------------------------------------- |
| "click here" | descriptive text about the target page |
| "learn more" | what the reader will learn, e.g. "customize player skins" |
| bare component names | "video player controls" or "PlayButton component" |
High-value linking opportunities:
- Homepage → framework installation pages (anchor text: "React video player", "HTML video player")
- Cross-links between related component reference pages
- Links from concept pages to relevant reference pages with descriptive text
+4
View File
@@ -889,6 +889,10 @@ devOnly: true
---
```
## SEO Metadata
When writing content for the site, especially page titles and descriptions, follow the conventions in `.claude/skills/docs/references/seo.md` for the full keyword list and guidelines.
## Common Tasks
### Adding a New Docs Guide
+3 -2
View File
@@ -1,5 +1,6 @@
export const SITE_TITLE = 'Video.js 10';
export const SITE_DESCRIPTION = `For over 15 years, Video.js has been the world's web video player. Now rebuilt in v10 for modern development and performance.`;
export const SITE_TITLE = 'Video.js';
export const SEO_SUFFIX = 'Open Source Video Player';
export const SITE_DESCRIPTION = `The open-source video player for React and HTML. Lightweight, accessible components built for performance and streaming.`;
export const GITHUB_REPO_URL = 'https://github.com/videojs/v10/';
export const DISCORD_INVITE_URL = 'https://discord.gg/JBqHh485uF';
export const THEME_KEY = 'vjs-site-theme';
@@ -1,6 +1,6 @@
---
title: Architecture
description: Understanding the three-tier architecture of Video.js v10 - State, UI, and Renderers
description: How Video.js structures its video player — state management, UI components, and platform renderers
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: Skins
description: Understanding skins in Video.js v10 - packaged sets of UI components and styles
description: Packaged sets of video player UI components and styles that you can customize or replace
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
@@ -1,6 +1,6 @@
---
title: UI components
description: Understanding Video.js v10's primitive-based component architecture
description: Primitive-based component architecture for building accessible, composable video player controls
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
@@ -1,6 +1,6 @@
---
title: Installation
description: Get started quickly with Video.js by building your first embed code
description: Install Video.js and build your first player with streaming support and accessible controls
---
import JSPicker from '@/components/installation/JSPicker.astro';
@@ -21,7 +21,13 @@ import { TabsRoot, TabsList, TabsPanel, Tab } from '@/components/Tabs.tsx';
Video.js v10 is currently in _beta_. The API may evolve with [feedback&#x1F64F;](https://github.com/videojs/v10/issues). See the [Changelog](https://github.com/videojs/v10/blob/main/CHANGELOG.md)\[todo\] for recent updates and the <DocsLink slug="concepts/v10-roadmap">Roadmap</DocsLink> for more details on what's coming.
</Aside>
Video.js is **audio and video player components** &mdash; intentionally built to achieve **minimal bundle sizes** while providing **framework-specific customization** and a **great viewer experience**.
<FrameworkCase frameworks={["react"]}>
Video.js is a **React video player component library** &mdash; composable primitives, hooks, and TypeScript types for building accessible, customizable players with minimal bundle size.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
Video.js is an **HTML video player built on custom elements** &mdash; lightweight, framework-free components for building accessible, customizable players with minimal bundle size.
</FrameworkCase>
Answer the questions below to get started quickly with your first embed code.
## Choose your JS framework
@@ -2,7 +2,7 @@
title: BufferingIndicator
frameworkTitle:
html: media-buffering-indicator
description: A component that displays a loading indicator when media is buffering
description: Loading indicator that displays when the video player is buffering or waiting for data
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
+1 -1
View File
@@ -2,7 +2,7 @@
title: Controls
frameworkTitle:
html: media-controls
description: Containers for composing and auto-hiding player controls
description: Container component for composing and auto-hiding video player controls on user interaction
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
@@ -2,7 +2,7 @@
title: FullscreenButton
frameworkTitle:
html: media-fullscreen-button
description: A button component for entering and exiting fullscreen mode
description: Accessible fullscreen toggle button with keyboard support and state reflection
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
@@ -2,7 +2,7 @@
title: MuteButton
frameworkTitle:
html: media-mute-button
description: A button component for muting and unmuting audio playback
description: Accessible mute/unmute button with keyboard support and volume state reflection
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
@@ -2,7 +2,7 @@
title: PiPButton
frameworkTitle:
html: media-pip-button
description: A button component for entering and exiting picture-in-picture mode
description: Accessible picture-in-picture toggle button with keyboard support and state reflection
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
@@ -2,7 +2,7 @@
title: PlayButton
frameworkTitle:
html: media-play-button
description: A button component for playing and pausing media playback
description: Accessible play/pause button with keyboard support and customizable rendering
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
+1 -1
View File
@@ -2,7 +2,7 @@
title: Poster
frameworkTitle:
html: media-poster
description: Poster image component that stays visible until playback starts
description: Poster image component that displays a thumbnail until video playback starts
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
@@ -2,7 +2,7 @@
title: SeekButton
frameworkTitle:
html: media-seek-button
description: A button component for seeking media playback forward or backward by a specified number of seconds
description: Accessible seek button for skipping forward or backward by a configurable number of seconds
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
+1 -1
View File
@@ -2,7 +2,7 @@
title: Time
frameworkTitle:
html: media-time
description: Components for displaying and composing media time information
description: Time display components for showing current time, duration, and remaining time in a video player
---
import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
+7 -7
View File
@@ -4,7 +4,7 @@
import { Font } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { SITE_TITLE } from '@/consts';
import { SEO_SUFFIX, SITE_TITLE } from '@/consts';
import '@/styles/globals.css';
@@ -17,17 +17,17 @@ interface Props {
description: string;
image?: ImageMetadata;
canonical?: string;
suffix?: string;
}
const { canonical } = Astro.props;
const { canonical, suffix } = Astro.props;
const canonicalURL = canonical || new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props;
const fullTitle = Array.isArray(title)
? `${title.filter((t) => t !== SITE_TITLE).join(' | ')} | ${SITE_TITLE}`
: title === SITE_TITLE
? title
: `${title} | ${SITE_TITLE}`;
const seoSuffix = suffix ?? SEO_SUFFIX;
const pageTitle = Array.isArray(title) ? title.join(' | ') : title;
const fullTitle =
pageTitle === SITE_TITLE ? `${SITE_TITLE} | ${seoSuffix}` : `${pageTitle} | ${SITE_TITLE} | ${seoSuffix}`;
---
<html lang="en">
+3 -2
View File
@@ -9,7 +9,7 @@ import FilmGrain from '@/components/FilmGrain';
import Footer from '@/components/Footer.astro';
import FooterEasterEgg from '@/components/FooterEasterEgg.astro';
import NavBar from '@/components/NavBar/NavBar.astro';
import type { SupportedFramework } from '@/types/docs';
import { FRAMEWORK_LABELS, type SupportedFramework } from '@/types/docs';
import { getDocTitle } from '@/utils/docs/title';
import Base from './Base.astro';
@@ -25,10 +25,11 @@ const { doc, framework, slug } = Astro.props;
const allDocs = await getCollection('docs');
const docTitles = new Map(allDocs.map((d) => [d.id, getDocTitle(d, framework)]));
const seoCategory = `${FRAMEWORK_LABELS[framework]} Video Player`;
const docsSidebarId = 'docs-sidebar';
---
<Base title={[getDocTitle(doc, framework), 'Docs']} description={doc.data.description}>
<Base title={getDocTitle(doc, framework)} suffix={`Open Source ${FRAMEWORK_LABELS[framework]} Video Player`} description={`${doc.data.description} — Video.js ${seoCategory}.`}>
<Fragment slot="priority-head">
<StyleInit />
</Fragment>
+2 -2
View File
@@ -44,12 +44,12 @@ const jsonLdSchema = createBlogCollectionSchema({
});
---
<Blog title="Blog" description="Articles, announcements, news, updates, and more">
<Blog title="Blog" description="News and updates from the Video.js open-source video player project">
<JsonLd slot="head" schema={jsonLdSchema} />
<header class="mb-10 md:mb-20 w-full max-w-3xl mx-auto">
<h1 class="text-h4 md:text-h2 mb-2">Video.js Blog</h1>
<p class="text-lg font-medium md:text-h5 text-dark-40 dark:text-light-40">
{page.data.length === 0 ? 'Coming soon' : 'Articles, announcements, news, updates, and more'}
{page.data.length === 0 ? 'Coming soon' : 'News and updates from the Video.js open-source video player project'}
</p>
</header>
<div class="grid gap-10 w-full max-w-3xl mx-auto">
+2 -2
View File
@@ -25,8 +25,8 @@ const poster = `${VJS8_DEMO_VIDEO.poster}?time=0`;
>
<h1 class="text-h4 md:text-h1 text-balance lg:text-pretty">The Open Source Player for the Web</h1>
<p class="md:text-lg text-balance max-w-100">
For over 15 years, Video.js has been the worlds web video player. Now rebuilt in v10 for modern development
and performance.
The open-source video player for React and HTML. Lightweight, accessible components built for performance and
streaming.
</p>
</div>
<div class="vjs relative z-10 w-full -mb-11 md:-mb-21 lg:-mb-32 grid grid-cols-1 grid-rows-1">
+7 -1
View File
@@ -30,6 +30,12 @@ export function createTechArticleSchema(params: {
...(params.wordCount && { wordCount: params.wordCount }),
...(params.readingTime && { timeRequired: `PT${params.readingTime}M` }),
...(params.articleSection && { articleSection: params.articleSection }),
about: {
'@type': 'SoftwareApplication',
name: 'Video.js',
applicationCategory: 'MultimediaApplication',
operatingSystem: 'Web',
},
author: {
'@type': 'Organization',
name: 'Video.js',
@@ -112,7 +118,7 @@ export function createBlogCollectionSchema(params: {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Video.js Blog',
description: 'Articles, announcements, news, updates, and more',
description: 'News and updates from the Video.js open-source video player project',
url: params.url,
mainEntity: {
'@type': 'ItemList',