@rimelight/seo 0.0.4 → 0.0.5

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.
@@ -0,0 +1,52 @@
1
+ //#region src/schema.ts
2
+ /**
3
+ * Builds Schema.org Article / TechArticle / BlogPosting JSON-LD.
4
+ */
5
+ function buildArticleSchema(options) {
6
+ const { type = "TechArticle", title, description, url, datePublished, dateModified, authorName, authorUrl, image, publisherName = "Rimelight Entertainment", publisherLogo } = options;
7
+ const schema = {
8
+ "@context": "https://schema.org",
9
+ "@type": type,
10
+ "headline": title,
11
+ "mainEntityOfPage": {
12
+ "@type": "WebPage",
13
+ "@id": url
14
+ }
15
+ };
16
+ if (description) schema["description"] = description;
17
+ if (image) schema["image"] = image;
18
+ if (datePublished) schema["datePublished"] = datePublished instanceof Date ? datePublished.toISOString() : datePublished;
19
+ if (dateModified) schema["dateModified"] = dateModified instanceof Date ? dateModified.toISOString() : dateModified;
20
+ else if (datePublished) schema["dateModified"] = schema["datePublished"];
21
+ if (authorName) schema["author"] = {
22
+ "@type": "Person",
23
+ "name": authorName,
24
+ ...authorUrl ? { url: authorUrl } : {}
25
+ };
26
+ if (publisherName) schema["publisher"] = {
27
+ "@type": "Organization",
28
+ "name": publisherName,
29
+ ...publisherLogo ? { logo: {
30
+ "@type": "ImageObject",
31
+ "url": publisherLogo
32
+ } } : {}
33
+ };
34
+ return schema;
35
+ }
36
+ /**
37
+ * Builds Schema.org BreadcrumbList JSON-LD.
38
+ */
39
+ function buildBreadcrumbSchema(items) {
40
+ return {
41
+ "@context": "https://schema.org",
42
+ "@type": "BreadcrumbList",
43
+ "itemListElement": items.map((item, index) => ({
44
+ "@type": "ListItem",
45
+ "position": index + 1,
46
+ "name": item.name,
47
+ "item": item.url
48
+ }))
49
+ };
50
+ }
51
+ //#endregion
52
+ export { buildArticleSchema, buildBreadcrumbSchema };
@@ -1,20 +1,19 @@
1
- import { r as LocaleConfig, s as SeoEntry, u as SitemapUrl } from "./types-e7gqYZwc.mjs";
1
+ import { LocaleConfig, SeoEntry, SitemapUrl } from "./types.mjs";
2
2
  //#region src/sitemap.d.ts
3
3
  /**
4
4
  * Build fully-resolved sitemap URLs from entries and locale config
5
5
  */
6
- declare function buildSitemapUrls(entries: SeoEntry[], config: LocaleConfig): SitemapUrl[];
6
+ export declare function buildSitemapUrls(entries: SeoEntry[], config: LocaleConfig): SitemapUrl[];
7
7
  /**
8
8
  * Build sitemap XML from resolved URLs
9
9
  */
10
- declare function buildSitemapXml(urls: SitemapUrl[]): string;
10
+ export declare function buildSitemapXml(urls: SitemapUrl[]): string;
11
11
  /**
12
12
  * Build sitemap index XML from sitemap URLs
13
13
  */
14
- declare function buildSitemapIndexXml(sitemapUrls: string[]): string;
14
+ export declare function buildSitemapIndexXml(sitemapUrls: string[]): string;
15
15
  /**
16
16
  * Chunk sitemap URLs into multiple sitemaps (max 50k URLs per sitemap)
17
17
  */
