@djangocfg/nextjs 2.1.437 → 2.1.439
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -41
- package/dist/config/index.mjs +1 -1
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/og-image/index.d.mts +114 -13
- package/dist/og-image/index.mjs +89 -27
- package/dist/og-image/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/og-image/README.md +106 -31
- package/src/og-image/index.ts +10 -2
- package/src/og-image/metadata-factory.ts +146 -0
- package/src/og-image/metadata.ts +68 -47
- package/src/og-image/types.ts +15 -0
|
@@ -2,6 +2,23 @@ import { Metadata } from 'next';
|
|
|
2
2
|
|
|
3
3
|
type OGPreset = 'DARK' | 'DARK_BLUE' | 'DARK_PURPLE' | 'DARK_GREEN' | 'DARK_ROSE' | 'LIGHT' | 'LIGHT_GRAY' | 'LIGHT_WARM' | 'LIGHT_GREEN';
|
|
4
4
|
type OGLayout = 'DEFAULT' | 'HERO' | 'ARTICLE' | 'MINIMAL';
|
|
5
|
+
/**
|
|
6
|
+
* Per-page OG image intent — the single axis that decides what the OG image is.
|
|
7
|
+
* A discriminated union so every case is explicit and type-checked:
|
|
8
|
+
*
|
|
9
|
+
* 'static' brand image from site config (the default)
|
|
10
|
+
* 'dynamic' render the page title onto the OG card
|
|
11
|
+
* { render: {...} } render with explicit preset/layout/colors
|
|
12
|
+
* { image, alt? } an exact image URL (e.g. a content screenshot)
|
|
13
|
+
*/
|
|
14
|
+
type OgSpec = 'static' | 'dynamic' | {
|
|
15
|
+
render: Omit<OGImageParams, 'title'> & {
|
|
16
|
+
title?: string;
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
image: string;
|
|
20
|
+
alt?: string;
|
|
21
|
+
};
|
|
5
22
|
/** Mirrors Django OGImageParams exactly */
|
|
6
23
|
interface OGImageParams {
|
|
7
24
|
title: string;
|
|
@@ -20,6 +37,84 @@ interface OGImageParams {
|
|
|
20
37
|
page_url?: string;
|
|
21
38
|
}
|
|
22
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Site-wide identity the metadata factory needs. Config-agnostic on purpose —
|
|
42
|
+
* the package doesn't know about any app's `settings` object, so the consumer
|
|
43
|
+
* passes these once (typically derived from their own settings module).
|
|
44
|
+
*/
|
|
45
|
+
interface SiteMetaConfig {
|
|
46
|
+
/** Brand / app name, e.g. "CMDOP". Used in titles, siteName, alt text. */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Default description used when a page doesn't supply its own. */
|
|
49
|
+
description: string;
|
|
50
|
+
/** Absolute site origin, e.g. "https://cmdop.com". Used for canonical/og url. */
|
|
51
|
+
siteUrl: string;
|
|
52
|
+
/** Ready-made static brand OG image URL (absolute or root-relative). */
|
|
53
|
+
ogImage: string;
|
|
54
|
+
/** Optional favicon / apple-touch icons forwarded to Metadata.icons. */
|
|
55
|
+
icons?: Metadata['icons'];
|
|
56
|
+
/** Locales for hreflang alternates. Omit for single-locale sites. */
|
|
57
|
+
locales?: readonly string[];
|
|
58
|
+
/** Locale used for x-default hreflang (defaults to "en" or first locale). */
|
|
59
|
+
defaultLocale?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Per-page inputs. Only `title` is really worth setting per page; everything
|
|
63
|
+
* else falls back to the site config.
|
|
64
|
+
*/
|
|
65
|
+
interface PageMeta {
|
|
66
|
+
/** Page title (without the brand suffix). Falls back to the site name. */
|
|
67
|
+
title?: string;
|
|
68
|
+
/** Page description; falls back to the site description. */
|
|
69
|
+
description?: string;
|
|
70
|
+
keywords?: string[];
|
|
71
|
+
/** Current locale, used for canonical + openGraph.locale + alternates. */
|
|
72
|
+
locale?: string;
|
|
73
|
+
/** Path without locale prefix, e.g. "/skills". '' for home. */
|
|
74
|
+
path?: string;
|
|
75
|
+
/**
|
|
76
|
+
* What the OG image should be. Defaults to `'static'` (the brand image).
|
|
77
|
+
* - `'static'` brand image from config (default)
|
|
78
|
+
* - `'dynamic'` render the page title onto the card
|
|
79
|
+
* - `{ render: {...} }` render with explicit preset/layout/colors
|
|
80
|
+
* - `{ image, alt? }` an exact image URL (e.g. a content screenshot)
|
|
81
|
+
*/
|
|
82
|
+
og?: OgSpec;
|
|
83
|
+
/** Escape hatch: extra Metadata fields merged last (robots, authors, etc.). */
|
|
84
|
+
extra?: Metadata;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Builds a complete Next.js `Metadata` from a one-time site config plus per-page
|
|
88
|
+
* inputs — title-template, description, openGraph, twitter, canonical + hreflang,
|
|
89
|
+
* icons, and the OG image — so pages only declare what's unique.
|
|
90
|
+
*
|
|
91
|
+
* The OG image is chosen by the single `og` field (defaults to the static brand
|
|
92
|
+
* image). There is exactly one way to do each thing.
|
|
93
|
+
*
|
|
94
|
+
* // app/_core/metadata.ts
|
|
95
|
+
* export const createMetadata = makeMetadataFactory({
|
|
96
|
+
* name: settings.app.name,
|
|
97
|
+
* description: settings.app.description,
|
|
98
|
+
* siteUrl: settings.app.siteUrl,
|
|
99
|
+
* ogImage: settings.app.media.ogimage,
|
|
100
|
+
* icons: { icon: settings.app.media.favicon },
|
|
101
|
+
* locales: LOCALES,
|
|
102
|
+
* })
|
|
103
|
+
*
|
|
104
|
+
* // landing page — static brand image (default)
|
|
105
|
+
* export const metadata = createMetadata({ title: 'Pricing', path: '/pricing' })
|
|
106
|
+
* // content page — title rendered onto the card
|
|
107
|
+
* export const metadata = createMetadata({ title: post.title, path, og: 'dynamic' })
|
|
108
|
+
* // content page with its own image
|
|
109
|
+
* export const metadata = createMetadata({ title: p.name, og: { image: p.image } })
|
|
110
|
+
*/
|
|
111
|
+
declare function createMetadata(config: SiteMetaConfig, page?: PageMeta): Metadata;
|
|
112
|
+
/**
|
|
113
|
+
* Curries `createMetadata` with a fixed site config so pages call a zero-config
|
|
114
|
+
* factory. This is the single recommended entry point for app metadata.
|
|
115
|
+
*/
|
|
116
|
+
declare function makeMetadataFactory(config: SiteMetaConfig): (page?: PageMeta) => Metadata;
|
|
117
|
+
|
|
23
118
|
/**
|
|
24
119
|
* Builds an absolute (or relative) OG image URL pointing to Django's
|
|
25
120
|
* django_ogimage endpoint: /cfg/og/<base64params>/
|
|
@@ -29,24 +124,30 @@ interface OGImageParams {
|
|
|
29
124
|
declare function buildOgUrl(params: OGImageParams): string;
|
|
30
125
|
|
|
31
126
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* Usage in generateMetadata():
|
|
36
|
-
* return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })
|
|
127
|
+
* Metadata fragment whose OG image is rendered by Django's django_ogimage
|
|
128
|
+
* endpoint (/cfg/og/). Low-level — most callers use `createMetadata`'s `og`
|
|
129
|
+
* field instead.
|
|
37
130
|
*/
|
|
38
131
|
declare function createOgMetadata(params: OGImageParams): Metadata;
|
|
132
|
+
/** Extract a plain title string from Next.js's title shapes. */
|
|
133
|
+
declare function extractTitle(metadata: Metadata): string;
|
|
134
|
+
/**
|
|
135
|
+
* Resolves an `OgSpec` against a base Metadata into a concrete og/twitter image
|
|
136
|
+
* fragment. `staticUrl` is the site's brand image, used for the `'static'` case.
|
|
137
|
+
* Returns `null` only if there's genuinely nothing to set.
|
|
138
|
+
*
|
|
139
|
+
* This is the single decision point for "what is the OG image" — no other code
|
|
140
|
+
* branches on the spec.
|
|
141
|
+
*/
|
|
142
|
+
declare function resolveOgImage(spec: OgSpec, staticUrl: string, fallbackTitle: string): Pick<Metadata, 'openGraph' | 'twitter'>;
|
|
39
143
|
/**
|
|
40
|
-
* Merges
|
|
144
|
+
* Merges OG image metadata (dynamic render) into existing Metadata. Kept for
|
|
145
|
+
* low-level/manual use; `createMetadata` is the recommended entry point.
|
|
41
146
|
*
|
|
42
|
-
*
|
|
43
|
-
* return withOgImage(
|
|
44
|
-
* { title: 'Page', description: '...' },
|
|
45
|
-
* { preset: 'DARK_BLUE', layout: 'HERO' }
|
|
46
|
-
* )
|
|
147
|
+
* return withOgImage({ title: 'Page' }, { preset: 'DARK_BLUE' })
|
|
47
148
|
*/
|
|
48
|
-
declare function withOgImage(metadata: Metadata, params
|
|
149
|
+
declare function withOgImage(metadata: Metadata, params?: Omit<OGImageParams, 'title'> & {
|
|
49
150
|
title?: string;
|
|
50
151
|
}): Metadata;
|
|
51
152
|
|
|
52
|
-
export { type OGImageParams, type OGLayout, type OGPreset, buildOgUrl, createOgMetadata, withOgImage };
|
|
153
|
+
export { type OGImageParams, type OGLayout, type OGPreset, type OgSpec, type PageMeta, type SiteMetaConfig, buildOgUrl, createMetadata, createOgMetadata, extractTitle, makeMetadataFactory, resolveOgImage, withOgImage };
|
package/dist/og-image/index.mjs
CHANGED
|
@@ -23,37 +23,20 @@ function buildOgUrl(params) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/og-image/metadata.ts
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
openGraph: {
|
|
31
|
-
images: [image]
|
|
32
|
-
},
|
|
33
|
-
twitter: {
|
|
34
|
-
card: "summary_large_image",
|
|
35
|
-
images: [url]
|
|
36
|
-
}
|
|
37
|
-
};
|
|
26
|
+
var OG_WIDTH = 1200;
|
|
27
|
+
var OG_HEIGHT = 630;
|
|
28
|
+
function imageEntry(url, alt) {
|
|
29
|
+
return { url, width: OG_WIDTH, height: OG_HEIGHT, alt };
|
|
38
30
|
}
|
|
39
|
-
function
|
|
40
|
-
const resolvedParams = {
|
|
41
|
-
title: extractTitle(metadata),
|
|
42
|
-
...params
|
|
43
|
-
};
|
|
44
|
-
const ogMeta = createOgMetadata(resolvedParams);
|
|
31
|
+
function ogImageFragment(url, alt) {
|
|
45
32
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...metadata.openGraph,
|
|
49
|
-
...ogMeta.openGraph
|
|
50
|
-
},
|
|
51
|
-
twitter: {
|
|
52
|
-
...metadata.twitter,
|
|
53
|
-
...ogMeta.twitter
|
|
54
|
-
}
|
|
33
|
+
openGraph: { images: [imageEntry(url, alt)] },
|
|
34
|
+
twitter: { card: "summary_large_image", images: [url] }
|
|
55
35
|
};
|
|
56
36
|
}
|
|
37
|
+
function createOgMetadata(params) {
|
|
38
|
+
return ogImageFragment(buildOgUrl(params), params.title);
|
|
39
|
+
}
|
|
57
40
|
function extractTitle(metadata) {
|
|
58
41
|
const t = metadata.title;
|
|
59
42
|
if (!t) return "";
|
|
@@ -62,9 +45,88 @@ function extractTitle(metadata) {
|
|
|
62
45
|
if (typeof t === "object" && "default" in t) return t.default;
|
|
63
46
|
return "";
|
|
64
47
|
}
|
|
48
|
+
function resolveOgImage(spec, staticUrl, fallbackTitle) {
|
|
49
|
+
if (spec === "static") {
|
|
50
|
+
return ogImageFragment(staticUrl, fallbackTitle);
|
|
51
|
+
}
|
|
52
|
+
if (spec === "dynamic") {
|
|
53
|
+
return ogImageFragment(buildOgUrl({ title: fallbackTitle }), fallbackTitle);
|
|
54
|
+
}
|
|
55
|
+
if ("image" in spec) {
|
|
56
|
+
return ogImageFragment(spec.image, spec.alt ?? fallbackTitle);
|
|
57
|
+
}
|
|
58
|
+
const title = spec.render.title ?? fallbackTitle;
|
|
59
|
+
return ogImageFragment(buildOgUrl({ ...spec.render, title }), title);
|
|
60
|
+
}
|
|
61
|
+
function withOgImage(metadata, params = {}) {
|
|
62
|
+
const title = params.title ?? extractTitle(metadata);
|
|
63
|
+
const frag = createOgMetadata({ ...params, title });
|
|
64
|
+
return {
|
|
65
|
+
...metadata,
|
|
66
|
+
openGraph: { ...metadata.openGraph, ...frag.openGraph },
|
|
67
|
+
twitter: { ...metadata.twitter, ...frag.twitter }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/og-image/metadata-factory.ts
|
|
72
|
+
function buildAlternates(config, path, locale) {
|
|
73
|
+
if (!config.locales?.length) {
|
|
74
|
+
return { canonical: `${config.siteUrl}/${path}`.replace(/\/+$/, "") || config.siteUrl };
|
|
75
|
+
}
|
|
76
|
+
const languages = {};
|
|
77
|
+
for (const loc of config.locales) {
|
|
78
|
+
languages[loc] = `${config.siteUrl}/${loc}${path}`;
|
|
79
|
+
}
|
|
80
|
+
const xDefault = config.defaultLocale ?? (config.locales.includes("en") ? "en" : config.locales[0]);
|
|
81
|
+
languages["x-default"] = `${config.siteUrl}/${xDefault}${path}`;
|
|
82
|
+
return { canonical: `${config.siteUrl}/${locale}${path}`, languages };
|
|
83
|
+
}
|
|
84
|
+
function createMetadata(config, page = {}) {
|
|
85
|
+
const title = page.title ?? config.name;
|
|
86
|
+
const description = page.description ?? config.description;
|
|
87
|
+
const locale = page.locale ?? config.defaultLocale ?? "en";
|
|
88
|
+
const path = page.path ?? "";
|
|
89
|
+
const ogUrl = config.locales?.length ? `${config.siteUrl}/${locale}${path}` : `${config.siteUrl}${path}`;
|
|
90
|
+
const ogImage = resolveOgImage(page.og ?? "static", config.ogImage, title);
|
|
91
|
+
const metadata = {
|
|
92
|
+
title,
|
|
93
|
+
description,
|
|
94
|
+
keywords: page.keywords,
|
|
95
|
+
alternates: buildAlternates(config, path, locale),
|
|
96
|
+
openGraph: {
|
|
97
|
+
type: "website",
|
|
98
|
+
locale,
|
|
99
|
+
url: ogUrl,
|
|
100
|
+
siteName: config.name,
|
|
101
|
+
title,
|
|
102
|
+
description,
|
|
103
|
+
...ogImage.openGraph
|
|
104
|
+
},
|
|
105
|
+
twitter: {
|
|
106
|
+
title,
|
|
107
|
+
description,
|
|
108
|
+
...ogImage.twitter
|
|
109
|
+
},
|
|
110
|
+
icons: config.icons
|
|
111
|
+
};
|
|
112
|
+
if (!page.extra) return metadata;
|
|
113
|
+
return {
|
|
114
|
+
...metadata,
|
|
115
|
+
...page.extra,
|
|
116
|
+
openGraph: { ...metadata.openGraph, ...page.extra.openGraph },
|
|
117
|
+
twitter: { ...metadata.twitter, ...page.extra.twitter }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function makeMetadataFactory(config) {
|
|
121
|
+
return (page = {}) => createMetadata(config, page);
|
|
122
|
+
}
|
|
65
123
|
export {
|
|
66
124
|
buildOgUrl,
|
|
125
|
+
createMetadata,
|
|
67
126
|
createOgMetadata,
|
|
127
|
+
extractTitle,
|
|
128
|
+
makeMetadataFactory,
|
|
129
|
+
resolveOgImage,
|
|
68
130
|
withOgImage
|
|
69
131
|
};
|
|
70
132
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/og-image/url.ts","../../src/og-image/metadata.ts"],"sourcesContent":["import type { OGImageParams } from './types'\n\n/**\n * Resolves the base URL for og:image meta tags.\n * Must be publicly accessible by crawlers (Twitter, Facebook, etc).\n *\n * Priority:\n * 1. NEXT_PUBLIC_MEDIA_URL — explicit media domain (nginx proxy or CDN)\n * 2. NEXT_PUBLIC_API_URL — direct Django API domain\n * 3. NEXT_PUBLIC_SITE_URL — site domain (same-domain/static build)\n * 4. '' — relative URL (last resort)\n *\n * Use cases:\n * - Same domain (static build): API_URL='' → relative /cfg/og/...\n * - Split domain (standalone): API_URL=https://api.x.com → absolute\n * - Media via nginx proxy: MEDIA_URL=https://x.com, API_URL=https://api.x.com\n * - CDN: MEDIA_URL=https://cdn.x.com\n */\nfunction resolveOgPublicBase(): string {\n if (typeof process === 'undefined') return ''\n return (\n process.env.NEXT_PUBLIC_MEDIA_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_API_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_SITE_URL?.replace(/\\/$/, '') ??\n ''\n )\n}\n\nfunction encodeBase64(str: string): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str).toString('base64url')\n }\n // Browser fallback\n return btoa(unescape(encodeURIComponent(str)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '')\n}\n\nfunction cleanParams(params: OGImageParams): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(params).filter(\n ([, v]) => v !== undefined && v !== null && v !== ''\n )\n )\n}\n\n/**\n * Builds an absolute (or relative) OG image URL pointing to Django's\n * django_ogimage endpoint: /cfg/og/<base64params>/\n *\n * The URL is suitable for use in og:image and twitter:image meta tags.\n */\nexport function buildOgUrl(params: OGImageParams): string {\n const b64 = encodeBase64(JSON.stringify(cleanParams(params)))\n const base = resolveOgPublicBase()\n return `${base}/cfg/og/${b64}/`\n}\n","import type { Metadata } from 'next'\nimport type { OGImageParams } from './types'\nimport { buildOgUrl } from './url'\n\n/**\n * Creates Next.js Metadata fragment with og:image and twitter:image\n * pointing to Django's django_ogimage renderer.\n *\n * Usage in generateMetadata():\n * return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })\n */\nexport function createOgMetadata(params: OGImageParams): Metadata {\n const url = buildOgUrl(params)\n const image = { url, width: 1200, height: 630, alt: params.title }\n return {\n openGraph: {\n images: [image],\n },\n twitter: {\n card: 'summary_large_image',\n images: [url],\n },\n }\n}\n\n/**\n * Merges og:image metadata into an existing Metadata object.\n *\n * Usage:\n * return withOgImage(\n * { title: 'Page', description: '...' },\n * { preset: 'DARK_BLUE', layout: 'HERO' }\n * )\n */\nexport function withOgImage(\n metadata: Metadata,\n params: Omit<OGImageParams, 'title'> & { title?: string }\n): Metadata {\n // Auto-extract title from metadata; params.title overrides if explicitly provided\n const resolvedParams: OGImageParams = {\n title: extractTitle(metadata),\n ...params,\n }\n\n const ogMeta = createOgMetadata(resolvedParams)\n\n return {\n ...metadata,\n openGraph: {\n ...metadata.openGraph,\n ...ogMeta.openGraph,\n },\n twitter: {\n ...metadata.twitter,\n ...ogMeta.twitter,\n },\n }\n}\n\nfunction extractTitle(metadata: Metadata): string {\n const t = metadata.title\n if (!t) return ''\n if (typeof t === 'string') return t\n if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute\n if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default\n return ''\n}\n"],"mappings":";AAkBA,SAAS,sBAA8B;AACrC,MAAI,OAAO,YAAY,YAAa,QAAO;AAC3C,SACE,QAAQ,IAAI,uBAAuB,QAAQ,OAAO,EAAE,KACpD,QAAQ,IAAI,qBAAqB,QAAQ,OAAO,EAAE,KAClD,QAAQ,IAAI,sBAAsB,QAAQ,OAAO,EAAE,KACnD;AAEJ;AAEA,SAAS,aAAa,KAAqB;AACzC,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,OAAO,KAAK,GAAG,EAAE,SAAS,WAAW;AAAA,EAC9C;AAEA,SAAO,KAAK,SAAS,mBAAmB,GAAG,CAAC,CAAC,EAC1C,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEA,SAAS,YAAY,QAAgD;AACnE,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,MAAM,EAAE;AAAA,MACrB,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,UAAa,MAAM,QAAQ,MAAM;AAAA,IACpD;AAAA,EACF;AACF;AAQO,SAAS,WAAW,QAA+B;AACxD,QAAM,MAAM,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAC5D,QAAM,OAAO,oBAAoB;AACjC,SAAO,GAAG,IAAI,WAAW,GAAG;AAC9B;;;AC9CO,SAAS,iBAAiB,QAAiC;AAChE,QAAM,MAAM,WAAW,MAAM;AAC7B,QAAM,QAAQ,EAAE,KAAK,OAAO,MAAM,QAAQ,KAAK,KAAK,OAAO,MAAM;AACjE,SAAO;AAAA,IACL,WAAW;AAAA,MACT,QAAQ,CAAC,KAAK;AAAA,IAChB;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,QAAQ,CAAC,GAAG;AAAA,IACd;AAAA,EACF;AACF;AAWO,SAAS,YACd,UACA,QACU;AAEV,QAAM,iBAAgC;AAAA,IACpC,OAAO,aAAa,QAAQ;AAAA,IAC5B,GAAG;AAAA,EACL;AAEA,QAAM,SAAS,iBAAiB,cAAc;AAE9C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,WAAW;AAAA,MACT,GAAG,SAAS;AAAA,MACZ,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,SAAS;AAAA,MACP,GAAG,SAAS;AAAA,MACZ,GAAG,OAAO;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,aAAa,UAA4B;AAChD,QAAM,IAAI,SAAS;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,OAAO,MAAM,YAAY,cAAc,EAAG,QAAQ,EAA2B;AACjF,MAAI,OAAO,MAAM,YAAY,aAAa,EAAG,QAAQ,EAA0B;AAC/E,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/og-image/url.ts","../../src/og-image/metadata.ts","../../src/og-image/metadata-factory.ts"],"sourcesContent":["import type { OGImageParams } from './types'\n\n/**\n * Resolves the base URL for og:image meta tags.\n * Must be publicly accessible by crawlers (Twitter, Facebook, etc).\n *\n * Priority:\n * 1. NEXT_PUBLIC_MEDIA_URL — explicit media domain (nginx proxy or CDN)\n * 2. NEXT_PUBLIC_API_URL — direct Django API domain\n * 3. NEXT_PUBLIC_SITE_URL — site domain (same-domain/static build)\n * 4. '' — relative URL (last resort)\n *\n * Use cases:\n * - Same domain (static build): API_URL='' → relative /cfg/og/...\n * - Split domain (standalone): API_URL=https://api.x.com → absolute\n * - Media via nginx proxy: MEDIA_URL=https://x.com, API_URL=https://api.x.com\n * - CDN: MEDIA_URL=https://cdn.x.com\n */\nfunction resolveOgPublicBase(): string {\n if (typeof process === 'undefined') return ''\n return (\n process.env.NEXT_PUBLIC_MEDIA_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_API_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_SITE_URL?.replace(/\\/$/, '') ??\n ''\n )\n}\n\nfunction encodeBase64(str: string): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str).toString('base64url')\n }\n // Browser fallback\n return btoa(unescape(encodeURIComponent(str)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '')\n}\n\nfunction cleanParams(params: OGImageParams): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(params).filter(\n ([, v]) => v !== undefined && v !== null && v !== ''\n )\n )\n}\n\n/**\n * Builds an absolute (or relative) OG image URL pointing to Django's\n * django_ogimage endpoint: /cfg/og/<base64params>/\n *\n * The URL is suitable for use in og:image and twitter:image meta tags.\n */\nexport function buildOgUrl(params: OGImageParams): string {\n const b64 = encodeBase64(JSON.stringify(cleanParams(params)))\n const base = resolveOgPublicBase()\n return `${base}/cfg/og/${b64}/`\n}\n","import type { Metadata } from 'next'\nimport type { OGImageParams, OgSpec } from './types'\nimport { buildOgUrl } from './url'\n\nconst OG_WIDTH = 1200\nconst OG_HEIGHT = 630\n\n/** A Next.js OG image entry. */\nfunction imageEntry(url: string, alt: string) {\n return { url, width: OG_WIDTH, height: OG_HEIGHT, alt }\n}\n\n/**\n * Builds the og/twitter image fragment for a given image URL — the one place\n * that knows the shape Next.js expects. Both static URLs and the dynamic\n * renderer funnel through here, so there's a single source of truth.\n */\nfunction ogImageFragment(url: string, alt: string): Pick<Metadata, 'openGraph' | 'twitter'> {\n return {\n openGraph: { images: [imageEntry(url, alt)] },\n twitter: { card: 'summary_large_image', images: [url] },\n }\n}\n\n/**\n * Metadata fragment whose OG image is rendered by Django's django_ogimage\n * endpoint (/cfg/og/). Low-level — most callers use `createMetadata`'s `og`\n * field instead.\n */\nexport function createOgMetadata(params: OGImageParams): Metadata {\n return ogImageFragment(buildOgUrl(params), params.title)\n}\n\n/** Extract a plain title string from Next.js's title shapes. */\nexport function extractTitle(metadata: Metadata): string {\n const t = metadata.title\n if (!t) return ''\n if (typeof t === 'string') return t\n if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute\n if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default\n return ''\n}\n\n/**\n * Resolves an `OgSpec` against a base Metadata into a concrete og/twitter image\n * fragment. `staticUrl` is the site's brand image, used for the `'static'` case.\n * Returns `null` only if there's genuinely nothing to set.\n *\n * This is the single decision point for \"what is the OG image\" — no other code\n * branches on the spec.\n */\nexport function resolveOgImage(\n spec: OgSpec,\n staticUrl: string,\n fallbackTitle: string\n): Pick<Metadata, 'openGraph' | 'twitter'> {\n if (spec === 'static') {\n return ogImageFragment(staticUrl, fallbackTitle)\n }\n if (spec === 'dynamic') {\n return ogImageFragment(buildOgUrl({ title: fallbackTitle }), fallbackTitle)\n }\n if ('image' in spec) {\n return ogImageFragment(spec.image, spec.alt ?? fallbackTitle)\n }\n // { render: {...} } — dynamic with explicit params; title defaults to page title.\n const title = spec.render.title ?? fallbackTitle\n return ogImageFragment(buildOgUrl({ ...spec.render, title }), title)\n}\n\n/**\n * Merges OG image metadata (dynamic render) into existing Metadata. Kept for\n * low-level/manual use; `createMetadata` is the recommended entry point.\n *\n * return withOgImage({ title: 'Page' }, { preset: 'DARK_BLUE' })\n */\nexport function withOgImage(\n metadata: Metadata,\n params: Omit<OGImageParams, 'title'> & { title?: string } = {}\n): Metadata {\n const title = params.title ?? extractTitle(metadata)\n const frag = createOgMetadata({ ...params, title })\n return {\n ...metadata,\n openGraph: { ...metadata.openGraph, ...frag.openGraph },\n twitter: { ...metadata.twitter, ...frag.twitter },\n }\n}\n","import type { Metadata } from 'next'\nimport type { OgSpec } from './types'\nimport { resolveOgImage } from './metadata'\n\n/**\n * Site-wide identity the metadata factory needs. Config-agnostic on purpose —\n * the package doesn't know about any app's `settings` object, so the consumer\n * passes these once (typically derived from their own settings module).\n */\nexport interface SiteMetaConfig {\n /** Brand / app name, e.g. \"CMDOP\". Used in titles, siteName, alt text. */\n name: string\n /** Default description used when a page doesn't supply its own. */\n description: string\n /** Absolute site origin, e.g. \"https://cmdop.com\". Used for canonical/og url. */\n siteUrl: string\n /** Ready-made static brand OG image URL (absolute or root-relative). */\n ogImage: string\n /** Optional favicon / apple-touch icons forwarded to Metadata.icons. */\n icons?: Metadata['icons']\n /** Locales for hreflang alternates. Omit for single-locale sites. */\n locales?: readonly string[]\n /** Locale used for x-default hreflang (defaults to \"en\" or first locale). */\n defaultLocale?: string\n}\n\n/**\n * Per-page inputs. Only `title` is really worth setting per page; everything\n * else falls back to the site config.\n */\nexport interface PageMeta {\n /** Page title (without the brand suffix). Falls back to the site name. */\n title?: string\n /** Page description; falls back to the site description. */\n description?: string\n keywords?: string[]\n /** Current locale, used for canonical + openGraph.locale + alternates. */\n locale?: string\n /** Path without locale prefix, e.g. \"/skills\". '' for home. */\n path?: string\n /**\n * What the OG image should be. Defaults to `'static'` (the brand image).\n * - `'static'` brand image from config (default)\n * - `'dynamic'` render the page title onto the card\n * - `{ render: {...} }` render with explicit preset/layout/colors\n * - `{ image, alt? }` an exact image URL (e.g. a content screenshot)\n */\n og?: OgSpec\n /** Escape hatch: extra Metadata fields merged last (robots, authors, etc.). */\n extra?: Metadata\n}\n\nfunction buildAlternates(\n config: SiteMetaConfig,\n path: string,\n locale: string\n): Metadata['alternates'] {\n if (!config.locales?.length) {\n return { canonical: `${config.siteUrl}/${path}`.replace(/\\/+$/, '') || config.siteUrl }\n }\n const languages: Record<string, string> = {}\n for (const loc of config.locales) {\n languages[loc] = `${config.siteUrl}/${loc}${path}`\n }\n const xDefault =\n config.defaultLocale ?? (config.locales.includes('en') ? 'en' : config.locales[0])\n languages['x-default'] = `${config.siteUrl}/${xDefault}${path}`\n return { canonical: `${config.siteUrl}/${locale}${path}`, languages }\n}\n\n/**\n * Builds a complete Next.js `Metadata` from a one-time site config plus per-page\n * inputs — title-template, description, openGraph, twitter, canonical + hreflang,\n * icons, and the OG image — so pages only declare what's unique.\n *\n * The OG image is chosen by the single `og` field (defaults to the static brand\n * image). There is exactly one way to do each thing.\n *\n * // app/_core/metadata.ts\n * export const createMetadata = makeMetadataFactory({\n * name: settings.app.name,\n * description: settings.app.description,\n * siteUrl: settings.app.siteUrl,\n * ogImage: settings.app.media.ogimage,\n * icons: { icon: settings.app.media.favicon },\n * locales: LOCALES,\n * })\n *\n * // landing page — static brand image (default)\n * export const metadata = createMetadata({ title: 'Pricing', path: '/pricing' })\n * // content page — title rendered onto the card\n * export const metadata = createMetadata({ title: post.title, path, og: 'dynamic' })\n * // content page with its own image\n * export const metadata = createMetadata({ title: p.name, og: { image: p.image } })\n */\nexport function createMetadata(config: SiteMetaConfig, page: PageMeta = {}): Metadata {\n const title = page.title ?? config.name\n const description = page.description ?? config.description\n const locale = page.locale ?? config.defaultLocale ?? 'en'\n const path = page.path ?? ''\n const ogUrl = config.locales?.length\n ? `${config.siteUrl}/${locale}${path}`\n : `${config.siteUrl}${path}`\n\n const ogImage = resolveOgImage(page.og ?? 'static', config.ogImage, title)\n\n const metadata: Metadata = {\n title,\n description,\n keywords: page.keywords,\n alternates: buildAlternates(config, path, locale),\n openGraph: {\n type: 'website',\n locale,\n url: ogUrl,\n siteName: config.name,\n title,\n description,\n ...ogImage.openGraph,\n },\n twitter: {\n title,\n description,\n ...ogImage.twitter,\n },\n icons: config.icons,\n }\n\n if (!page.extra) return metadata\n // Shallow-merge the escape hatch, but deep-merge the nested og/twitter so the\n // image we just set isn't clobbered by a partial `extra.openGraph`.\n return {\n ...metadata,\n ...page.extra,\n openGraph: { ...metadata.openGraph, ...page.extra.openGraph },\n twitter: { ...metadata.twitter, ...page.extra.twitter },\n }\n}\n\n/**\n * Curries `createMetadata` with a fixed site config so pages call a zero-config\n * factory. This is the single recommended entry point for app metadata.\n */\nexport function makeMetadataFactory(config: SiteMetaConfig) {\n return (page: PageMeta = {}): Metadata => createMetadata(config, page)\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.439",
|
|
4
4
|
"description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -143,9 +143,9 @@
|
|
|
143
143
|
"ai-docs": "tsx src/ai/cli.ts"
|
|
144
144
|
},
|
|
145
145
|
"peerDependencies": {
|
|
146
|
-
"@djangocfg/i18n": "^2.1.
|
|
147
|
-
"@djangocfg/monitor": "^2.1.
|
|
148
|
-
"@djangocfg/ui-core": "^2.1.
|
|
146
|
+
"@djangocfg/i18n": "^2.1.439",
|
|
147
|
+
"@djangocfg/monitor": "^2.1.439",
|
|
148
|
+
"@djangocfg/ui-core": "^2.1.439",
|
|
149
149
|
"next": "^16.2.2"
|
|
150
150
|
},
|
|
151
151
|
"peerDependenciesMeta": {
|
|
@@ -167,11 +167,11 @@
|
|
|
167
167
|
"serwist": "^9.2.3"
|
|
168
168
|
},
|
|
169
169
|
"devDependencies": {
|
|
170
|
-
"@djangocfg/i18n": "^2.1.
|
|
171
|
-
"@djangocfg/monitor": "^2.1.
|
|
172
|
-
"@djangocfg/ui-core": "^2.1.
|
|
173
|
-
"@djangocfg/layouts": "^2.1.
|
|
174
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
170
|
+
"@djangocfg/i18n": "^2.1.439",
|
|
171
|
+
"@djangocfg/monitor": "^2.1.439",
|
|
172
|
+
"@djangocfg/ui-core": "^2.1.439",
|
|
173
|
+
"@djangocfg/layouts": "^2.1.439",
|
|
174
|
+
"@djangocfg/typescript-config": "^2.1.439",
|
|
175
175
|
"@types/node": "^25.2.3",
|
|
176
176
|
"@types/react": "^19.2.15",
|
|
177
177
|
"@types/react-dom": "^19.2.3",
|
package/src/og-image/README.md
CHANGED
|
@@ -1,30 +1,80 @@
|
|
|
1
1
|
# @djangocfg/nextjs/og-image
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Page metadata + typed OG image builder for Django's `django_ogimage` renderer.
|
|
4
4
|
|
|
5
5
|
**No Edge Runtime. No `@vercel/og`. No JSX templates.**
|
|
6
6
|
Django renders and caches the PNG — Next.js only builds the URL and injects it into metadata.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
One factory per app; pages declare only what's unique. The OG image is chosen by
|
|
13
|
+
a single `og` field (defaults to your static brand image):
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// app/_core/metadata.ts — bind the factory once
|
|
17
|
+
import { makeMetadataFactory } from '@djangocfg/nextjs/og-image'
|
|
18
|
+
import { settings } from './settings'
|
|
19
|
+
import { LOCALES } from '@i18n/locales.config'
|
|
20
|
+
|
|
21
|
+
export const createMetadata = makeMetadataFactory({
|
|
22
|
+
name: settings.app.name,
|
|
23
|
+
description: settings.app.description,
|
|
24
|
+
siteUrl: settings.app.siteUrl,
|
|
25
|
+
ogImage: settings.app.media.ogimage, // static brand card (the default)
|
|
26
|
+
icons: { icon: settings.app.media.favicon },
|
|
27
|
+
locales: LOCALES, // omit for single-locale sites
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// any page.tsx
|
|
31
|
+
export const metadata = createMetadata({ title: 'Pricing', path: '/pricing' })
|
|
32
|
+
// ↑ static brand image, canonical, hreflang, og, twitter, icons
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// content page — render the title onto the OG card
|
|
37
|
+
export const metadata = createMetadata({ title: post.title, path, og: 'dynamic' })
|
|
38
|
+
|
|
39
|
+
// content page with its own image (a screenshot, cover, …)
|
|
40
|
+
export const metadata = createMetadata({ title: p.name, path, og: { image: p.image, alt: p.name } })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### The `og` axis (`OgSpec`)
|
|
44
|
+
|
|
45
|
+
A single discriminated union decides what the OG image is — there is exactly one
|
|
46
|
+
way to express each case:
|
|
47
|
+
|
|
48
|
+
| `og` value | Result |
|
|
49
|
+
|---|---|
|
|
50
|
+
| omitted / `'static'` | the brand image from `config.ogImage` (default) |
|
|
51
|
+
| `'dynamic'` | Django renders the page **title** onto the card |
|
|
52
|
+
| `{ render: { preset, layout, … } }` | dynamic render with explicit params |
|
|
53
|
+
| `{ image, alt? }` | an exact image URL (content screenshot, cover) |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
10
57
|
## How It Works
|
|
11
58
|
|
|
12
59
|
```
|
|
13
|
-
|
|
60
|
+
createMetadata({ … , og }) — picks the OG image by the single `og` field
|
|
61
|
+
↓ (dynamic cases only)
|
|
62
|
+
buildOgUrl(params) — base64-encodes OGImageParams
|
|
14
63
|
↓
|
|
15
|
-
|
|
16
|
-
↓
|
|
17
|
-
resolveOgPublicBase() — picks the right base URL from env vars
|
|
64
|
+
resolveOgPublicBase() — picks the right base URL from env vars
|
|
18
65
|
↓
|
|
19
66
|
<meta og:image> = "{base}/cfg/og/{b64}/"
|
|
20
67
|
|
|
21
68
|
Crawler/browser hits that URL
|
|
22
69
|
↓
|
|
23
|
-
Django OGImageRenderView
|
|
70
|
+
Django OGImageRenderView — decodes params, renders PNG via PicTex
|
|
24
71
|
↓
|
|
25
72
|
FileResponse (cached in MEDIA_ROOT/ogimage/<sharded>/<key>.png)
|
|
26
73
|
```
|
|
27
74
|
|
|
75
|
+
Static / `{ image }` cases skip Django entirely — the URL points straight at the
|
|
76
|
+
given image.
|
|
77
|
+
|
|
28
78
|
---
|
|
29
79
|
|
|
30
80
|
## URL Resolution
|
|
@@ -66,45 +116,70 @@ NEXT_PUBLIC_MEDIA_URL=https://cdn.example.com
|
|
|
66
116
|
|
|
67
117
|
## API
|
|
68
118
|
|
|
69
|
-
###
|
|
119
|
+
### Recommended
|
|
70
120
|
|
|
71
|
-
|
|
121
|
+
#### `makeMetadataFactory(config) → (page) => Metadata`
|
|
72
122
|
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
Binds a `SiteMetaConfig` once and returns a per-page factory. This is the single
|
|
124
|
+
recommended entry point.
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
const createMetadata = makeMetadataFactory(config)
|
|
128
|
+
export const metadata = createMetadata({ title: 'About', path: '/about' })
|
|
78
129
|
```
|
|
79
130
|
|
|
80
|
-
|
|
131
|
+
**`SiteMetaConfig`**
|
|
81
132
|
|
|
82
|
-
|
|
133
|
+
| Field | Type | Notes |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `name` | `string` | brand name (titles, siteName, alt) |
|
|
136
|
+
| `description` | `string` | default description |
|
|
137
|
+
| `siteUrl` | `string` | absolute origin, e.g. `https://example.com` |
|
|
138
|
+
| `ogImage` | `string` | static brand OG image URL |
|
|
139
|
+
| `icons?` | `Metadata['icons']` | favicon / apple-touch |
|
|
140
|
+
| `locales?` | `readonly string[]` | for hreflang; omit for single-locale |
|
|
141
|
+
| `defaultLocale?` | `string` | x-default locale (defaults `en` / first) |
|
|
83
142
|
|
|
84
|
-
|
|
85
|
-
import { createOgMetadata } from '@djangocfg/nextjs/og-image'
|
|
143
|
+
**`PageMeta`**
|
|
86
144
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
145
|
+
| Field | Type | Notes |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `title?` | `string` | falls back to `config.name` |
|
|
148
|
+
| `description?` | `string` | falls back to `config.description` |
|
|
149
|
+
| `keywords?` | `string[]` | |
|
|
150
|
+
| `locale?` | `string` | canonical + `openGraph.locale` |
|
|
151
|
+
| `path?` | `string` | path without locale prefix, `''` for home |
|
|
152
|
+
| `og?` | `OgSpec` | the OG image axis (see table above) |
|
|
153
|
+
| `extra?` | `Metadata` | escape hatch merged last (robots, authors, title-template, …) |
|
|
92
154
|
|
|
93
|
-
|
|
155
|
+
#### `createMetadata(config, page)`
|
|
94
156
|
|
|
95
|
-
|
|
157
|
+
The un-curried form `makeMetadataFactory` wraps. Use when you don't want a bound factory.
|
|
96
158
|
|
|
97
|
-
|
|
98
|
-
import { withOgImage } from '@djangocfg/nextjs/og-image'
|
|
159
|
+
### Low-level (escape hatches)
|
|
99
160
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
Most pages never need these — they're the primitives the factory is built on.
|
|
162
|
+
|
|
163
|
+
#### `buildOgUrl(params)`
|
|
164
|
+
|
|
165
|
+
Raw OG image URL string for the dynamic renderer.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const url = buildOgUrl({ title: 'Hello', preset: 'DARK_BLUE' })
|
|
169
|
+
// → "https://api.example.com/cfg/og/eyJ0aXRsZSI6Ikhlb.../"
|
|
106
170
|
```
|
|
107
171
|
|
|
172
|
+
#### `createOgMetadata(params)` · `withOgImage(metadata, params)`
|
|
173
|
+
|
|
174
|
+
`createOgMetadata` returns a `{ openGraph, twitter }` image fragment for the
|
|
175
|
+
dynamic renderer; `withOgImage` merges that fragment into an existing `Metadata`
|
|
176
|
+
(auto-extracting `title`). Both are for manual metadata assembly outside the factory.
|
|
177
|
+
|
|
178
|
+
#### `resolveOgImage(spec, staticUrl, fallbackTitle)`
|
|
179
|
+
|
|
180
|
+
The single decision point that turns an `OgSpec` into an image fragment — exported
|
|
181
|
+
for advanced custom factories.
|
|
182
|
+
|
|
108
183
|
---
|
|
109
184
|
|
|
110
185
|
## OGImageParams
|
package/src/og-image/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
// ── Recommended API ──────────────────────────────────────────────────────────
|
|
2
|
+
// Bind the factory once per app, then call it from each page.
|
|
3
|
+
export { createMetadata, makeMetadataFactory } from './metadata-factory'
|
|
4
|
+
export type { SiteMetaConfig, PageMeta } from './metadata-factory'
|
|
5
|
+
|
|
6
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
7
|
+
export type { OGImageParams, OGPreset, OGLayout, OgSpec } from './types'
|
|
8
|
+
|
|
9
|
+
// ── Low-level primitives (escape hatches) ────────────────────────────────────
|
|
2
10
|
export { buildOgUrl } from './url'
|
|
3
|
-
export { createOgMetadata, withOgImage } from './metadata'
|
|
11
|
+
export { createOgMetadata, withOgImage, resolveOgImage, extractTitle } from './metadata'
|