@djangocfg/nextjs 2.1.437 → 2.1.438
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/dist/config/index.mjs +1 -1
- package/dist/config/index.mjs.map +1 -1
- package/dist/i18n/routing.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/og-image/index.d.mts +95 -7
- package/dist/og-image/index.mjs +76 -2
- package/dist/og-image/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/og-image/index.ts +3 -0
- package/src/og-image/metadata-factory.ts +141 -0
- package/src/og-image/metadata.ts +49 -7
|
@@ -36,17 +36,105 @@ declare function buildOgUrl(params: OGImageParams): string;
|
|
|
36
36
|
* return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })
|
|
37
37
|
*/
|
|
38
38
|
declare function createOgMetadata(params: OGImageParams): Metadata;
|
|
39
|
+
/**
|
|
40
|
+
* Options for `withOgImage`. Extends the dynamic-renderer params with an
|
|
41
|
+
* `image` opt-out so a page can keep its own (static) OG image instead of the
|
|
42
|
+
* dynamically-rendered one.
|
|
43
|
+
*/
|
|
44
|
+
type WithOgImageOptions = Omit<OGImageParams, 'title'> & {
|
|
45
|
+
title?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Controls the OG image source:
|
|
48
|
+
* - omitted / `true` → dynamic render via Django's /cfg/og/ (default)
|
|
49
|
+
* - `false` → don't touch images; the page/layout's own og:image
|
|
50
|
+
* is preserved (use for a ready-made static brand image)
|
|
51
|
+
* - `string` → use this exact URL as a static og/twitter image
|
|
52
|
+
*/
|
|
53
|
+
image?: boolean | string;
|
|
54
|
+
};
|
|
39
55
|
/**
|
|
40
56
|
* Merges og:image metadata into an existing Metadata object.
|
|
41
57
|
*
|
|
42
58
|
* Usage:
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* )
|
|
59
|
+
* // dynamic (default)
|
|
60
|
+
* return withOgImage({ title: 'Page', description: '...' }, { preset: 'DARK_BLUE' })
|
|
61
|
+
* // keep the page's own static image
|
|
62
|
+
* return withOgImage(meta, { preset: 'DARK_BLUE', image: false })
|
|
63
|
+
* // explicit static image
|
|
64
|
+
* return withOgImage(meta, { image: '/static/ogimage.png' })
|
|
47
65
|
*/
|
|
48
|
-
declare function withOgImage(metadata: Metadata, params:
|
|
66
|
+
declare function withOgImage(metadata: Metadata, params?: WithOgImageOptions): Metadata;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Site-wide identity the metadata factory needs. Config-agnostic on purpose —
|
|
70
|
+
* the package doesn't know about any app's `settings` object, so the consumer
|
|
71
|
+
* passes these once (typically derived from their own settings module).
|
|
72
|
+
*/
|
|
73
|
+
interface SiteMetaConfig {
|
|
74
|
+
/** Brand / app name, e.g. "CMDOP". Used in titles, siteName, alt text. */
|
|
75
|
+
name: string;
|
|
76
|
+
/** Default description used when a page doesn't supply its own. */
|
|
77
|
+
description: string;
|
|
78
|
+
/** Absolute site origin, e.g. "https://cmdop.com". Used for canonical/og url. */
|
|
79
|
+
siteUrl: string;
|
|
80
|
+
/** Ready-made static brand OG image URL (absolute or root-relative). */
|
|
81
|
+
ogImage: string;
|
|
82
|
+
/** Optional favicon / apple-touch icons forwarded to Metadata.icons. */
|
|
83
|
+
icons?: Metadata['icons'];
|
|
84
|
+
/** Locales for hreflang alternates (first is treated as x-default if no enDefault). */
|
|
85
|
+
locales?: readonly string[];
|
|
86
|
+
/** Locale used for x-default hreflang (defaults to "en" or first locale). */
|
|
87
|
+
defaultLocale?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Per-page inputs. Everything is optional except what makes the page unique. */
|
|
90
|
+
interface PageMeta {
|
|
91
|
+
/** Page title (without the brand suffix — the template adds it). */
|
|
49
92
|
title?: string;
|
|
50
|
-
|
|
93
|
+
/** Page description; falls back to the site description. */
|
|
94
|
+
description?: string;
|
|
95
|
+
keywords?: string[];
|
|
96
|
+
/** Current locale, used for canonical + openGraph.locale + alternates. */
|
|
97
|
+
locale?: string;
|
|
98
|
+
/** Path without locale prefix, e.g. "/skills". '' for home. Drives alternates. */
|
|
99
|
+
path?: string;
|
|
100
|
+
/**
|
|
101
|
+
* OG image control, forwarded to `withOgImage`:
|
|
102
|
+
* - omitted → static brand image (`config.ogImage`) — the safe default
|
|
103
|
+
* - `true` → dynamic render from the page title
|
|
104
|
+
* - OGImageParams → dynamic render with explicit preset/layout/colors
|
|
105
|
+
* - `false` → no image (inherit whatever the layout set)
|
|
106
|
+
* - string → explicit image URL
|
|
107
|
+
*/
|
|
108
|
+
og?: boolean | string | Omit<OGImageParams, 'title'>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds a complete Next.js `Metadata` object from a one-time site config plus
|
|
112
|
+
* per-page inputs — title-template, description, openGraph, twitter, canonical
|
|
113
|
+
* + hreflang alternates, and icons — so pages only declare what's unique.
|
|
114
|
+
*
|
|
115
|
+
* OG image defaults to the static brand image (`config.ogImage`); opt into the
|
|
116
|
+
* dynamic renderer per page with `og: true` / `og: { preset: 'DARK_BLUE' }`.
|
|
117
|
+
*
|
|
118
|
+
* Bind the config once in your app, then call the bound fn from each page:
|
|
119
|
+
*
|
|
120
|
+
* // app/_core/metadata.ts
|
|
121
|
+
* export const createMetadata = makeMetadataFactory({
|
|
122
|
+
* name: settings.app.name,
|
|
123
|
+
* description: settings.app.description,
|
|
124
|
+
* siteUrl: settings.app.siteUrl,
|
|
125
|
+
* ogImage: settings.app.media.ogimage,
|
|
126
|
+
* icons: { icon: settings.app.media.favicon },
|
|
127
|
+
* locales: LOCALES,
|
|
128
|
+
* })
|
|
129
|
+
*
|
|
130
|
+
* // any page.tsx
|
|
131
|
+
* export const metadata = createMetadata({ title: 'Skills', path: '/skills' })
|
|
132
|
+
*/
|
|
133
|
+
declare function createMetadata(config: SiteMetaConfig, page?: PageMeta): Metadata;
|
|
134
|
+
/**
|
|
135
|
+
* Curries `createMetadata` with a fixed site config so pages call a zero-config
|
|
136
|
+
* factory. See the docstring on `createMetadata` for the usage pattern.
|
|
137
|
+
*/
|
|
138
|
+
declare function makeMetadataFactory(config: SiteMetaConfig): (page?: PageMeta) => Metadata;
|
|
51
139
|
|
|
52
|
-
export { type OGImageParams, type OGLayout, type OGPreset, buildOgUrl, createOgMetadata, withOgImage };
|
|
140
|
+
export { type OGImageParams, type OGLayout, type OGPreset, type PageMeta, type SiteMetaConfig, type WithOgImageOptions, buildOgUrl, createMetadata, createOgMetadata, makeMetadataFactory, withOgImage };
|
package/dist/og-image/index.mjs
CHANGED
|
@@ -36,10 +36,28 @@ function createOgMetadata(params) {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
function withOgImage(metadata, params) {
|
|
39
|
+
function withOgImage(metadata, params = {}) {
|
|
40
|
+
const { image, ...ogParams } = params;
|
|
41
|
+
if (image === false) {
|
|
42
|
+
return metadata;
|
|
43
|
+
}
|
|
44
|
+
if (typeof image === "string") {
|
|
45
|
+
return {
|
|
46
|
+
...metadata,
|
|
47
|
+
openGraph: {
|
|
48
|
+
...metadata.openGraph,
|
|
49
|
+
images: [{ url: image, width: 1200, height: 630, alt: extractTitle(metadata) }]
|
|
50
|
+
},
|
|
51
|
+
twitter: {
|
|
52
|
+
card: "summary_large_image",
|
|
53
|
+
...metadata.twitter,
|
|
54
|
+
images: [image]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
40
58
|
const resolvedParams = {
|
|
41
59
|
title: extractTitle(metadata),
|
|
42
|
-
...
|
|
60
|
+
...ogParams
|
|
43
61
|
};
|
|
44
62
|
const ogMeta = createOgMetadata(resolvedParams);
|
|
45
63
|
return {
|
|
@@ -62,9 +80,65 @@ function extractTitle(metadata) {
|
|
|
62
80
|
if (typeof t === "object" && "default" in t) return t.default;
|
|
63
81
|
return "";
|
|
64
82
|
}
|
|
83
|
+
|
|
84
|
+
// src/og-image/metadata-factory.ts
|
|
85
|
+
function buildAlternates(config, path, locale) {
|
|
86
|
+
if (!config.locales?.length) {
|
|
87
|
+
return { canonical: `${config.siteUrl}/${locale}${path}` };
|
|
88
|
+
}
|
|
89
|
+
const languages = {};
|
|
90
|
+
for (const loc of config.locales) {
|
|
91
|
+
languages[loc] = `${config.siteUrl}/${loc}${path}`;
|
|
92
|
+
}
|
|
93
|
+
const xDefault = config.defaultLocale ?? (config.locales.includes("en") ? "en" : config.locales[0]);
|
|
94
|
+
languages["x-default"] = `${config.siteUrl}/${xDefault}${path}`;
|
|
95
|
+
return { canonical: `${config.siteUrl}/${locale}${path}`, languages };
|
|
96
|
+
}
|
|
97
|
+
function createMetadata(config, page = {}) {
|
|
98
|
+
const title = page.title ?? config.name;
|
|
99
|
+
const description = page.description ?? config.description;
|
|
100
|
+
const locale = page.locale ?? config.defaultLocale ?? "en";
|
|
101
|
+
const path = page.path ?? "";
|
|
102
|
+
const base = {
|
|
103
|
+
title,
|
|
104
|
+
description,
|
|
105
|
+
keywords: page.keywords,
|
|
106
|
+
alternates: buildAlternates(config, path, locale),
|
|
107
|
+
openGraph: {
|
|
108
|
+
type: "website",
|
|
109
|
+
locale,
|
|
110
|
+
url: `${config.siteUrl}/${locale}${path}`,
|
|
111
|
+
siteName: config.name,
|
|
112
|
+
title,
|
|
113
|
+
description
|
|
114
|
+
},
|
|
115
|
+
twitter: {
|
|
116
|
+
card: "summary_large_image",
|
|
117
|
+
title,
|
|
118
|
+
description
|
|
119
|
+
},
|
|
120
|
+
icons: config.icons
|
|
121
|
+
};
|
|
122
|
+
let ogOptions;
|
|
123
|
+
if (page.og === void 0 || page.og === false) {
|
|
124
|
+
ogOptions = { image: config.ogImage };
|
|
125
|
+
} else if (page.og === true) {
|
|
126
|
+
ogOptions = {};
|
|
127
|
+
} else if (typeof page.og === "string") {
|
|
128
|
+
ogOptions = { image: page.og };
|
|
129
|
+
} else {
|
|
130
|
+
ogOptions = page.og;
|
|
131
|
+
}
|
|
132
|
+
return withOgImage(base, ogOptions);
|
|
133
|
+
}
|
|
134
|
+
function makeMetadataFactory(config) {
|
|
135
|
+
return (page = {}) => createMetadata(config, page);
|
|
136
|
+
}
|
|
65
137
|
export {
|
|
66
138
|
buildOgUrl,
|
|
139
|
+
createMetadata,
|
|
67
140
|
createOgMetadata,
|
|
141
|
+
makeMetadataFactory,
|
|
68
142
|
withOgImage
|
|
69
143
|
};
|
|
70
144
|
//# 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 } 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 * Options for `withOgImage`. Extends the dynamic-renderer params with an\n * `image` opt-out so a page can keep its own (static) OG image instead of the\n * dynamically-rendered one.\n */\nexport type WithOgImageOptions = Omit<OGImageParams, 'title'> & {\n title?: string\n /**\n * Controls the OG image source:\n * - omitted / `true` → dynamic render via Django's /cfg/og/ (default)\n * - `false` → don't touch images; the page/layout's own og:image\n * is preserved (use for a ready-made static brand image)\n * - `string` → use this exact URL as a static og/twitter image\n */\n image?: boolean | string\n}\n\n/**\n * Merges og:image metadata into an existing Metadata object.\n *\n * Usage:\n * // dynamic (default)\n * return withOgImage({ title: 'Page', description: '...' }, { preset: 'DARK_BLUE' })\n * // keep the page's own static image\n * return withOgImage(meta, { preset: 'DARK_BLUE', image: false })\n * // explicit static image\n * return withOgImage(meta, { image: '/static/ogimage.png' })\n */\nexport function withOgImage(\n metadata: Metadata,\n params: WithOgImageOptions = {}\n): Metadata {\n const { image, ...ogParams } = params\n\n // Opt-out: leave the incoming metadata's images untouched.\n if (image === false) {\n return metadata\n }\n\n // Explicit static URL: set og/twitter images without hitting the renderer.\n if (typeof image === 'string') {\n return {\n ...metadata,\n openGraph: {\n ...metadata.openGraph,\n images: [{ url: image, width: 1200, height: 630, alt: extractTitle(metadata) }],\n },\n twitter: {\n card: 'summary_large_image',\n ...metadata.twitter,\n images: [image],\n },\n }\n }\n\n // Default: dynamic render. Auto-extract title; params.title overrides.\n const resolvedParams: OGImageParams = {\n title: extractTitle(metadata),\n ...ogParams,\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","import type { Metadata } from 'next'\nimport type { OGImageParams } from './types'\nimport { withOgImage, type WithOgImageOptions } 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 (first is treated as x-default if no enDefault). */\n locales?: readonly string[]\n /** Locale used for x-default hreflang (defaults to \"en\" or first locale). */\n defaultLocale?: string\n}\n\n/** Per-page inputs. Everything is optional except what makes the page unique. */\nexport interface PageMeta {\n /** Page title (without the brand suffix — the template adds it). */\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. Drives alternates. */\n path?: string\n /**\n * OG image control, forwarded to `withOgImage`:\n * - omitted → static brand image (`config.ogImage`) — the safe default\n * - `true` → dynamic render from the page title\n * - OGImageParams → dynamic render with explicit preset/layout/colors\n * - `false` → no image (inherit whatever the layout set)\n * - string → explicit image URL\n */\n og?: boolean | string | Omit<OGImageParams, 'title'>\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}/${locale}${path}` }\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 = 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` object from a one-time site config plus\n * per-page inputs — title-template, description, openGraph, twitter, canonical\n * + hreflang alternates, and icons — so pages only declare what's unique.\n *\n * OG image defaults to the static brand image (`config.ogImage`); opt into the\n * dynamic renderer per page with `og: true` / `og: { preset: 'DARK_BLUE' }`.\n *\n * Bind the config once in your app, then call the bound fn from each page:\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 * // any page.tsx\n * export const metadata = createMetadata({ title: 'Skills', path: '/skills' })\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\n const base: Metadata = {\n title,\n description,\n keywords: page.keywords,\n alternates: buildAlternates(config, path, locale),\n openGraph: {\n type: 'website',\n locale,\n url: `${config.siteUrl}/${locale}${path}`,\n siteName: config.name,\n title,\n description,\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n },\n icons: config.icons,\n }\n\n // Resolve the `og` shorthand into withOgImage options.\n let ogOptions: WithOgImageOptions\n if (page.og === undefined || page.og === false) {\n // Default & explicit-off both map to the static brand image. (false on the\n // factory means \"don't render dynamic\" — but we still want a brand image,\n // so we point at the static one. Use og:false at the page only if you've\n // set images elsewhere; here static is the friendly default.)\n ogOptions = { image: config.ogImage }\n } else if (page.og === true) {\n ogOptions = {} // dynamic, title auto-extracted\n } else if (typeof page.og === 'string') {\n ogOptions = { image: page.og }\n } else {\n ogOptions = page.og // OGImageParams → dynamic with overrides\n }\n\n return withOgImage(base, ogOptions)\n}\n\n/**\n * Curries `createMetadata` with a fixed site config so pages call a zero-config\n * factory. See the docstring on `createMetadata` for the usage pattern.\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.438",
|
|
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.438",
|
|
147
|
+
"@djangocfg/monitor": "^2.1.438",
|
|
148
|
+
"@djangocfg/ui-core": "^2.1.438",
|
|
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.438",
|
|
171
|
+
"@djangocfg/monitor": "^2.1.438",
|
|
172
|
+
"@djangocfg/ui-core": "^2.1.438",
|
|
173
|
+
"@djangocfg/layouts": "^2.1.438",
|
|
174
|
+
"@djangocfg/typescript-config": "^2.1.438",
|
|
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/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export type { OGImageParams, OGPreset, OGLayout } from './types'
|
|
2
2
|
export { buildOgUrl } from './url'
|
|
3
3
|
export { createOgMetadata, withOgImage } from './metadata'
|
|
4
|
+
export type { WithOgImageOptions } from './metadata'
|
|
5
|
+
export { createMetadata, makeMetadataFactory } from './metadata-factory'
|
|
6
|
+
export type { SiteMetaConfig, PageMeta } from './metadata-factory'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import type { OGImageParams } from './types'
|
|
3
|
+
import { withOgImage, type WithOgImageOptions } from './metadata'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Site-wide identity the metadata factory needs. Config-agnostic on purpose —
|
|
7
|
+
* the package doesn't know about any app's `settings` object, so the consumer
|
|
8
|
+
* passes these once (typically derived from their own settings module).
|
|
9
|
+
*/
|
|
10
|
+
export interface SiteMetaConfig {
|
|
11
|
+
/** Brand / app name, e.g. "CMDOP". Used in titles, siteName, alt text. */
|
|
12
|
+
name: string
|
|
13
|
+
/** Default description used when a page doesn't supply its own. */
|
|
14
|
+
description: string
|
|
15
|
+
/** Absolute site origin, e.g. "https://cmdop.com". Used for canonical/og url. */
|
|
16
|
+
siteUrl: string
|
|
17
|
+
/** Ready-made static brand OG image URL (absolute or root-relative). */
|
|
18
|
+
ogImage: string
|
|
19
|
+
/** Optional favicon / apple-touch icons forwarded to Metadata.icons. */
|
|
20
|
+
icons?: Metadata['icons']
|
|
21
|
+
/** Locales for hreflang alternates (first is treated as x-default if no enDefault). */
|
|
22
|
+
locales?: readonly string[]
|
|
23
|
+
/** Locale used for x-default hreflang (defaults to "en" or first locale). */
|
|
24
|
+
defaultLocale?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Per-page inputs. Everything is optional except what makes the page unique. */
|
|
28
|
+
export interface PageMeta {
|
|
29
|
+
/** Page title (without the brand suffix — the template adds it). */
|
|
30
|
+
title?: string
|
|
31
|
+
/** Page description; falls back to the site description. */
|
|
32
|
+
description?: string
|
|
33
|
+
keywords?: string[]
|
|
34
|
+
/** Current locale, used for canonical + openGraph.locale + alternates. */
|
|
35
|
+
locale?: string
|
|
36
|
+
/** Path without locale prefix, e.g. "/skills". '' for home. Drives alternates. */
|
|
37
|
+
path?: string
|
|
38
|
+
/**
|
|
39
|
+
* OG image control, forwarded to `withOgImage`:
|
|
40
|
+
* - omitted → static brand image (`config.ogImage`) — the safe default
|
|
41
|
+
* - `true` → dynamic render from the page title
|
|
42
|
+
* - OGImageParams → dynamic render with explicit preset/layout/colors
|
|
43
|
+
* - `false` → no image (inherit whatever the layout set)
|
|
44
|
+
* - string → explicit image URL
|
|
45
|
+
*/
|
|
46
|
+
og?: boolean | string | Omit<OGImageParams, 'title'>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildAlternates(
|
|
50
|
+
config: SiteMetaConfig,
|
|
51
|
+
path: string,
|
|
52
|
+
locale: string
|
|
53
|
+
): Metadata['alternates'] {
|
|
54
|
+
if (!config.locales?.length) {
|
|
55
|
+
return { canonical: `${config.siteUrl}/${locale}${path}` }
|
|
56
|
+
}
|
|
57
|
+
const languages: Record<string, string> = {}
|
|
58
|
+
for (const loc of config.locales) {
|
|
59
|
+
languages[loc] = `${config.siteUrl}/${loc}${path}`
|
|
60
|
+
}
|
|
61
|
+
const xDefault = config.defaultLocale ?? (config.locales.includes('en') ? 'en' : config.locales[0])
|
|
62
|
+
languages['x-default'] = `${config.siteUrl}/${xDefault}${path}`
|
|
63
|
+
return { canonical: `${config.siteUrl}/${locale}${path}`, languages }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds a complete Next.js `Metadata` object from a one-time site config plus
|
|
68
|
+
* per-page inputs — title-template, description, openGraph, twitter, canonical
|
|
69
|
+
* + hreflang alternates, and icons — so pages only declare what's unique.
|
|
70
|
+
*
|
|
71
|
+
* OG image defaults to the static brand image (`config.ogImage`); opt into the
|
|
72
|
+
* dynamic renderer per page with `og: true` / `og: { preset: 'DARK_BLUE' }`.
|
|
73
|
+
*
|
|
74
|
+
* Bind the config once in your app, then call the bound fn from each page:
|
|
75
|
+
*
|
|
76
|
+
* // app/_core/metadata.ts
|
|
77
|
+
* export const createMetadata = makeMetadataFactory({
|
|
78
|
+
* name: settings.app.name,
|
|
79
|
+
* description: settings.app.description,
|
|
80
|
+
* siteUrl: settings.app.siteUrl,
|
|
81
|
+
* ogImage: settings.app.media.ogimage,
|
|
82
|
+
* icons: { icon: settings.app.media.favicon },
|
|
83
|
+
* locales: LOCALES,
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* // any page.tsx
|
|
87
|
+
* export const metadata = createMetadata({ title: 'Skills', path: '/skills' })
|
|
88
|
+
*/
|
|
89
|
+
export function createMetadata(config: SiteMetaConfig, page: PageMeta = {}): Metadata {
|
|
90
|
+
const title = page.title ?? config.name
|
|
91
|
+
const description = page.description ?? config.description
|
|
92
|
+
const locale = page.locale ?? config.defaultLocale ?? 'en'
|
|
93
|
+
const path = page.path ?? ''
|
|
94
|
+
|
|
95
|
+
const base: Metadata = {
|
|
96
|
+
title,
|
|
97
|
+
description,
|
|
98
|
+
keywords: page.keywords,
|
|
99
|
+
alternates: buildAlternates(config, path, locale),
|
|
100
|
+
openGraph: {
|
|
101
|
+
type: 'website',
|
|
102
|
+
locale,
|
|
103
|
+
url: `${config.siteUrl}/${locale}${path}`,
|
|
104
|
+
siteName: config.name,
|
|
105
|
+
title,
|
|
106
|
+
description,
|
|
107
|
+
},
|
|
108
|
+
twitter: {
|
|
109
|
+
card: 'summary_large_image',
|
|
110
|
+
title,
|
|
111
|
+
description,
|
|
112
|
+
},
|
|
113
|
+
icons: config.icons,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Resolve the `og` shorthand into withOgImage options.
|
|
117
|
+
let ogOptions: WithOgImageOptions
|
|
118
|
+
if (page.og === undefined || page.og === false) {
|
|
119
|
+
// Default & explicit-off both map to the static brand image. (false on the
|
|
120
|
+
// factory means "don't render dynamic" — but we still want a brand image,
|
|
121
|
+
// so we point at the static one. Use og:false at the page only if you've
|
|
122
|
+
// set images elsewhere; here static is the friendly default.)
|
|
123
|
+
ogOptions = { image: config.ogImage }
|
|
124
|
+
} else if (page.og === true) {
|
|
125
|
+
ogOptions = {} // dynamic, title auto-extracted
|
|
126
|
+
} else if (typeof page.og === 'string') {
|
|
127
|
+
ogOptions = { image: page.og }
|
|
128
|
+
} else {
|
|
129
|
+
ogOptions = page.og // OGImageParams → dynamic with overrides
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return withOgImage(base, ogOptions)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Curries `createMetadata` with a fixed site config so pages call a zero-config
|
|
137
|
+
* factory. See the docstring on `createMetadata` for the usage pattern.
|
|
138
|
+
*/
|
|
139
|
+
export function makeMetadataFactory(config: SiteMetaConfig) {
|
|
140
|
+
return (page: PageMeta = {}): Metadata => createMetadata(config, page)
|
|
141
|
+
}
|
package/src/og-image/metadata.ts
CHANGED
|
@@ -23,23 +23,65 @@ export function createOgMetadata(params: OGImageParams): Metadata {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Options for `withOgImage`. Extends the dynamic-renderer params with an
|
|
28
|
+
* `image` opt-out so a page can keep its own (static) OG image instead of the
|
|
29
|
+
* dynamically-rendered one.
|
|
30
|
+
*/
|
|
31
|
+
export type WithOgImageOptions = Omit<OGImageParams, 'title'> & {
|
|
32
|
+
title?: string
|
|
33
|
+
/**
|
|
34
|
+
* Controls the OG image source:
|
|
35
|
+
* - omitted / `true` → dynamic render via Django's /cfg/og/ (default)
|
|
36
|
+
* - `false` → don't touch images; the page/layout's own og:image
|
|
37
|
+
* is preserved (use for a ready-made static brand image)
|
|
38
|
+
* - `string` → use this exact URL as a static og/twitter image
|
|
39
|
+
*/
|
|
40
|
+
image?: boolean | string
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
/**
|
|
27
44
|
* Merges og:image metadata into an existing Metadata object.
|
|
28
45
|
*
|
|
29
46
|
* Usage:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* )
|
|
47
|
+
* // dynamic (default)
|
|
48
|
+
* return withOgImage({ title: 'Page', description: '...' }, { preset: 'DARK_BLUE' })
|
|
49
|
+
* // keep the page's own static image
|
|
50
|
+
* return withOgImage(meta, { preset: 'DARK_BLUE', image: false })
|
|
51
|
+
* // explicit static image
|
|
52
|
+
* return withOgImage(meta, { image: '/static/ogimage.png' })
|
|
34
53
|
*/
|
|
35
54
|
export function withOgImage(
|
|
36
55
|
metadata: Metadata,
|
|
37
|
-
params:
|
|
56
|
+
params: WithOgImageOptions = {}
|
|
38
57
|
): Metadata {
|
|
39
|
-
|
|
58
|
+
const { image, ...ogParams } = params
|
|
59
|
+
|
|
60
|
+
// Opt-out: leave the incoming metadata's images untouched.
|
|
61
|
+
if (image === false) {
|
|
62
|
+
return metadata
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Explicit static URL: set og/twitter images without hitting the renderer.
|
|
66
|
+
if (typeof image === 'string') {
|
|
67
|
+
return {
|
|
68
|
+
...metadata,
|
|
69
|
+
openGraph: {
|
|
70
|
+
...metadata.openGraph,
|
|
71
|
+
images: [{ url: image, width: 1200, height: 630, alt: extractTitle(metadata) }],
|
|
72
|
+
},
|
|
73
|
+
twitter: {
|
|
74
|
+
card: 'summary_large_image',
|
|
75
|
+
...metadata.twitter,
|
|
76
|
+
images: [image],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default: dynamic render. Auto-extract title; params.title overrides.
|
|
40
82
|
const resolvedParams: OGImageParams = {
|
|
41
83
|
title: extractTitle(metadata),
|
|
42
|
-
...
|
|
84
|
+
...ogParams,
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
const ogMeta = createOgMetadata(resolvedParams)
|