Commit 589e5773 by PLN (Algolia)

blog: post metadata rides the og pass — real images, videos when they exist

lib/og.js builds the openGraph/twitter block per post rendering (blog
routes, language variants, root keys): disk-checked og image, og:video
when the exploratory mp4 exists, locale from the rendering language,
canonical still the root form. The /blog listing keeps the site-level
default — one source of og truth per surface. lib/site.js mints the
origin constant metadataBase now shares.
parent 1baa27ed
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import PostArticle from "../blog/PostArticle"; import PostArticle from "../blog/PostArticle";
import { buildLanguageNav } from "../blog/language-nav"; import { buildLanguageNav } from "../blog/language-nav";
import { blogCanonicalPath, getAllBlogKeys, getBlogPost, resolveBlogKey } from "@/lib/blog"; import { getAllBlogKeys, getBlogPost, resolveBlogKey } from "@/lib/blog";
import { blogPostMetadata } from "@/lib/og";
import { assertRootKeysAvailable } from "@/lib/keys"; import { assertRootKeysAvailable } from "@/lib/keys";
import { draftsVisible } from "@/lib/content"; import { draftsVisible } from "@/lib/content";
...@@ -37,20 +38,7 @@ export async function generateMetadata({ params }) { ...@@ -37,20 +38,7 @@ export async function generateMetadata({ params }) {
// The canonical is once per post: the root form of the canonical folder // The canonical is once per post: the root form of the canonical folder
// slug — even from a localized-slug alias rendering this very page. // slug — even from a localized-slug alias rendering this very page.
const canonical = blogCanonicalPath(post.slug); return blogPostMetadata(post);
return {
title: post.title,
description: post.description,
alternates: { canonical },
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: canonical,
publishedTime: post.date,
images: ['/images/profile.png'],
},
};
} }
export default async function RootKey({ params }) { export default async function RootKey({ params }) {
......
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import PostArticle from "../../PostArticle"; import PostArticle from "../../PostArticle";
import { buildLanguageNav } from "../../language-nav"; import { buildLanguageNav } from "../../language-nav";
import { BLOG_LANGS, DEFAULT_LANG, blogCanonicalPath, getAllBlogSlugs, getBlogLanguages, getBlogPost } from "@/lib/blog"; import { BLOG_LANGS, DEFAULT_LANG, getAllBlogSlugs, getBlogLanguages, getBlogPost } from "@/lib/blog";
import { blogPostMetadata } from "@/lib/og";
/** /**
* Non-default language route: `/blog/<slug>/<lang>`. The default language is * Non-default language route: `/blog/<slug>/<lang>`. The default language is
...@@ -28,20 +29,7 @@ export async function generateMetadata({ params }) { ...@@ -28,20 +29,7 @@ export async function generateMetadata({ params }) {
// Alias with canonical: the canonical is declared once per post — the root // Alias with canonical: the canonical is declared once per post — the root
// form of the canonical folder slug (DESIGN.md, "the root form is // form of the canonical folder slug (DESIGN.md, "the root form is
// canonical"). Language variants and root keys all point there. // canonical"). Language variants and root keys all point there.
const canonical = blogCanonicalPath(post.slug); return blogPostMetadata(post);
return {
title: post.title,
description: post.description,
alternates: { canonical },
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: canonical,
publishedTime: post.date,
images: ['/images/profile.png'],
},
};
} }
export default async function BlogPostLang({ params }) { export default async function BlogPostLang({ params }) {
......
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import PostArticle from "../PostArticle"; import PostArticle from "../PostArticle";
import { buildLanguageNav } from "../language-nav"; import { buildLanguageNav } from "../language-nav";
import { blogCanonicalPath, getAllBlogSlugs, getBlogPost } from "@/lib/blog"; import { getAllBlogSlugs, getBlogPost } from "@/lib/blog";
import { blogPostMetadata } from "@/lib/og";
export function generateStaticParams() { export function generateStaticParams() {
const slugs = getAllBlogSlugs(); const slugs = getAllBlogSlugs();
...@@ -16,20 +17,7 @@ export async function generateMetadata({ params }) { ...@@ -16,20 +17,7 @@ export async function generateMetadata({ params }) {
const post = await getBlogPost(slug); const post = await getBlogPost(slug);
// Alias with canonical: the root form is the real address (DESIGN.md) — // Alias with canonical: the root form is the real address (DESIGN.md) —
// this page serves the same content and points at it. // this page serves the same content and points at it.
const canonical = blogCanonicalPath(post.slug); return blogPostMetadata(post);
return {
title: post.title,
description: post.description,
alternates: { canonical },
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: canonical,
publishedTime: post.date,
images: ['/images/profile.png'],
},
};
} }
export default async function BlogPost({ params }) { export default async function BlogPost({ params }) {
......
...@@ -4,9 +4,10 @@ import '@/styles/globals.css'; ...@@ -4,9 +4,10 @@ import '@/styles/globals.css';
import '@/styles/main.css'; import '@/styles/main.css';
import '@/styles/masonry.css'; import '@/styles/masonry.css';
import ThemeSwitcher from '@/components/ThemeSwitcher'; import ThemeSwitcher from '@/components/ThemeSwitcher';
import { SITE_URL } from '@/lib/site';
export const metadata = { export const metadata = {
metadataBase: new URL('https://me.nech.pl'), metadataBase: new URL(SITE_URL),
title: { title: {
default: "PLN's Works", default: "PLN's Works",
template: '%s · PLN', template: '%s · PLN',
......
import fs from 'fs';
import path from 'path';
import { blogCanonicalPath } from './blog';
import { SITE_URL } from './site';
/**
* Open Graph / Twitter metadata for blog posts, fed by the og pass
* (scripts/og.mjs): the generator writes public/images/blog/<slug>/og.png
* (or .og.jpg over the size budget) and, for hydra posts, an exploratory
* og.mp4; everything here is disk-checked, so regenerating media never
* touches page code and a missing asset simply drops its tag.
*
* Licence note: a hydra post's og image is a render of the sketch author's
* CC-licensed artwork (see scripts/og.mjs) — the page carries the
* attribution; these tags just point at the depiction.
*/
const OG_IMAGE_DIR = (slug) => path.join(process.cwd(), 'public', 'images', 'blog', slug);
/** The og image a post currently ships, as a site-absolute path. */
export function blogOgImage(slug) {
for (const [name, type] of [
['og.png', 'image/png'],
['og.jpg', 'image/jpeg'],
]) {
if (fs.existsSync(path.join(OG_IMAGE_DIR(slug), name))) {
return { url: `/images/blog/${slug}/${name}`, type, width: 1200, height: 630 };
}
}
return null;
}
/** The og video a post currently ships, or null. Exploratory (og pass).
* Absolute URL — Next applies metadataBase to images but not to videos. */
export function blogOgVideo(slug) {
if (fs.existsSync(path.join(OG_IMAGE_DIR(slug), 'og.mp4'))) {
return {
url: new URL(`/images/blog/${slug}/og.mp4`, SITE_URL),
type: 'video/mp4',
width: 960,
height: 504,
};
}
return null;
}
const OG_LOCALES = { fr: 'fr_FR', en: 'en_US' };
/**
* Full metadata for any rendering of a post (blog routes, language
* variants, root keys): canonical stays the root form of the folder slug
* (DESIGN.md), locale follows the rendering language, and the social media
* ride on whatever the og pass generated.
*/
export function blogPostMetadata(post) {
const canonical = blogCanonicalPath(post.slug);
const image = blogOgImage(post.slug);
const video = blogOgVideo(post.slug);
const locale = OG_LOCALES[post.lang] ?? 'en_US';
const alternateLocale = Object.entries(OG_LOCALES)
.filter(([lang]) => post.languages?.includes(lang) && OG_LOCALES[lang] !== locale)
.map(([, value]) => value);
return {
title: post.title,
description: post.description,
alternates: { canonical },
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: canonical,
publishedTime: post.date,
locale,
...(alternateLocale.length > 0 ? { alternateLocale } : {}),
images: image ? [image] : undefined,
...(video ? { videos: [video] } : {}),
},
twitter: {
card: image ? 'summary_large_image' : 'summary',
title: post.title,
description: post.description,
images: image ? [image.url] : undefined,
},
};
}
/**
* The site's canonical origin — the one constant every absolute-URL need
* resolves against (metadataBase, og:video absolutization, …).
*/
export const SITE_URL = 'https://me.nech.pl';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment