forked from Mike/freno-dev
og image
This commit is contained in:
BIN
public/nook/og.png
Normal file
BIN
public/nook/og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -132,12 +132,17 @@ describe("resolvePageHeadMeta — canonical URL derivation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolvePageHeadMeta — OpenGraph fallbacks", () => {
|
describe("resolvePageHeadMeta — OpenGraph fallbacks", () => {
|
||||||
it("ogImage defaults to the site's ogDefaultImage when not provided", () => {
|
it("ogImage defaults to the site's ogDefaultImage, resolved to an absolute URL", () => {
|
||||||
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nessa, "/");
|
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nessa, "/");
|
||||||
expect(meta.ogImage).toBe(SITE_CONFIG.nessa.ogDefaultImage);
|
expect(meta.ogImage).toBe("https://nessa.freno.me/nessa/og-default.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("explicit ogImage overrides the site default", () => {
|
it("the nook default resolves to the absolute og.png URL", () => {
|
||||||
|
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nook, "/");
|
||||||
|
expect(meta.ogImage).toBe("https://nook.freno.me/nook/og.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("explicit absolute ogImage passes through unchanged", () => {
|
||||||
const meta = resolvePageHeadMeta(
|
const meta = resolvePageHeadMeta(
|
||||||
{ ...BASE_PROPS, ogImage: "https://cdn/custom.png" },
|
{ ...BASE_PROPS, ogImage: "https://cdn/custom.png" },
|
||||||
SITE_CONFIG.main,
|
SITE_CONFIG.main,
|
||||||
@@ -146,6 +151,15 @@ describe("resolvePageHeadMeta — OpenGraph fallbacks", () => {
|
|||||||
expect(meta.ogImage).toBe("https://cdn/custom.png");
|
expect(meta.ogImage).toBe("https://cdn/custom.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("explicit site-relative ogImage is resolved against the site domain", () => {
|
||||||
|
const meta = resolvePageHeadMeta(
|
||||||
|
{ ...BASE_PROPS, ogImage: "/og/custom.png" },
|
||||||
|
SITE_CONFIG.gaze,
|
||||||
|
"/"
|
||||||
|
);
|
||||||
|
expect(meta.ogImage).toBe("https://gaze.freno.me/og/custom.png");
|
||||||
|
});
|
||||||
|
|
||||||
it("ogTitle falls back to the base title (no suffix)", () => {
|
it("ogTitle falls back to the base title (no suffix)", () => {
|
||||||
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.main, "/");
|
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.main, "/");
|
||||||
expect(meta.ogTitle).toBe("Blog");
|
expect(meta.ogTitle).toBe("Blog");
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export default function PageHead(props: PageHeadProps) {
|
|||||||
<Meta property="og:description" content={meta().ogDescription} />
|
<Meta property="og:description" content={meta().ogDescription} />
|
||||||
)}
|
)}
|
||||||
<Meta property="og:image" content={meta().ogImage} />
|
<Meta property="og:image" content={meta().ogImage} />
|
||||||
|
{/* X/Twitter large-image card; falls back to the og:* tags above. */}
|
||||||
|
<Meta name="twitter:card" content="summary_large_image" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
* prefix-strip below also handles any legacy prefixed form, so the canonical
|
* prefix-strip below also handles any legacy prefixed form, so the canonical
|
||||||
* is always the public URL.
|
* is always the public URL.
|
||||||
* - `ogImage` → explicit `props.ogImage` wins; otherwise `site.ogDefaultImage`.
|
* - `ogImage` → explicit `props.ogImage` wins; otherwise `site.ogDefaultImage`.
|
||||||
|
* Either way it is resolved to an **absolute** URL against the site's
|
||||||
|
* domain — OG consumers (X, Slack, Discord, iMessage) require an absolute
|
||||||
|
* URL, and a site-relative path in `ogDefaultImage` would 404 otherwise.
|
||||||
* - `ogTitle` / `ogDescription` → explicit override wins; otherwise fall
|
* - `ogTitle` / `ogDescription` → explicit override wins; otherwise fall
|
||||||
* back to the base title (no suffix) / description (existing behavior).
|
* back to the base title (no suffix) / description (existing behavior).
|
||||||
*/
|
*/
|
||||||
@@ -49,6 +52,20 @@ export interface ResolvedPageHeadMeta {
|
|||||||
ogImage: string;
|
ogImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an OpenGraph image reference to an absolute URL.
|
||||||
|
*
|
||||||
|
* OG consumers (X, Slack, Discord, LinkedIn, iMessage) require an absolute
|
||||||
|
* URL; `ogDefaultImage` values are site-relative paths (`/nook/og.png`), so
|
||||||
|
* they are joined to the site origin here. Already-absolute references
|
||||||
|
* (`https:`, `data:`, protocol-relative `//`) pass through untouched.
|
||||||
|
*/
|
||||||
|
function absolutizeOgImage(image: string, domain: string): string {
|
||||||
|
if (image.startsWith("//")) return `https:${image}`;
|
||||||
|
if (/^[a-z][a-z0-9+.-]*:/i.test(image)) return image;
|
||||||
|
return `https://${domain}${image.startsWith("/") ? "" : "/"}${image}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePageHeadMeta(
|
export function resolvePageHeadMeta(
|
||||||
props: PageHeadProps,
|
props: PageHeadProps,
|
||||||
site: Site,
|
site: Site,
|
||||||
@@ -71,7 +88,10 @@ export function resolvePageHeadMeta(
|
|||||||
const canonical = props.canonical ?? `https://${site.domain}${publicPath}`;
|
const canonical = props.canonical ?? `https://${site.domain}${publicPath}`;
|
||||||
const ogTitle = props.ogTitle ?? props.title;
|
const ogTitle = props.ogTitle ?? props.title;
|
||||||
const ogDescription = props.ogDescription ?? props.description;
|
const ogDescription = props.ogDescription ?? props.description;
|
||||||
const ogImage = props.ogImage ?? site.ogDefaultImage;
|
const ogImage = absolutizeOgImage(
|
||||||
|
props.ogImage ?? site.ogDefaultImage,
|
||||||
|
site.domain
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
|||||||
titleSuffix: " | The Nook",
|
titleSuffix: " | The Nook",
|
||||||
brandColor: "#4C9FBC",
|
brandColor: "#4C9FBC",
|
||||||
brandColorDark: "#4C9FBC",
|
brandColorDark: "#4C9FBC",
|
||||||
ogDefaultImage: "/nook/og-default.png",
|
ogDefaultImage: "/nook/og.png",
|
||||||
faviconPath: "/nook/favicon/favicon.ico"
|
faviconPath: "/nook/favicon/favicon.ico"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ describe("Nessa landing page — PageHead metadata", () => {
|
|||||||
expect(meta.canonical).toBe("https://nessa.freno.me/");
|
expect(meta.canonical).toBe("https://nessa.freno.me/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ogImage falls back to the nessa site default", () => {
|
it("ogImage falls back to the nessa site default, resolved absolute", () => {
|
||||||
const meta = resolvePageHeadMeta(
|
const meta = resolvePageHeadMeta(
|
||||||
NESSA_LANDING_META,
|
NESSA_LANDING_META,
|
||||||
SITE_CONFIG.nessa,
|
SITE_CONFIG.nessa,
|
||||||
"/"
|
"/"
|
||||||
);
|
);
|
||||||
expect(meta.ogImage).toBe(SITE_CONFIG.nessa.ogDefaultImage);
|
expect(meta.ogImage).toBe("https://nessa.freno.me/nessa/og-default.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ogTitle uses the explicit marketing copy, not the bare title", () => {
|
it("ogTitle uses the explicit marketing copy, not the bare title", () => {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function NookLanding() {
|
|||||||
<PageHead
|
<PageHead
|
||||||
title="Home"
|
title="Home"
|
||||||
description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control."
|
description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control."
|
||||||
ogImage="/nook/og-default.png"
|
ogImage={`https://${site().domain}/nook/og.png`}
|
||||||
ogTitle="The Nook — native macOS agent orchestration"
|
ogTitle="The Nook — native macOS agent orchestration"
|
||||||
ogDescription="A one-time-purchase macOS app for coding-agent orchestration, fan control, and thermal insight."
|
ogDescription="A one-time-purchase macOS app for coding-agent orchestration, fan control, and thermal insight."
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user