18
- declare function chunkSitemapUrls(urls: SitemapUrl[], limit?: number): SitemapUrl[][];
19
- //#endregion
20
- export { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls };
18
+ export declare function chunkSitemapUrls(urls: SitemapUrl[], limit?: number): SitemapUrl[][];
19
+ //#endregion
package/dist/types.d.mts CHANGED
@@ -1,2 +1,277 @@
1
- import { _ as ArticleSchemaOptions, a as RSSFeed, c as SiteConfig, d as UserAgentRule, f as LlmsFullTxtOptions, i as OGImage, l as SitemapAlternate, m as LlmsTxtOptions, n as HeadProps, o as RobotsOptions, p as LlmsPage, r as LocaleConfig, s as SeoEntry, t as ChangeFreq, u as SitemapUrl, v as BreadcrumbItem } from "./types-e7gqYZwc.mjs";
2
- export { type ArticleSchemaOptions, type BreadcrumbItem, ChangeFreq, HeadProps, type LlmsFullTxtOptions, type LlmsPage, type LlmsTxtOptions, LocaleConfig, OGImage, RSSFeed, RobotsOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapUrl, UserAgentRule };
1
+ import { n as BreadcrumbItem, t as ArticleSchemaOptions } from "./schema-C8v69blQ.mjs";
2
+ import { n as LlmsPage, r as LlmsTxtOptions, t as LlmsFullTxtOptions } from "./llms-BVLxzjr-.mjs";
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Change frequency for sitemap entries
6
+ */
7
+ export type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
8
+ /**
9
+ * SEO entry — a single page to include in sitemaps
10
+ */
11
+ export interface SeoEntry {
12
+ /**
13
+ * Locale-less path, e.g. "/company/blog/hello". Leading slash required.
14
+ */
15
+ path: string;
16
+ lastmod?: Date;
17
+ changefreq?: ChangeFreq;
18
+ priority?: number;
19
+ /**
20
+ * Restrict to a subset of locales; defaults to all configured locales.
21
+ */
22
+ locales?: readonly string[];
23
+ }
24
+ /**
25
+ * Locale configuration for SEO builders
26
+ */
27
+ export interface LocaleConfig {
28
+ /**
29
+ * Absolute origin, e.g. "https://rimelight.com"
30
+ */
31
+ site: string;
32
+ /**
33
+ * Map of locale segment -> hreflang, e.g. { en: "en-US", pt: "pt-BR" }
34
+ */
35
+ locales: Record<string, string>;
36
+ defaultLocale: string;
37
+ /**
38
+ * Emit the default locale without a prefix. Default: false.
39
+ */
40
+ prefixDefaultLocale?: boolean;
41
+ trailingSlash?: "always" | "never";
42
+ }
43
+ /**
44
+ * Fully resolved sitemap URL with alternates
45
+ */
46
+ export interface SitemapUrl {
47
+ loc: string;
48
+ lastmod?: string;
49
+ changefreq?: ChangeFreq;
50
+ priority?: number;
51
+ alternates?: SitemapAlternate[];
52
+ }
53
+ /**
54
+ * Language alternate for a sitemap URL
55
+ */
56
+ export interface SitemapAlternate {
57
+ hreflang: string;
58
+ href: string;
59
+ }
60
+ /**
61
+ * Options for robots.txt generation
62
+ */
63
+ export interface RobotsOptions {
64
+ /**
65
+ * The sitemap URL to reference
66
+ */
67
+ sitemapUrl: string;
68
+ /**
69
+ * Paths to disallow for all user agents
70
+ */
71
+ disallow?: string[];
72
+ /**
73
+ * Paths to allow for all user agents
74
+ */
75
+ allow?: string[];
76
+ /**
77
+ * User-agent specific rules
78
+ */
79
+ userAgentRules?: UserAgentRule[];
80
+ }
81
+ /**
82
+ * User-agent specific robots.txt rule
83
+ */
84
+ export interface UserAgentRule {
85
+ userAgent: string;
86
+ disallow?: string[];
87
+ allow?: string[];
88
+ }
89
+ export interface OpenGraphMeta {
90
+ type?: string;
91
+ locale?: string;
92
+ url?: string;
93
+ title?: string;
94
+ description?: string;
95
+ images?: OGImage[] | OGImage;
96
+ siteName?: string;
97
+ [key: string]: unknown;
98
+ }
99
+ export interface TwitterMeta {
100
+ card?: "summary" | "summary_large_image" | "player" | "app";
101
+ site?: string;
102
+ creator?: string;
103
+ title?: string;
104
+ description?: string;
105
+ images?: OGImage[] | OGImage | string;
106
+ [key: string]: unknown;
107
+ }
108
+ /**
109
+ * Site identity and SEO defaults shared across all Rimelight Astro sites. The UI-specific overrides
110
+ * (component themes, shortcuts, etc.) live in UIConfig (packages/ui) and are passed directly to the
111
+ * ui() integration.
112
+ */
113
+ export interface SiteConfig {
114
+ id: string;
115
+ name: string;
116
+ description: string;
117
+ url: string;
118
+ ogImage: string;
119
+ author: string;
120
+ email: string;
121
+ branding: {
122
+ logo: {
123
+ alt: string;
124
+ };
125
+ favicon: {
126
+ svg: string;
127
+ };
128
+ colors: {
129
+ themeColor: string;
130
+ backgroundColor: string;
131
+ };
132
+ };
133
+ seo: {
134
+ titleTemplate: string;
135
+ ogImageFallback: string;
136
+ maxDescriptionLength: number;
137
+ authorHandle?: string;
138
+ };
139
+ openGraph?: OpenGraphMeta;
140
+ twitter?: TwitterMeta;
141
+ }
142
+ /**
143
+ * Open Graph image with alt text
144
+ */
145
+ export interface OGImage {
146
+ /**
147
+ * Image URL (relative or absolute)
148
+ */
149
+ src: string;
150
+ /**
151
+ * Alt text for the image
152
+ */
153
+ alt: string;
154
+ }
155
+ /**
156
+ * RSS feed link configuration
157
+ */
158
+ export interface RSSFeed {
159
+ /**
160
+ * RSS feed URL
161
+ */
162
+ href: string;
163
+ /**
164
+ * Feed title (defaults to "RSS")
165
+ */
166
+ title?: string;
167
+ }
168
+ /**
169
+ * Props for the RLAHead Astro component. Defined here so consumers can construct head props without
170
+ * importing @rimelight/ui.
171
+ */
172
+ export interface HeadProps {
173
+ /**
174
+ * Page title
175
+ */
176
+ title?: string;
177
+ /**
178
+ * Page description
179
+ */
180
+ description?: string;
181
+ /**
182
+ * Open Graph image with alt text
183
+ */
184
+ ogImage?: OGImage;
185
+ /**
186
+ * Open Graph type (e.g., "website", "article")
187
+ */
188
+ ogType?: string;
189
+ /**
190
+ * Open Graph locale (defaults to "en_US")
191
+ */
192
+ ogLocale?: string;
193
+ /**
194
+ * Whether to prevent indexing
195
+ */
196
+ noindex?: boolean;
197
+ /**
198
+ * Whether this is a 404 page
199
+ */
200
+ is404?: boolean;
201
+ /**
202
+ * Robots metadata (overrides noindex/is404 logic if provided)
203
+ */
204
+ robots?: string;
205
+ /**
206
+ * Canonical URL
207
+ */
208
+ canonical?: string;
209
+ /**
210
+ * Language alternates for i18n
211
+ */
212
+ languageAlternates?: Array<{
213
+ hreflang: string;
214
+ href: string;
215
+ }>;
216
+ /**
217
+ * Custom favicon SVG or image path (overrides config.branding.favicon.svg)
218
+ */
219
+ favicon?: string;
220
+ /**
221
+ * Site configuration for defaults
222
+ */
223
+ config?: SiteConfig;
224
+ /**
225
+ * Whether to enable View Transitions (ClientRouter)
226
+ */
227
+ transitions?: boolean;
228
+ /**
229
+ * Meta charset
230
+ */
231
+ charset?: string;
232
+ /**
233
+ * Meta viewport
234
+ */
235
+ viewport?: string;
236
+ /**
237
+ * RSS feed configuration
238
+ */
239
+ rssFeed?: RSSFeed;
240
+ /**
241
+ * Article or BlogPosting JSON-LD configuration
242
+ */
243
+ articleSchema?: ArticleSchemaOptions;
244
+ /**
245
+ * Breadcrumb list for JSON-LD structured data
246
+ */
247
+ breadcrumbs?: BreadcrumbItem[];
248
+ /**
249
+ * URL for this page's raw Markdown alternate twin
250
+ */
251
+ markdownUrl?: string;
252
+ /**
253
+ * Version alternates for multi-version documentation
254
+ */
255
+ versionAlternates?: Array<{
256
+ version: string;
257
+ href: string;
258
+ }>;
259
+ /**
260
+ * Whether the title should bypass site title template
261
+ */
262
+ absolute?: boolean;
263
+ /**
264
+ * Twitter card type
265
+ */
266
+ twitterCard?: "summary" | "summary_large_image" | "player" | "app";
267
+ /**
268
+ * Open Graph metadata overrides
269
+ */
270
+ openGraph?: OpenGraphMeta;
271
+ /**
272
+ * Twitter metadata overrides
273
+ */
274
+ twitter?: TwitterMeta;
275
+ }
276
+ //#endregion
277
+ export type { ArticleSchemaOptions, BreadcrumbItem, LlmsFullTxtOptions, LlmsPage, LlmsTxtOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/seo",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's SEO Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -37,22 +37,27 @@
37
37
  "types": "./dist/robots.d.mts",
38
38
  "import": "./dist/robots.mjs"
39
39
  },
