mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): add optional OG image support to blog posts (#878)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
57ab4b2cdb
commit
156986abb9
@@ -421,9 +421,16 @@ All content must be written in **MDX format** to support:
|
||||
authors: string[]; // Reference to authors.json
|
||||
canonical?: string; // Canonical URL override
|
||||
devOnly?: boolean; // Show only in development
|
||||
ogImage?: ImageMetadata | string; // Local image or external URL
|
||||
twitterImage?: ImageMetadata | string; // Falls back to ogImage
|
||||
}
|
||||
```
|
||||
|
||||
**OG images** go in `src/assets/blog/{date-slug}/og.png`. Reference from frontmatter with a relative path:
|
||||
```yaml
|
||||
ogImage: '../../assets/blog/2026-03-10-my-post/og.png'
|
||||
```
|
||||
|
||||
### Docs Collection (`src/content/docs/`)
|
||||
|
||||
**Subdirectories:**
|
||||
|
||||
@@ -18,7 +18,14 @@ import remarkConditionalHeadings from './src/utils/remarkConditionalHeadings';
|
||||
import { remarkReadingTime } from './src/utils/remarkReadingTime.mjs';
|
||||
import shikiTransformMetadata from './src/utils/shikiTransformMetadata';
|
||||
|
||||
const SITE_URL = 'https://videojs.org';
|
||||
// Astro docs say `site` should be "your final, deployed URL", but we override
|
||||
// it per-deploy so that generated absolute URLs (OG images, RSS, etc.) resolve
|
||||
// correctly on Netlify deploy previews. On production, DEPLOY_PRIME_URL is the
|
||||
// primary site URL so it's equivalent.
|
||||
//
|
||||
// For URLs that must always point to production regardless of deploy context
|
||||
// (e.g. canonical, JSON-LD), use PRODUCTION_URL from src/consts.ts instead.
|
||||
const SITE_URL = process.env.DEPLOY_PRIME_URL || 'https://videojs.org';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,3 +1,7 @@
|
||||
// Always https://videojs.org. Unlike Astro.site, which varies per deploy
|
||||
// (e.g. deploy preview URLs), this is stable for canonical URLs and other
|
||||
// references that must always point to production.
|
||||
export const PRODUCTION_URL = new URL('https://videojs.org');
|
||||
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.`;
|
||||
|
||||
@@ -49,7 +49,7 @@ const blog = defineCollection({
|
||||
},
|
||||
}),
|
||||
// Type-check frontmatter using a schema
|
||||
schema: () =>
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
@@ -58,6 +58,8 @@ const blog = defineCollection({
|
||||
authors: z.array(reference('authors')),
|
||||
canonical: z.string().url().optional(),
|
||||
devOnly: z.boolean().optional(), // only visible in development mode
|
||||
ogImage: image().or(z.string().url()).optional(),
|
||||
twitterImage: image().or(z.string().url()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: 'Video.js v10 Beta: Hello, World (again)'
|
||||
description: "Video.js v10 is a ground-up rewrite combining four open source players into one modern framework — 88% smaller default bundles, first-class React and TypeScript support, composable architecture, and beautiful new skins designed by Plyr's creator."
|
||||
authors: [steve-heffernan]
|
||||
ogImage: '../../assets/blog/2026-03-10-videojs-v10-beta-hello-world-again/og.png'
|
||||
---
|
||||
|
||||
Today we're excited to release the Video.js v10.0.0 beta. It's the result of a rather large ground-up rewrite, not just of Video.js ([discussion](https://github.com/videojs/video.js/discussions/9035)) but also of [Plyr](https://github.com/sampotts/plyr/discussions/2871), [Vidstack](https://github.com/vidstack/player/discussions/1747), and [Media Chrome](https://www.mux.com/blog/from-media-chrome-to-video-js-v10-the-evolution-of-html-first-video-players), through a rare teaming-up of open source projects and people who care a lot about web video, with a combined 75,000 github stars and tens of billions of video plays monthly.
|
||||
|
||||
@@ -3,6 +3,7 @@ title: 'Blog Component Test'
|
||||
description: 'A dev-only blog post for testing blog components and MDX features'
|
||||
authors: [darius-cepulis]
|
||||
devOnly: true
|
||||
ogImage: '../../assets/blog/2099-12-31-blog-component-test/og.png'
|
||||
---
|
||||
|
||||
import Aside from '@/components/Aside.astro'
|
||||
|
||||
File diff suppressed because one or more lines are too long
+23
-13
@@ -2,7 +2,7 @@
|
||||
import { Font } from 'astro:assets';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
|
||||
import { SEO_SUFFIX, SITE_TITLE } from '@/consts';
|
||||
import { PRODUCTION_URL, SEO_SUFFIX, SITE_TITLE } from '@/consts';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
@@ -14,14 +14,22 @@ import ThemeInit from '@/components/ThemeInit.astro';
|
||||
interface Props {
|
||||
title: string | string[];
|
||||
description: string;
|
||||
image?: ImageMetadata;
|
||||
image?: ImageMetadata | string;
|
||||
twitterImage?: ImageMetadata | string;
|
||||
canonical?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
const { canonical, suffix } = Astro.props;
|
||||
const canonicalURL = canonical || new URL(Astro.url.pathname, Astro.site);
|
||||
const canonicalURL = canonical || new URL(Astro.url.pathname, PRODUCTION_URL);
|
||||
|
||||
const { title, description, image } = Astro.props;
|
||||
const { title, description, image, twitterImage } = Astro.props;
|
||||
|
||||
function resolveImageUrl(img: ImageMetadata | string): string {
|
||||
if (typeof img === 'string') return img;
|
||||
return new URL(img.src, Astro.url).toString();
|
||||
}
|
||||
|
||||
const resolvedTwitterImage = twitterImage ?? image;
|
||||
|
||||
const seoSuffix = suffix ?? SEO_SUFFIX;
|
||||
const pageTitle = Array.isArray(title) ? title.join(' | ') : title;
|
||||
@@ -109,25 +117,27 @@ const fullTitle =
|
||||
|
||||
{/* Open Graph / Facebook */}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta
|
||||
property="og:url"
|
||||
content={new URL(Astro.url.pathname, PRODUCTION_URL)}
|
||||
/>
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
{
|
||||
image && (
|
||||
<meta property="og:image" content={new URL(image.src, Astro.url)} />
|
||||
)
|
||||
}
|
||||
{image && <meta property="og:image" content={resolveImageUrl(image)} />}
|
||||
|
||||
{/* Twitter */}
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta
|
||||
property="twitter:url"
|
||||
content={new URL(Astro.url.pathname, PRODUCTION_URL)}
|
||||
/>
|
||||
<meta property="twitter:title" content={fullTitle} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
{
|
||||
image && (
|
||||
resolvedTwitterImage && (
|
||||
<meta
|
||||
property="twitter:image"
|
||||
content={new URL(image.src, Astro.url)}
|
||||
content={resolveImageUrl(resolvedTwitterImage)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import Footer from '@/components/Footer.astro';
|
||||
import FooterEasterEgg from '@/components/FooterEasterEgg.astro';
|
||||
import NavBar from '@/components/NavBar/NavBar.astro';
|
||||
@@ -9,12 +10,20 @@ interface Props {
|
||||
title: string | string[];
|
||||
description: string;
|
||||
canonical?: string;
|
||||
image?: ImageMetadata | string;
|
||||
twitterImage?: ImageMetadata | string;
|
||||
}
|
||||
|
||||
const { title, description, canonical } = Astro.props;
|
||||
const { title, description, canonical, image, twitterImage } = Astro.props;
|
||||
---
|
||||
|
||||
<Base title={title} description={description} canonical={canonical}>
|
||||
<Base
|
||||
title={title}
|
||||
description={description}
|
||||
canonical={canonical}
|
||||
image={image}
|
||||
twitterImage={twitterImage}
|
||||
>
|
||||
<slot name="head" slot="head" />
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<NavBar />
|
||||
|
||||
@@ -7,6 +7,7 @@ import BlogPagination from '@/components/blog/BlogPagination.astro';
|
||||
import BlogPostCard from '@/components/blog/BlogPostCard.astro';
|
||||
import FooterEasterEgg from '@/components/FooterEasterEgg.astro';
|
||||
import JsonLd from '@/components/JsonLd.astro';
|
||||
import { PRODUCTION_URL } from '@/consts';
|
||||
import Blog from '@/layouts/Blog.astro';
|
||||
import { createBlogCollectionSchema } from '@/utils/jsonLd/schemas';
|
||||
|
||||
@@ -37,11 +38,11 @@ interface Props {
|
||||
const { page } = Astro.props;
|
||||
|
||||
// Build JSON-LD schema for CollectionPage
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, PRODUCTION_URL).toString();
|
||||
const jsonLdSchema = createBlogCollectionSchema({
|
||||
url: pageUrl,
|
||||
posts: page.data,
|
||||
siteUrl: Astro.site!.toString(),
|
||||
siteUrl: PRODUCTION_URL.toString(),
|
||||
});
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getCollection, getEntries, render } from 'astro:content';
|
||||
import FormattedDate from '@/components/FormattedDate.astro';
|
||||
import JsonLd from '@/components/JsonLd.astro';
|
||||
import defaultMarkdownComponents from '@/components/typography/defaultMarkdownComponents';
|
||||
import { PRODUCTION_URL } from '@/consts';
|
||||
import Blog from '@/layouts/Blog.astro';
|
||||
import { createBlogPostingSchema } from '@/utils/jsonLd/schemas';
|
||||
|
||||
@@ -23,8 +24,15 @@ const post = Astro.props;
|
||||
const { Content, remarkPluginFrontmatter } = await render(post);
|
||||
const authors = await getEntries(post.data.authors);
|
||||
|
||||
// Resolve OG image URL for JSON-LD
|
||||
const ogImageUrl = post.data.ogImage
|
||||
? typeof post.data.ogImage === 'string'
|
||||
? post.data.ogImage
|
||||
: new URL(post.data.ogImage.src, Astro.url).toString()
|
||||
: undefined;
|
||||
|
||||
// Build JSON-LD schema for BlogPosting
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, PRODUCTION_URL).toString();
|
||||
const jsonLdSchema = createBlogPostingSchema({
|
||||
title: post.data.title,
|
||||
description: post.data.description,
|
||||
@@ -33,7 +41,8 @@ const jsonLdSchema = createBlogPostingSchema({
|
||||
updatedDate: post.data.updatedDate,
|
||||
readingTime: remarkPluginFrontmatter.readingTimeMinutes,
|
||||
authors,
|
||||
siteUrl: Astro.site!.toString(),
|
||||
siteUrl: PRODUCTION_URL.toString(),
|
||||
image: ogImageUrl,
|
||||
});
|
||||
---
|
||||
|
||||
@@ -41,6 +50,8 @@ const jsonLdSchema = createBlogPostingSchema({
|
||||
title={[post.data.title, "Blog"]}
|
||||
description={post.data.description}
|
||||
canonical={post.data.canonical}
|
||||
image={post.data.ogImage}
|
||||
twitterImage={post.data.twitterImage}
|
||||
>
|
||||
<JsonLd slot="head" schema={jsonLdSchema} />
|
||||
<article
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getCollection, getEntries } from 'astro:content';
|
||||
import { AuthorSocialLinks } from '@/components/blog/AuthorSocialLinks';
|
||||
import BlogPostCard from '@/components/blog/BlogPostCard.astro';
|
||||
import JsonLd from '@/components/JsonLd.astro';
|
||||
import { PRODUCTION_URL } from '@/consts';
|
||||
import Blog from '@/layouts/Blog.astro';
|
||||
import { createProfilePageSchema } from '@/utils/jsonLd/schemas';
|
||||
|
||||
@@ -36,12 +37,12 @@ const postsWithAuthors = await Promise.all(
|
||||
);
|
||||
|
||||
// Build JSON-LD schema for ProfilePage
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, PRODUCTION_URL).toString();
|
||||
const jsonLdSchema = createProfilePageSchema({
|
||||
url: pageUrl,
|
||||
author,
|
||||
posts: authorPosts,
|
||||
siteUrl: Astro.site!.toString(),
|
||||
siteUrl: PRODUCTION_URL.toString(),
|
||||
});
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TableOfContents } from '@/components/docs/TableOfContents';
|
||||
import JsonLd from '@/components/JsonLd.astro';
|
||||
import defaultMarkdownComponents from '@/components/typography/defaultMarkdownComponents';
|
||||
import H2Markdown from '@/components/typography/H2Markdown.astro';
|
||||
import { PRODUCTION_URL } from '@/consts';
|
||||
import DocsLayout from '@/layouts/Docs.astro';
|
||||
import type { SupportedFramework } from '@/types/docs';
|
||||
import { SUPPORTED_FRAMEWORKS } from '@/types/docs';
|
||||
@@ -81,7 +82,7 @@ const docTitles = new Map(allDocs.map((d) => [d.id, getDocTitle(d, framework)]))
|
||||
const sections = getSectionsForGuide(slug, filterSidebar(framework));
|
||||
|
||||
// Build JSON-LD schema for TechArticle
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, PRODUCTION_URL).toString();
|
||||
const jsonLdSchema = createTechArticleSchema({
|
||||
title: getDocTitle(doc, framework),
|
||||
description: doc.data.description,
|
||||
|
||||
@@ -63,6 +63,7 @@ export function createBlogPostingSchema(params: {
|
||||
readingTime?: number;
|
||||
authors: CollectionEntry<'authors'>[];
|
||||
siteUrl: string;
|
||||
image?: string;
|
||||
}): WithContext<BlogPosting> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -70,6 +71,7 @@ export function createBlogPostingSchema(params: {
|
||||
headline: params.title,
|
||||
description: params.description,
|
||||
url: params.url,
|
||||
...(params.image && { image: params.image }),
|
||||
datePublished: params.pubDate.toISOString(),
|
||||
...(params.updatedDate && { dateModified: params.updatedDate.toISOString() }),
|
||||
...(params.wordCount && { wordCount: params.wordCount }),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "stream",
|
||||
"concurrency": "20",
|
||||
"globalEnv": ["DEPLOY_PRIME_URL"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
|
||||
Reference in New Issue
Block a user