@rimelight/seo 0.0.4 → 0.0.6
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/index.d.mts +8 -38
- package/dist/index.mjs +8 -175
- package/dist/integration.d.mts +6 -0
- package/dist/integration.mjs +101 -0
- package/dist/llms-BVLxzjr-.d.mts +41 -0
- package/dist/llms.d.mts +2 -0
- package/dist/llms.mjs +75 -0
- package/dist/meta.d.mts +42 -0
- package/dist/meta.mjs +66 -0
- package/dist/og.d.mts +117 -0
- package/dist/og.mjs +302 -0
- package/dist/robots.d.mts +3 -4
- package/dist/routes/llms-full.txt.d.mts +5 -0
- package/dist/routes/llms-full.txt.mjs +57 -0
- package/dist/routes/llms.txt.d.mts +5 -0
- package/dist/routes/llms.txt.mjs +68 -0
- package/dist/routes/robots.txt.d.mts +4 -0
- package/dist/routes/robots.txt.mjs +46 -0
- package/dist/routes/rss.xml.d.mts +4 -0
- package/dist/routes/rss.xml.mjs +51 -0
- package/dist/routes/sitemap.xml.d.mts +4 -0
- package/dist/routes/sitemap.xml.mjs +27 -0
- package/dist/rss.d.mts +8 -0
- package/dist/rss.mjs +44 -0
- package/dist/schema-C8v69blQ.d.mts +28 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +52 -0
- package/dist/sitemap.d.mts +6 -7
- package/dist/types.d.mts +354 -2
- package/package.json +24 -5
- package/src/components/SEOHead.astro +65 -18
- package/src/env.d.ts +33 -3
- package/src/index.ts +19 -1
- package/src/integration.ts +172 -0
- package/src/meta.test.ts +25 -1
- package/src/meta.ts +32 -2
- package/src/og.ts +444 -0
- package/src/routes/llms-full.txt.ts +77 -0
- package/src/routes/llms.txt.ts +90 -0
- package/src/routes/robots.txt.ts +63 -0
- package/src/routes/rss.xml.ts +87 -0
- package/src/routes/sitemap.xml.ts +43 -0
- package/src/rss.test.ts +28 -0
- package/src/rss.ts +63 -0
- package/src/types.ts +367 -242
- package/dist/types-e7gqYZwc.d.mts +0 -305
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { HeadProps } from "../types.js"
|
|
2
|
+
import type { HeadProps, OpenGraphMeta, TwitterMeta } from "../types.js"
|
|
3
3
|
import {
|
|
4
4
|
buildCanonicalUrl,
|
|
5
5
|
buildPageTitle,
|
|
6
6
|
buildSafeDescription,
|
|
7
7
|
buildRobotsMetaContent,
|
|
8
|
-
buildWebSiteSchema
|
|
8
|
+
buildWebSiteSchema,
|
|
9
|
+
deepMerge
|
|
9
10
|
} from "../meta.js"
|
|
10
11
|
import { buildArticleSchema, buildBreadcrumbSchema } from "../schema.js"
|
|
11
12
|
|
|
@@ -29,6 +30,10 @@ const {
|
|
|
29
30
|
breadcrumbs,
|
|
30
31
|
markdownUrl,
|
|
31
32
|
versionAlternates = [],
|
|
33
|
+
absolute = false,
|
|
34
|
+
twitterCard,
|
|
35
|
+
openGraph,
|
|
36
|
+
twitter,
|
|
32
37
|
} = Astro.props as HeadProps
|
|
33
38
|
|
|
34
39
|
// Resolve defaults from config if available
|
|
@@ -50,7 +55,8 @@ const displayTitle = buildPageTitle({
|
|
|
50
55
|
...(title !== undefined && { title }),
|
|
51
56
|
siteName,
|
|
52
57
|
titleTemplate,
|
|
53
|
-
...(is404 !== undefined && { is404 })
|
|
58
|
+
...(is404 !== undefined && { is404 }),
|
|
59
|
+
...(absolute !== undefined && { absolute })
|
|
54
60
|
})
|
|
55
61
|
|
|
56
62
|
const finalDescription = description || siteDescription
|
|
@@ -72,6 +78,47 @@ const robotsContent = buildRobotsMetaContent({
|
|
|
72
78
|
is404,
|
|
73
79
|
...(robots !== undefined && { override: robots })
|
|
74
80
|
})
|
|
81
|
+
|
|
82
|
+
// Deep-merged OpenGraph
|
|
83
|
+
const defaultOpenGraph: OpenGraphMeta = {
|
|
84
|
+
type: ogType,
|
|
85
|
+
locale: ogLocale,
|
|
86
|
+
url: Astro.url.href,
|
|
87
|
+
title: title || siteName,
|
|
88
|
+
...(safeDescription && { description: safeDescription }),
|
|
89
|
+
...(absoluteOgImage && { images: [{ src: absoluteOgImage, alt: finalOgImage?.alt || siteName }] }),
|
|
90
|
+
siteName,
|
|
91
|
+
...config?.openGraph,
|
|
92
|
+
}
|
|
93
|
+
const resolvedOpenGraph = deepMerge(defaultOpenGraph, openGraph || {})
|
|
94
|
+
if (ogImage) {
|
|
95
|
+
resolvedOpenGraph.images = ogImage
|
|
96
|
+
}
|
|
97
|
+
const ogImgUrl = resolvedOpenGraph.images
|
|
98
|
+
? (Array.isArray(resolvedOpenGraph.images) ? resolvedOpenGraph.images[0]?.src : (typeof resolvedOpenGraph.images === 'string' ? resolvedOpenGraph.images : resolvedOpenGraph.images.src))
|
|
99
|
+
: absoluteOgImage
|
|
100
|
+
const ogImgAlt = resolvedOpenGraph.images
|
|
101
|
+
? (Array.isArray(resolvedOpenGraph.images) ? resolvedOpenGraph.images[0]?.alt : (typeof resolvedOpenGraph.images === 'object' && 'alt' in resolvedOpenGraph.images ? resolvedOpenGraph.images.alt : finalOgImage?.alt || siteName))
|
|
102
|
+
: finalOgImage?.alt || siteName
|
|
103
|
+
|
|
104
|
+
const absoluteResolvedOgImage = ogImgUrl ? new URL(ogImgUrl, Astro.site ?? Astro.url).toString() : undefined
|
|
105
|
+
|
|
106
|
+
// Deep-merged Twitter
|
|
107
|
+
const defaultTwitter: TwitterMeta = {
|
|
108
|
+
card: twitterCard || config?.twitter?.card || (absoluteResolvedOgImage ? 'summary_large_image' : 'summary'),
|
|
109
|
+
...(twitterHandle && { site: twitterHandle, creator: twitterHandle }),
|
|
110
|
+
title: resolvedOpenGraph.title || title || siteName,
|
|
111
|
+
...(safeDescription && { description: safeDescription }),
|
|
112
|
+
...(absoluteResolvedOgImage && { images: absoluteResolvedOgImage }),
|
|
113
|
+
...config?.twitter,
|
|
114
|
+
}
|
|
115
|
+
const resolvedTwitter = deepMerge(defaultTwitter, twitter || {})
|
|
116
|
+
if (twitterCard) {
|
|
117
|
+
resolvedTwitter.card = twitterCard
|
|
118
|
+
}
|
|
119
|
+
if (!resolvedTwitter.card) {
|
|
120
|
+
resolvedTwitter.card = absoluteResolvedOgImage ? 'summary_large_image' : 'summary'
|
|
121
|
+
}
|
|
75
122
|
---
|
|
76
123
|
|
|
77
124
|
<!-- Basic Metadata -->
|
|
@@ -103,24 +150,24 @@ const robotsContent = buildRobotsMetaContent({
|
|
|
103
150
|
))}
|
|
104
151
|
|
|
105
152
|
<!-- Open Graph / Facebook -->
|
|
106
|
-
<meta property="og:type" content={
|
|
107
|
-
<meta property="og:locale" content={
|
|
108
|
-
<meta property="og:url" content={Astro.url.href} />
|
|
109
|
-
<meta property="og:title" content={title || siteName} />
|
|
110
|
-
{
|
|
111
|
-
{
|
|
112
|
-
{
|
|
113
|
-
<meta property="og:site_name" content={siteName} />
|
|
153
|
+
<meta property="og:type" content={resolvedOpenGraph.type || 'website'} />
|
|
154
|
+
<meta property="og:locale" content={resolvedOpenGraph.locale || 'en_US'} />
|
|
155
|
+
<meta property="og:url" content={resolvedOpenGraph.url || Astro.url.href} />
|
|
156
|
+
<meta property="og:title" content={resolvedOpenGraph.title || title || siteName} />
|
|
157
|
+
{resolvedOpenGraph.description && <meta property="og:description" content={resolvedOpenGraph.description} />}
|
|
158
|
+
{absoluteResolvedOgImage && <meta property="og:image" content={absoluteResolvedOgImage} />}
|
|
159
|
+
{ogImgAlt && <meta property="og:image:alt" content={ogImgAlt} />}
|
|
160
|
+
<meta property="og:site_name" content={resolvedOpenGraph.siteName || siteName} />
|
|
114
161
|
|
|
115
162
|
<!-- Twitter -->
|
|
116
|
-
<meta property="twitter:card" content=
|
|
163
|
+
<meta property="twitter:card" content={resolvedTwitter.card} />
|
|
117
164
|
<meta property="twitter:url" content={Astro.url.href} />
|
|
118
|
-
<meta property="twitter:title" content={title || siteName} />
|
|
119
|
-
{
|
|
120
|
-
{
|
|
121
|
-
{
|
|
122
|
-
{
|
|
123
|
-
{
|
|
165
|
+
<meta property="twitter:title" content={resolvedTwitter.title || title || siteName} />
|
|
166
|
+
{resolvedTwitter.description && <meta property="twitter:description" content={resolvedTwitter.description} />}
|
|
167
|
+
{absoluteResolvedOgImage && <meta property="twitter:image" content={absoluteResolvedOgImage} />}
|
|
168
|
+
{ogImgAlt && <meta property="twitter:image:alt" content={ogImgAlt} />}
|
|
169
|
+
{resolvedTwitter.site && <meta property="twitter:site" content={resolvedTwitter.site} />}
|
|
170
|
+
{resolvedTwitter.creator && <meta property="twitter:creator" content={resolvedTwitter.creator} />}
|
|
124
171
|
|
|
125
172
|
<!-- Icons & Manifest -->
|
|
126
173
|
<link rel="icon" type={faviconType} href={favicon} />
|
package/src/env.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
declare module "*.astro" {
|
|
2
|
-
export default {} as import("astro/runtime/server").AstroComponentFactory
|
|
3
|
-
}
|
|
1
|
+
declare module "*.astro" {
|
|
2
|
+
export default {} as import("astro/runtime/server").AstroComponentFactory
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
declare module "virtual:rimelight-seo-config" {
|
|
6
|
+
const config: import("./integration.js").RimelightSeoOptions
|
|
7
|
+
export default config
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module "virtual:rimelight-seo/options" {
|
|
11
|
+
const config: import("./integration.js").RimelightSeoOptions
|
|
12
|
+
export default config
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module "virtual:rimelight-seo/config" {
|
|
16
|
+
export const SEO_LOCALES: import("./types.js").LocaleConfig | undefined
|
|
17
|
+
export const PRIVATE_PATH_PREFIXES: string[] | undefined
|
|
18
|
+
export const seoEntries: (() => Promise<import("./types.js").SeoEntry[]>) | undefined
|
|
19
|
+
export const rssEntries: (() => Promise<import("./types.js").RssItem[]>) | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "virtual:rimelight-seo/site-config" {
|
|
23
|
+
export const siteConfig: import("./types.js").SiteConfig | Record<string, any>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module "virtual:rimelight-seo/db" {
|
|
27
|
+
export const db: any
|
|
28
|
+
export const pages: any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module "virtual:rimelight-seo/corpus" {
|
|
32
|
+
export const corpusPages: any[]
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,17 +2,26 @@ export type {
|
|
|
2
2
|
ChangeFreq,
|
|
3
3
|
LocaleConfig,
|
|
4
4
|
RobotsOptions,
|
|
5
|
+
RobotsRouteOptions,
|
|
5
6
|
SeoEntry,
|
|
6
7
|
SitemapAlternate,
|
|
7
8
|
SitemapUrl,
|
|
9
|
+
SitemapOptions,
|
|
8
10
|
UserAgentRule,
|
|
9
11
|
SiteConfig,
|
|
10
12
|
OGImage,
|
|
11
13
|
RSSFeed,
|
|
14
|
+
RssItem,
|
|
15
|
+
RssFeedOptions,
|
|
16
|
+
RssOptions,
|
|
12
17
|
HeadProps,
|
|
18
|
+
OpenGraphMeta,
|
|
19
|
+
TwitterMeta,
|
|
13
20
|
LlmsPage,
|
|
14
21
|
LlmsTxtOptions,
|
|
15
22
|
LlmsFullTxtOptions,
|
|
23
|
+
LlmsRouteOptions,
|
|
24
|
+
LlmsFullRouteOptions,
|
|
16
25
|
ArticleSchemaOptions,
|
|
17
26
|
BreadcrumbItem
|
|
18
27
|
} from "./types.js"
|
|
@@ -26,13 +35,22 @@ export {
|
|
|
26
35
|
|
|
27
36
|
export { buildRobotsTxt } from "./robots.js"
|
|
28
37
|
|
|
38
|
+
export { buildRssXml, escapeXml } from "./rss.js"
|
|
39
|
+
|
|
29
40
|
export {
|
|
30
41
|
buildCanonicalUrl,
|
|
31
42
|
buildPageTitle,
|
|
32
43
|
buildSafeDescription,
|
|
33
44
|
buildRobotsMetaContent,
|
|
34
|
-
buildWebSiteSchema
|
|
45
|
+
buildWebSiteSchema,
|
|
46
|
+
deepMerge
|
|
35
47
|
} from "./meta.js"
|
|
36
48
|
|
|
37
49
|
export { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
|
|
38
50
|
export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
|
|
51
|
+
export type { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.js"
|
|
52
|
+
|
|
53
|
+
import { rimelightSeo } from "./integration.js"
|
|
54
|
+
export type { RimelightSeoOptions } from "./integration.js"
|
|
55
|
+
export { rimelightSeo }
|
|
56
|
+
export default rimelightSeo
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import type { RimelightSeoOptions } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export { type RimelightSeoOptions } from "./types.js"
|
|
8
|
+
|
|
9
|
+
export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegration {
|
|
10
|
+
return {
|
|
11
|
+
name: "@rimelight/seo",
|
|
12
|
+
hooks: {
|
|
13
|
+
"astro:config:setup": ({ injectRoute, updateConfig, config }) => {
|
|
14
|
+
const rootDir = fileURLToPath(config.root)
|
|
15
|
+
|
|
16
|
+
// Resolve site files
|
|
17
|
+
const findFile = (candidates: string[]): string | null => {
|
|
18
|
+
for (const c of candidates) {
|
|
19
|
+
const resolved = path.resolve(rootDir, c)
|
|
20
|
+
if (fs.existsSync(resolved)) return resolved
|
|
21
|
+
}
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const seoConfigPath = findFile([
|
|
26
|
+
"src/config/seo.config.ts",
|
|
27
|
+
"src/config/seo.config.js",
|
|
28
|
+
"src/config/seo.config.mjs"
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const siteConfigPath = findFile([
|
|
32
|
+
"src/config/site.config.ts",
|
|
33
|
+
"src/config/site.config.js",
|
|
34
|
+
"src/config/site.config.mjs"
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const dbPath = findFile(["src/db/index.ts", "src/db/index.js", "src/db/index.mjs"])
|
|
38
|
+
|
|
39
|
+
const corpusPath = findFile([
|
|
40
|
+
"src/config/llms.corpus.ts",
|
|
41
|
+
"src/config/llms.corpus.js",
|
|
42
|
+
"src/config/llms.corpus.mjs"
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
// Inject routes
|
|
46
|
+
if (options.robots !== false) {
|
|
47
|
+
const pattern =
|
|
48
|
+
typeof options.robots === "object" && options.robots.path
|
|
49
|
+
? options.robots.path
|
|
50
|
+
: "/robots.txt"
|
|
51
|
+
injectRoute({
|
|
52
|
+
pattern,
|
|
53
|
+
entrypoint: "@rimelight/seo/routes/robots.txt.ts"
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.sitemap !== false) {
|
|
58
|
+
const pattern =
|
|
59
|
+
typeof options.sitemap === "object" && options.sitemap.path
|
|
60
|
+
? options.sitemap.path
|
|
61
|
+
: "/sitemap.xml"
|
|
62
|
+
injectRoute({
|
|
63
|
+
pattern,
|
|
64
|
+
entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.rss !== false) {
|
|
69
|
+
const pattern =
|
|
70
|
+
typeof options.rss === "object" && options.rss.path ? options.rss.path : "/rss.xml"
|
|
71
|
+
injectRoute({
|
|
72
|
+
pattern,
|
|
73
|
+
entrypoint: "@rimelight/seo/routes/rss.xml.ts"
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.llms !== false) {
|
|
78
|
+
const pattern =
|
|
79
|
+
typeof options.llms === "object" && options.llms.path ? options.llms.path : "/llms.txt"
|
|
80
|
+
injectRoute({
|
|
81
|
+
pattern,
|
|
82
|
+
entrypoint: "@rimelight/seo/routes/llms.txt.ts"
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.llmsFull !== false) {
|
|
87
|
+
const pattern =
|
|
88
|
+
typeof options.llmsFull === "object" && options.llmsFull.path
|
|
89
|
+
? options.llmsFull.path
|
|
90
|
+
: "/llms-full.txt"
|
|
91
|
+
injectRoute({
|
|
92
|
+
pattern,
|
|
93
|
+
entrypoint: "@rimelight/seo/routes/llms-full.txt.ts"
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
updateConfig({
|
|
98
|
+
vite: {
|
|
99
|
+
plugins: [
|
|
100
|
+
{
|
|
101
|
+
name: "vite-plugin-rimelight-seo",
|
|
102
|
+
resolveId(id) {
|
|
103
|
+
if (
|
|
104
|
+
id === "virtual:rimelight-seo-config" ||
|
|
105
|
+
id === "virtual:rimelight-seo/options"
|
|
106
|
+
) {
|
|
107
|
+
return "\0virtual:rimelight-seo/options"
|
|
108
|
+
}
|
|
109
|
+
if (id === "virtual:rimelight-seo/config") {
|
|
110
|
+
return "\0virtual:rimelight-seo/config"
|
|
111
|
+
}
|
|
112
|
+
if (id === "virtual:rimelight-seo/site-config") {
|
|
113
|
+
return "\0virtual:rimelight-seo/site-config"
|
|
114
|
+
}
|
|
115
|
+
if (id === "virtual:rimelight-seo/db") {
|
|
116
|
+
return "\0virtual:rimelight-seo/db"
|
|
117
|
+
}
|
|
118
|
+
if (id === "virtual:rimelight-seo/corpus") {
|
|
119
|
+
return "\0virtual:rimelight-seo/corpus"
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
},
|
|
123
|
+
load(id) {
|
|
124
|
+
if (id === "\0virtual:rimelight-seo/options") {
|
|
125
|
+
return `export default ${JSON.stringify(options)};`
|
|
126
|
+
}
|
|
127
|
+
if (id === "\0virtual:rimelight-seo/config") {
|
|
128
|
+
if (seoConfigPath) {
|
|
129
|
+
return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
|
|
130
|
+
export const SEO_LOCALES = customConfig.SEO_LOCALES;
|
|
131
|
+
export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
|
|
132
|
+
export const seoEntries = customConfig.seoEntries;
|
|
133
|
+
export const rssEntries = customConfig.rssEntries;
|
|
134
|
+
export default customConfig;`
|
|
135
|
+
}
|
|
136
|
+
return `export const SEO_LOCALES = undefined;
|
|
137
|
+
export const PRIVATE_PATH_PREFIXES = [];
|
|
138
|
+
export const seoEntries = async () => [];
|
|
139
|
+
export const rssEntries = undefined;`
|
|
140
|
+
}
|
|
141
|
+
if (id === "\0virtual:rimelight-seo/site-config") {
|
|
142
|
+
if (siteConfigPath) {
|
|
143
|
+
return `export * from ${JSON.stringify(siteConfigPath)};`
|
|
144
|
+
}
|
|
145
|
+
return `export const siteConfig = { name: "Site", description: "", url: "" };`
|
|
146
|
+
}
|
|
147
|
+
if (id === "\0virtual:rimelight-seo/db") {
|
|
148
|
+
if (dbPath) {
|
|
149
|
+
return `export * from ${JSON.stringify(dbPath)};`
|
|
150
|
+
}
|
|
151
|
+
return `export const db = null;
|
|
152
|
+
export const pages = null;`
|
|
153
|
+
}
|
|
154
|
+
if (id === "\0virtual:rimelight-seo/corpus") {
|
|
155
|
+
if (corpusPath) {
|
|
156
|
+
return `export * from ${JSON.stringify(corpusPath)};`
|
|
157
|
+
}
|
|
158
|
+
return `export const corpusPages = [];`
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default rimelightSeo
|
|
172
|
+
|
package/src/meta.test.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
buildPageTitle,
|
|
5
5
|
buildSafeDescription,
|
|
6
6
|
buildRobotsMetaContent,
|
|
7
|
-
buildWebSiteSchema
|
|
7
|
+
buildWebSiteSchema,
|
|
8
|
+
deepMerge
|
|
8
9
|
} from "./meta.js"
|
|
9
10
|
|
|
10
11
|
const BASE = "https://example.com"
|
|
@@ -49,6 +50,29 @@ describe("buildPageTitle", () => {
|
|
|
49
50
|
"404 - Not Found | My Site"
|
|
50
51
|
)
|
|
51
52
|
})
|
|
53
|
+
|
|
54
|
+
it("bypasses title template when absolute is true", () => {
|
|
55
|
+
expect(buildPageTitle({ ...opts, title: "Landing Page", absolute: true })).toBe("Landing Page")
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe("deepMerge", () => {
|
|
60
|
+
it("deep merges nested objects", () => {
|
|
61
|
+
const base = {
|
|
62
|
+
openGraph: { type: "website", siteName: "Base", locale: "en_US" },
|
|
63
|
+
twitter: { card: "summary", site: "@base" }
|
|
64
|
+
}
|
|
65
|
+
const override = {
|
|
66
|
+
openGraph: { siteName: "Override" },
|
|
67
|
+
twitter: { card: "summary_large_image" }
|
|
68
|
+
}
|
|
69
|
+
const result = deepMerge(base, override)
|
|
70
|
+
expect(result.openGraph.type).toBe("website")
|
|
71
|
+
expect(result.openGraph.siteName).toBe("Override")
|
|
72
|
+
expect(result.openGraph.locale).toBe("en_US")
|
|
73
|
+
expect(result.twitter.card).toBe("summary_large_image")
|
|
74
|
+
expect(result.twitter.site).toBe("@base")
|
|
75
|
+
})
|
|
52
76
|
})
|
|
53
77
|
|
|
54
78
|
describe("buildSafeDescription", () => {
|
package/src/meta.ts
CHANGED
|
@@ -12,19 +12,49 @@ export function buildCanonicalUrl(url: string | URL, base: string | URL): string
|
|
|
12
12
|
return path.includes("?") ? path.replace(/\/?$/, "") : path.replace(/\/?$/, "/")
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Deep merge two metadata or configuration objects.
|
|
17
|
+
*/
|
|
18
|
+
export function deepMerge<T extends Record<string, any>>(
|
|
19
|
+
base: T,
|
|
20
|
+
override?: Record<string, any>
|
|
21
|
+
): T {
|
|
22
|
+
if (!override) return { ...base }
|
|
23
|
+
const result: Record<string, any> = { ...base }
|
|
24
|
+
for (const key of Object.keys(override)) {
|
|
25
|
+
const baseVal = base[key]
|
|
26
|
+
const overrideVal = override[key]
|
|
27
|
+
if (
|
|
28
|
+
baseVal &&
|
|
29
|
+
overrideVal &&
|
|
30
|
+
typeof baseVal === "object" &&
|
|
31
|
+
typeof overrideVal === "object" &&
|
|
32
|
+
!Array.isArray(baseVal) &&
|
|
33
|
+
!Array.isArray(overrideVal)
|
|
34
|
+
) {
|
|
35
|
+
result[key] = deepMerge(baseVal, overrideVal)
|
|
36
|
+
} else if (overrideVal !== undefined) {
|
|
37
|
+
result[key] = overrideVal
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result as T
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
/**
|
|
16
44
|
* Build a page title using a site's title template. Falls back to siteName for untitled pages; uses
|
|
17
|
-
* a 404 label for error pages.
|
|
45
|
+
* a 404 label for error pages. Supports absolute title bypass.
|
|
18
46
|
*/
|
|
19
47
|
export function buildPageTitle(opts: {
|
|
20
48
|
title?: string
|
|
21
49
|
siteName: string
|
|
22
50
|
titleTemplate: string
|
|
23
51
|
is404?: boolean
|
|
52
|
+
absolute?: boolean
|
|
24
53
|
}): string {
|
|
25
|
-
const { title, siteName, titleTemplate, is404 } = opts
|
|
54
|
+
const { title, siteName, titleTemplate, is404, absolute } = opts
|
|
26
55
|
if (is404) return `404 - Not Found | ${siteName}`
|
|
27
56
|
if (!title) return siteName
|
|
57
|
+
if (absolute) return title
|
|
28
58
|
return titleTemplate.replace("%s", title)
|
|
29
59
|
}
|
|
30
60
|
|