40
+ "./integration": {
41
+ "types": "./dist/integration.d.mts",
42
+ "import": "./dist/integration.mjs"
43
+ },
40
44
  "./components/*": "./src/components/*",
45
+ "./routes/*": "./src/routes/*",
41
46
  "./*": "./src/*"
42
47
  },
43
48
  "publishConfig": {
44
49
  "access": "public"
45
50
  },
46
51
  "devDependencies": {
47
- "@rimelight/config": "0.0.5",
48
- "astro": "7.3.1",
52
+ "@rimelight/config": "0.0.6",
53
+ "astro": "7.3.2",
49
54
  "typescript": "6.0.3"
50
55
  },
51
56
  "peerDependencies": {
52
57
  "astro": ">=7.0.0"
53
58
  },
54
59
  "engines": {
55
- "node": ">=26.7.0"
60
+ "node": ">=26.8.2"
56
61
  },
57
62
  "scripts": {
58
63
  "build": "vp pack",
@@ -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={ogType} />
107
- <meta property="og:locale" content={ogLocale} />
108
- <meta property="og:url" content={Astro.url.href} />
109
- <meta property="og:title" content={title || siteName} />
110
- {safeDescription && <meta property="og:description" content={safeDescription} />}
111
- {absoluteOgImage && <meta property="og:image" content={absoluteOgImage} />}
112
- {finalOgImage && <meta property="og:image:alt" content={finalOgImage.alt} />}
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="summary_large_image" />
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
- {safeDescription && <meta property="twitter:description" content={safeDescription} />}
120
- {absoluteOgImage && <meta property="twitter:image" content={absoluteOgImage} />}
121
- {finalOgImage && <meta property="twitter:image:alt" content={finalOgImage.alt} />}
122
- {twitterHandle && <meta property="twitter:site" content={twitterHandle} />}
123
- {twitterHandle && <meta property="twitter:creator" content={twitterHandle} />}
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,8 @@
1
1
  declare module "*.astro" {
2
2
  export default {} as import("astro/runtime/server").AstroComponentFactory
3
3
  }
4
+
5
+ declare module "virtual:rimelight-seo-config" {
6
+ const config: import("./integration.js").RimelightSeoOptions
7
+ export default config
8
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export type {
10
10
  OGImage,
11
11
  RSSFeed,
12
12
  HeadProps,
13
+ OpenGraphMeta,
14
+ TwitterMeta,
13
15
  LlmsPage,
14
16
  LlmsTxtOptions,
15
17
  LlmsFullTxtOptions,
@@ -31,8 +33,10 @@ export {
31
33
  buildPageTitle,
32
34
  buildSafeDescription,
33
35
  buildRobotsMetaContent,
34
- buildWebSiteSchema
36
+ buildWebSiteSchema,
37
+ deepMerge
35
38
  } from "./meta.js"
36
39
 
37
40
  export { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
38
41
  export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
42
+ export { rimelightSeo, type RimelightSeoOptions } from "./integration.js"
@@ -0,0 +1,74 @@
1
+ import type { AstroIntegration } from "astro"
2
+ import type { RobotsOptions, SitemapUrl, LlmsTxtOptions } from "./types.js"
3
+
4
+ export interface RimelightSeoOptions {
5
+ /**
6
+ * Sitemap configuration or false to disable automatic /sitemap.xml
7
+ */
8
+ sitemap?:
9
+ | boolean
10
+ | {
11
+ urls?: SitemapUrl[]
12
+ [key: string]: any
13
+ }
14
+ /**
15
+ * Robots.txt configuration or false to disable automatic /robots.txt
16
+ */
17
+ robots?: boolean | RobotsOptions
18
+ /**
19
+ * LLMs.txt configuration or false to disable automatic /llms.txt
20
+ */
21
+ llms?: boolean | LlmsTxtOptions
22
+ }
23
+
24
+ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegration {
25
+ return {
26
+ name: "@rimelight/seo",
27
+ hooks: {
28
+ "astro:config:setup": ({ injectRoute, updateConfig }) => {
29
+ if (options.robots !== false) {
30
+ injectRoute({
31
+ pattern: "/robots.txt",
32
+ entrypoint: "@rimelight/seo/routes/robots.txt.ts"
33
+ })
34
+ }
35
+
36
+ if (options.sitemap !== false) {
37
+ injectRoute({
38
+ pattern: "/sitemap.xml",
39
+ entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
40
+ })
41
+ }
42
+
43
+ if (options.llms !== false) {
44
+ injectRoute({
45
+ pattern: "/llms.txt",
46
+ entrypoint: "@rimelight/seo/routes/llms.txt.ts"
47
+ })
48
+ }
49
+
50
+ updateConfig({
51
+ vite: {
52
+ plugins: [
53
+ {
54
+ name: "vite-plugin-rimelight-seo",
55
+ resolveId(id) {
56
+ if (id === "virtual:rimelight-seo-config") {
57
+ return "\0virtual:rimelight-seo-config"
58
+ }
59
+ return null
60
+ },
61
+ load(id) {
62
+ if (id === "\0virtual:rimelight-seo-config") {
63
+ return `export default ${JSON.stringify(options)};`
64
+ }
65
+ return null
66
+ }
67
+ }
68
+ ]
69
+ }
70
+ })
71
+ }
72
+ }
73
+ }
74
+ }
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", () => {