@djangocfg/nextjs 2.1.438 → 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/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 +80 -67
- package/dist/og-image/index.mjs +50 -62
- 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 +9 -4
- package/src/og-image/metadata-factory.ts +49 -44
- package/src/og-image/metadata.ts +64 -85
- package/src/og-image/types.ts +15 -0
package/src/og-image/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export { createOgMetadata, withOgImage } from './metadata'
|
|
4
|
-
export type { WithOgImageOptions } from './metadata'
|
|
1
|
+
// ── Recommended API ──────────────────────────────────────────────────────────
|
|
2
|
+
// Bind the factory once per app, then call it from each page.
|
|
5
3
|
export { createMetadata, makeMetadataFactory } from './metadata-factory'
|
|
6
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) ────────────────────────────────────
|
|
10
|
+
export { buildOgUrl } from './url'
|
|
11
|
+
export { createOgMetadata, withOgImage, resolveOgImage, extractTitle } from './metadata'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { OgSpec } from './types'
|
|
3
|
+
import { resolveOgImage } from './metadata'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Site-wide identity the metadata factory needs. Config-agnostic on purpose —
|
|
@@ -18,32 +18,36 @@ export interface SiteMetaConfig {
|
|
|
18
18
|
ogImage: string
|
|
19
19
|
/** Optional favicon / apple-touch icons forwarded to Metadata.icons. */
|
|
20
20
|
icons?: Metadata['icons']
|
|
21
|
-
/** Locales for hreflang alternates
|
|
21
|
+
/** Locales for hreflang alternates. Omit for single-locale sites. */
|
|
22
22
|
locales?: readonly string[]
|
|
23
23
|
/** Locale used for x-default hreflang (defaults to "en" or first locale). */
|
|
24
24
|
defaultLocale?: string
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Per-page inputs. Only `title` is really worth setting per page; everything
|
|
29
|
+
* else falls back to the site config.
|
|
30
|
+
*/
|
|
28
31
|
export interface PageMeta {
|
|
29
|
-
/** Page title (without the brand suffix
|
|
32
|
+
/** Page title (without the brand suffix). Falls back to the site name. */
|
|
30
33
|
title?: string
|
|
31
34
|
/** Page description; falls back to the site description. */
|
|
32
35
|
description?: string
|
|
33
36
|
keywords?: string[]
|
|
34
37
|
/** Current locale, used for canonical + openGraph.locale + alternates. */
|
|
35
38
|
locale?: string
|
|
36
|
-
/** Path without locale prefix, e.g. "/skills". '' for home.
|
|
39
|
+
/** Path without locale prefix, e.g. "/skills". '' for home. */
|
|
37
40
|
path?: string
|
|
38
41
|
/**
|
|
39
|
-
* OG image
|
|
40
|
-
* -
|
|
41
|
-
* - `
|
|
42
|
-
* -
|
|
43
|
-
* - `
|
|
44
|
-
* - string → explicit image URL
|
|
42
|
+
* What the OG image should be. Defaults to `'static'` (the brand image).
|
|
43
|
+
* - `'static'` brand image from config (default)
|
|
44
|
+
* - `'dynamic'` render the page title onto the card
|
|
45
|
+
* - `{ render: {...} }` render with explicit preset/layout/colors
|
|
46
|
+
* - `{ image, alt? }` an exact image URL (e.g. a content screenshot)
|
|
45
47
|
*/
|
|
46
|
-
og?:
|
|
48
|
+
og?: OgSpec
|
|
49
|
+
/** Escape hatch: extra Metadata fields merged last (robots, authors, etc.). */
|
|
50
|
+
extra?: Metadata
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function buildAlternates(
|
|
@@ -52,26 +56,25 @@ function buildAlternates(
|
|
|
52
56
|
locale: string
|
|
53
57
|
): Metadata['alternates'] {
|
|
54
58
|
if (!config.locales?.length) {
|
|
55
|
-
return { canonical: `${config.siteUrl}/${
|
|
59
|
+
return { canonical: `${config.siteUrl}/${path}`.replace(/\/+$/, '') || config.siteUrl }
|
|
56
60
|
}
|
|
57
61
|
const languages: Record<string, string> = {}
|
|
58
62
|
for (const loc of config.locales) {
|
|
59
63
|
languages[loc] = `${config.siteUrl}/${loc}${path}`
|
|
60
64
|
}
|
|
61
|
-
const xDefault =
|
|
65
|
+
const xDefault =
|
|
66
|
+
config.defaultLocale ?? (config.locales.includes('en') ? 'en' : config.locales[0])
|
|
62
67
|
languages['x-default'] = `${config.siteUrl}/${xDefault}${path}`
|
|
63
68
|
return { canonical: `${config.siteUrl}/${locale}${path}`, languages }
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
|
-
* Builds a complete Next.js `Metadata`
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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' }`.
|
|
72
|
+
* Builds a complete Next.js `Metadata` from a one-time site config plus per-page
|
|
73
|
+
* inputs — title-template, description, openGraph, twitter, canonical + hreflang,
|
|
74
|
+
* icons, and the OG image — so pages only declare what's unique.
|
|
73
75
|
*
|
|
74
|
-
*
|
|
76
|
+
* The OG image is chosen by the single `og` field (defaults to the static brand
|
|
77
|
+
* image). There is exactly one way to do each thing.
|
|
75
78
|
*
|
|
76
79
|
* // app/_core/metadata.ts
|
|
77
80
|
* export const createMetadata = makeMetadataFactory({
|
|
@@ -83,16 +86,25 @@ function buildAlternates(
|
|
|
83
86
|
* locales: LOCALES,
|
|
84
87
|
* })
|
|
85
88
|
*
|
|
86
|
-
* //
|
|
87
|
-
* export const metadata = createMetadata({ title: '
|
|
89
|
+
* // landing page — static brand image (default)
|
|
90
|
+
* export const metadata = createMetadata({ title: 'Pricing', path: '/pricing' })
|
|
91
|
+
* // content page — title rendered onto the card
|
|
92
|
+
* export const metadata = createMetadata({ title: post.title, path, og: 'dynamic' })
|
|
93
|
+
* // content page with its own image
|
|
94
|
+
* export const metadata = createMetadata({ title: p.name, og: { image: p.image } })
|
|
88
95
|
*/
|
|
89
96
|
export function createMetadata(config: SiteMetaConfig, page: PageMeta = {}): Metadata {
|
|
90
97
|
const title = page.title ?? config.name
|
|
91
98
|
const description = page.description ?? config.description
|
|
92
99
|
const locale = page.locale ?? config.defaultLocale ?? 'en'
|
|
93
100
|
const path = page.path ?? ''
|
|
101
|
+
const ogUrl = config.locales?.length
|
|
102
|
+
? `${config.siteUrl}/${locale}${path}`
|
|
103
|
+
: `${config.siteUrl}${path}`
|
|
94
104
|
|
|
95
|
-
const
|
|
105
|
+
const ogImage = resolveOgImage(page.og ?? 'static', config.ogImage, title)
|
|
106
|
+
|
|
107
|
+
const metadata: Metadata = {
|
|
96
108
|
title,
|
|
97
109
|
description,
|
|
98
110
|
keywords: page.keywords,
|
|
@@ -100,41 +112,34 @@ export function createMetadata(config: SiteMetaConfig, page: PageMeta = {}): Met
|
|
|
100
112
|
openGraph: {
|
|
101
113
|
type: 'website',
|
|
102
114
|
locale,
|
|
103
|
-
url:
|
|
115
|
+
url: ogUrl,
|
|
104
116
|
siteName: config.name,
|
|
105
117
|
title,
|
|
106
118
|
description,
|
|
119
|
+
...ogImage.openGraph,
|
|
107
120
|
},
|
|
108
121
|
twitter: {
|
|
109
|
-
card: 'summary_large_image',
|
|
110
122
|
title,
|
|
111
123
|
description,
|
|
124
|
+
...ogImage.twitter,
|
|
112
125
|
},
|
|
113
126
|
icons: config.icons,
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
129
|
+
if (!page.extra) return metadata
|
|
130
|
+
// Shallow-merge the escape hatch, but deep-merge the nested og/twitter so the
|
|
131
|
+
// image we just set isn't clobbered by a partial `extra.openGraph`.
|
|
132
|
+
return {
|
|
133
|
+
...metadata,
|
|
134
|
+
...page.extra,
|
|
135
|
+
openGraph: { ...metadata.openGraph, ...page.extra.openGraph },
|
|
136
|
+
twitter: { ...metadata.twitter, ...page.extra.twitter },
|
|
130
137
|
}
|
|
131
|
-
|
|
132
|
-
return withOgImage(base, ogOptions)
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
141
|
* Curries `createMetadata` with a fixed site config so pages call a zero-config
|
|
137
|
-
* factory.
|
|
142
|
+
* factory. This is the single recommended entry point for app metadata.
|
|
138
143
|
*/
|
|
139
144
|
export function makeMetadataFactory(config: SiteMetaConfig) {
|
|
140
145
|
return (page: PageMeta = {}): Metadata => createMetadata(config, page)
|
package/src/og-image/metadata.ts
CHANGED
|
@@ -1,109 +1,88 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
|
-
import type { OGImageParams } from './types'
|
|
2
|
+
import type { OGImageParams, OgSpec } from './types'
|
|
3
3
|
import { buildOgUrl } from './url'
|
|
4
4
|
|
|
5
|
+
const OG_WIDTH = 1200
|
|
6
|
+
const OG_HEIGHT = 630
|
|
7
|
+
|
|
8
|
+
/** A Next.js OG image entry. */
|
|
9
|
+
function imageEntry(url: string, alt: string) {
|
|
10
|
+
return { url, width: OG_WIDTH, height: OG_HEIGHT, alt }
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Usage in generateMetadata():
|
|
10
|
-
* return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })
|
|
14
|
+
* Builds the og/twitter image fragment for a given image URL — the one place
|
|
15
|
+
* that knows the shape Next.js expects. Both static URLs and the dynamic
|
|
16
|
+
* renderer funnel through here, so there's a single source of truth.
|
|
11
17
|
*/
|
|
12
|
-
|
|
13
|
-
const url = buildOgUrl(params)
|
|
14
|
-
const image = { url, width: 1200, height: 630, alt: params.title }
|
|
18
|
+
function ogImageFragment(url: string, alt: string): Pick<Metadata, 'openGraph' | 'twitter'> {
|
|
15
19
|
return {
|
|
16
|
-
openGraph: {
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
twitter: {
|
|
20
|
-
card: 'summary_large_image',
|
|
21
|
-
images: [url],
|
|
22
|
-
},
|
|
20
|
+
openGraph: { images: [imageEntry(url, alt)] },
|
|
21
|
+
twitter: { card: 'summary_large_image', images: [url] },
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* Metadata fragment whose OG image is rendered by Django's django_ogimage
|
|
27
|
+
* endpoint (/cfg/og/). Low-level — most callers use `createMetadata`'s `og`
|
|
28
|
+
* field instead.
|
|
30
29
|
*/
|
|
31
|
-
export
|
|
32
|
-
title
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
export function createOgMetadata(params: OGImageParams): Metadata {
|
|
31
|
+
return ogImageFragment(buildOgUrl(params), params.title)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract a plain title string from Next.js's title shapes. */
|
|
35
|
+
export function extractTitle(metadata: Metadata): string {
|
|
36
|
+
const t = metadata.title
|
|
37
|
+
if (!t) return ''
|
|
38
|
+
if (typeof t === 'string') return t
|
|
39
|
+
if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute
|
|
40
|
+
if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default
|
|
41
|
+
return ''
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* Resolves an `OgSpec` against a base Metadata into a concrete og/twitter image
|
|
46
|
+
* fragment. `staticUrl` is the site's brand image, used for the `'static'` case.
|
|
47
|
+
* Returns `null` only if there's genuinely nothing to set.
|
|
45
48
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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' })
|
|
49
|
+
* This is the single decision point for "what is the OG image" — no other code
|
|
50
|
+
* branches on the spec.
|
|
53
51
|
*/
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (image === false) {
|
|
62
|
-
return metadata
|
|
52
|
+
export function resolveOgImage(
|
|
53
|
+
spec: OgSpec,
|
|
54
|
+
staticUrl: string,
|
|
55
|
+
fallbackTitle: string
|
|
56
|
+
): Pick<Metadata, 'openGraph' | 'twitter'> {
|
|
57
|
+
if (spec === 'static') {
|
|
58
|
+
return ogImageFragment(staticUrl, fallbackTitle)
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
60
|
+
if (spec === 'dynamic') {
|
|
61
|
+
return ogImageFragment(buildOgUrl({ title: fallbackTitle }), fallbackTitle)
|
|
79
62
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const resolvedParams: OGImageParams = {
|
|
83
|
-
title: extractTitle(metadata),
|
|
84
|
-
...ogParams,
|
|
63
|
+
if ('image' in spec) {
|
|
64
|
+
return ogImageFragment(spec.image, spec.alt ?? fallbackTitle)
|
|
85
65
|
}
|
|
66
|
+
// { render: {...} } — dynamic with explicit params; title defaults to page title.
|
|
67
|
+
const title = spec.render.title ?? fallbackTitle
|
|
68
|
+
return ogImageFragment(buildOgUrl({ ...spec.render, title }), title)
|
|
69
|
+
}
|
|
86
70
|
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Merges OG image metadata (dynamic render) into existing Metadata. Kept for
|
|
73
|
+
* low-level/manual use; `createMetadata` is the recommended entry point.
|
|
74
|
+
*
|
|
75
|
+
* return withOgImage({ title: 'Page' }, { preset: 'DARK_BLUE' })
|
|
76
|
+
*/
|
|
77
|
+
export function withOgImage(
|
|
78
|
+
metadata: Metadata,
|
|
79
|
+
params: Omit<OGImageParams, 'title'> & { title?: string } = {}
|
|
80
|
+
): Metadata {
|
|
81
|
+
const title = params.title ?? extractTitle(metadata)
|
|
82
|
+
const frag = createOgMetadata({ ...params, title })
|
|
89
83
|
return {
|
|
90
84
|
...metadata,
|
|
91
|
-
openGraph: {
|
|
92
|
-
|
|
93
|
-
...ogMeta.openGraph,
|
|
94
|
-
},
|
|
95
|
-
twitter: {
|
|
96
|
-
...metadata.twitter,
|
|
97
|
-
...ogMeta.twitter,
|
|
98
|
-
},
|
|
85
|
+
openGraph: { ...metadata.openGraph, ...frag.openGraph },
|
|
86
|
+
twitter: { ...metadata.twitter, ...frag.twitter },
|
|
99
87
|
}
|
|
100
88
|
}
|
|
101
|
-
|
|
102
|
-
function extractTitle(metadata: Metadata): string {
|
|
103
|
-
const t = metadata.title
|
|
104
|
-
if (!t) return ''
|
|
105
|
-
if (typeof t === 'string') return t
|
|
106
|
-
if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute
|
|
107
|
-
if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default
|
|
108
|
-
return ''
|
|
109
|
-
}
|
package/src/og-image/types.ts
CHANGED
|
@@ -11,6 +11,21 @@ export type OGPreset =
|
|
|
11
11
|
|
|
12
12
|
export type OGLayout = 'DEFAULT' | 'HERO' | 'ARTICLE' | 'MINIMAL'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Per-page OG image intent — the single axis that decides what the OG image is.
|
|
16
|
+
* A discriminated union so every case is explicit and type-checked:
|
|
17
|
+
*
|
|
18
|
+
* 'static' brand image from site config (the default)
|
|
19
|
+
* 'dynamic' render the page title onto the OG card
|
|
20
|
+
* { render: {...} } render with explicit preset/layout/colors
|
|
21
|
+
* { image, alt? } an exact image URL (e.g. a content screenshot)
|
|
22
|
+
*/
|
|
23
|
+
export type OgSpec =
|
|
24
|
+
| 'static'
|
|
25
|
+
| 'dynamic'
|
|
26
|
+
| { render: Omit<OGImageParams, 'title'> & { title?: string } }
|
|
27
|
+
| { image: string; alt?: string }
|
|
28
|
+
|
|
14
29
|
/** Mirrors Django OGImageParams exactly */
|
|
15
30
|
export interface OGImageParams {
|
|
16
31
|
title: string
|