@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.
@@ -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
- * return withOgImage(
44
- * { title: 'Page', description: '...' },
45
- * { preset: 'DARK_BLUE', layout: 'HERO' }
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: Omit<OGImageParams, 'title'> & {
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
- }): Metadata;
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 };
@@ -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
- ...params
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.437",
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.437",
147
- "@djangocfg/monitor": "^2.1.437",
148
- "@djangocfg/ui-core": "^2.1.437",
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.437",
171
- "@djangocfg/monitor": "^2.1.437",
172
- "@djangocfg/ui-core": "^2.1.437",
173
- "@djangocfg/layouts": "^2.1.437",
174
- "@djangocfg/typescript-config": "^2.1.437",
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",
@@ -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
+ }
@@ -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
- * return withOgImage(
31
- * { title: 'Page', description: '...' },
32
- * { preset: 'DARK_BLUE', layout: 'HERO' }
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: Omit<OGImageParams, 'title'> & { title?: string }
56
+ params: WithOgImageOptions = {}
38
57
  ): Metadata {
39
- // Auto-extract title from metadata; params.title overrides if explicitly provided
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
- ...params,
84
+ ...ogParams,
43
85
  }
44
86
 
45
87
  const ogMeta = createOgMetadata(resolvedParams)