@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.
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
 
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from "astro"
2
+ import { buildLlmsTxt } from "../llms.js"
3
+ import seoConfig from "virtual:rimelight-seo-config"
4
+
5
+ export const GET: APIRoute = ({ site }) => {
6
+ const llmsOpts = typeof seoConfig?.llms === "object" ? seoConfig.llms : {}
7
+ const siteStr = site ? site.toString().replace(/\/$/, "") : ""
8
+ const hostname = site ? new URL(site).hostname : "Site"
9
+ const content = buildLlmsTxt({
10
+ site: siteStr,
11
+ title: hostname,
12
+ pages: [],
13
+ ...llmsOpts
14
+ })
15
+
16
+ return new Response(content, {
17
+ headers: {
18
+ "Content-Type": "text/plain; charset=utf-8",
19
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
20
+ }
21
+ })
22
+ }
@@ -0,0 +1,20 @@
1
+ import type { APIRoute } from "astro"
2
+ import { buildRobotsTxt } from "../robots.js"
3
+ import seoConfig from "virtual:rimelight-seo-config"
4
+
5
+ export const GET: APIRoute = ({ site }) => {
6
+ const sitemapURL = site ? new URL("sitemap.xml", site).toString() : ""
7
+ const robotsOpts = typeof seoConfig?.robots === "object" ? seoConfig.robots : {}
8
+ const robots = buildRobotsTxt({
9
+ sitemapUrl: sitemapURL,
10
+ allow: ["/"],
11
+ ...robotsOpts
12
+ })
13
+
14
+ return new Response(robots, {
15
+ headers: {
16
+ "Content-Type": "text/plain; charset=utf-8",
17
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
18
+ }
19
+ })
20
+ }
@@ -0,0 +1,16 @@
1
+ import type { APIRoute } from "astro"
2
+ import { buildSitemapXml } from "../sitemap.js"
3
+ import seoConfig from "virtual:rimelight-seo-config"
4
+
5
+ export const GET: APIRoute = () => {
6
+ const sitemapOpts = typeof seoConfig?.sitemap === "object" ? seoConfig.sitemap : {}
7
+ const urls = sitemapOpts.urls || []
8
+ const xml = buildSitemapXml(urls)
9
+
10
+ return new Response(xml, {
11
+ headers: {
12
+ "Content-Type": "application/xml; charset=utf-8",
13
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
14
+ }
15
+ })
16
+ }
package/src/types.ts CHANGED
@@ -90,6 +90,27 @@ export interface UserAgentRule {
90
90
  allow?: string[]
91
91
  }
92
92
 
93
+ export interface OpenGraphMeta {
94
+ type?: string
95
+ locale?: string
96
+ url?: string
97
+ title?: string
98
+ description?: string
99
+ images?: OGImage[] | OGImage
100
+ siteName?: string
101
+ [key: string]: unknown
102
+ }
103
+
104
+ export interface TwitterMeta {
105
+ card?: "summary" | "summary_large_image" | "player" | "app"
106
+ site?: string
107
+ creator?: string
108
+ title?: string
109
+ description?: string
110
+ images?: OGImage[] | OGImage | string
111
+ [key: string]: unknown
112
+ }
113
+
93
114
  /**
94
115
  * Site identity and SEO defaults shared across all Rimelight Astro sites. The UI-specific overrides
95
116
  * (component themes, shortcuts, etc.) live in UIConfig (packages/ui) and are passed directly to the
@@ -121,6 +142,8 @@ export interface SiteConfig {
121
142
  maxDescriptionLength: number
122
143
  authorHandle?: string
123
144
  }
145
+ openGraph?: OpenGraphMeta
146
+ twitter?: TwitterMeta
124
147
  }
125
148
 
126
149
  /**
@@ -236,6 +259,22 @@ export interface HeadProps {
236
259
  * Version alternates for multi-version documentation
237
260
  */
238
261
  versionAlternates?: Array<{ version: string; href: string }>
262
+ /**
263
+ * Whether the title should bypass site title template
264
+ */
265
+ absolute?: boolean
266
+ /**
267
+ * Twitter card type
268
+ */
269
+ twitterCard?: "summary" | "summary_large_image" | "player" | "app"
270
+ /**
271
+ * Open Graph metadata overrides
272
+ */
273
+ openGraph?: OpenGraphMeta
274
+ /**
275
+ * Twitter metadata overrides
276
+ */
277
+ twitter?: TwitterMeta
239
278
  }
240
279
 
241
280
  export type { LlmsPage, LlmsTxtOptions, LlmsFullTxtOptions } from "./llms.js"
@@ -1,305 +0,0 @@
1
- //#region src/schema.d.ts
2
- interface ArticleSchemaOptions {
3
- type?: "TechArticle" | "Article" | "BlogPosting";
4
- title: string;
5
- description?: string;
6
- url: string;
7
- datePublished?: string | Date;
8
- dateModified?: string | Date;
9
- authorName?: string;
10
- authorUrl?: string;
11
- image?: string;
12
- publisherName?: string;
13
- publisherLogo?: string;
14
- }
15
- interface BreadcrumbItem {
16
- name: string;
17
- url: string;
18
- }
19
- /**
20
- * Builds Schema.org Article / TechArticle / BlogPosting JSON-LD.
21
- */
22
- declare function buildArticleSchema(options: ArticleSchemaOptions): Record<string, any>;
23
- /**
24
- * Builds Schema.org BreadcrumbList JSON-LD.
25
- */
26
- declare function buildBreadcrumbSchema(items: BreadcrumbItem[]): Record<string, any>;
27
- //#endregion
28
- //#region src/llms.d.ts
29
- interface LlmsPage {
30
- title: string;
31
- url: string;
32
- description?: string;
33
- markdownUrl?: string;
34
- section?: string;
35
- }
36
- interface LlmsTxtOptions {
37
- site: string;
38
- title: string;
39
- description?: string;
40
- fullCorpusUrl?: string;
41
- pages: LlmsPage[];
42
- sections?: Array<{
43
- label: string;
44
- url: string;
45
- }>;
46
- }
47
- interface LlmsFullTxtOptions {
48
- site: string;
49
- title: string;
50
- description?: string;
51
- pages: Array<{
52
- title: string;
53
- url: string;
54
- description?: string;
55
- markdown: string;
56
- version?: string;
57
- }>;
58
- }
59
- /**
60
- * Builds standard /llms.txt document format for AI discovery.
61
- */
62
- declare function buildLlmsTxt(options: LlmsTxtOptions): string;
63
- /**
64
- * Builds concatenated /llms-full.txt single corpus document for AI agents.
65
- */
66
- declare function buildLlmsFullTxt(options: LlmsFullTxtOptions): string;
67
- //#endregion
68
- //#region src/types.d.ts
69
- /**
70
- * Change frequency for sitemap entries
71
- */
72
- type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
73
- /**
74
- * SEO entry — a single page to include in sitemaps
75
- */
76
- interface SeoEntry {
77
- /**
78
- * Locale-less path, e.g. "/company/blog/hello". Leading slash required.
79
- */
80
- path: string;
81
- lastmod?: Date;
82
- changefreq?: ChangeFreq;
83
- priority?: number;
84
- /**
85
- * Restrict to a subset of locales; defaults to all configured locales.
86
- */
87
- locales?: readonly string[];
88
- }
89
- /**
90
- * Locale configuration for SEO builders
91
- */
92
- interface LocaleConfig {
93
- /**
94
- * Absolute origin, e.g. "https://rimelight.com"
95
- */
96
- site: string;
97
- /**
98
- * Map of locale segment -> hreflang, e.g. { en: "en-US", pt: "pt-BR" }
99
- */
100
- locales: Record<string, string>;
101
- defaultLocale: string;
102
- /**
103
- * Emit the default locale without a prefix. Default: false.
104
- */
105
- prefixDefaultLocale?: boolean;
106
- trailingSlash?: "always" | "never";
107
- }
108
- /**
109
- * Fully resolved sitemap URL with alternates
110
- */
111
- interface SitemapUrl {
112
- loc: string;
113
- lastmod?: string;
114
- changefreq?: ChangeFreq;
115
- priority?: number;
116
- alternates?: SitemapAlternate[];
117
- }
118
- /**
119
- * Language alternate for a sitemap URL
120
- */
121
- interface SitemapAlternate {
122
- hreflang: string;
123
- href: string;
124
- }
125
- /**
126
- * Options for robots.txt generation
127
- */
128
- interface RobotsOptions {
129
- /**
130
- * The sitemap URL to reference
131
- */
132
- sitemapUrl: string;
133
- /**
134
- * Paths to disallow for all user agents
135
- */
136
- disallow?: string[];
137
- /**
138
- * Paths to allow for all user agents
139
- */
140
- allow?: string[];
141
- /**
142
- * User-agent specific rules
143
- */
144
- userAgentRules?: UserAgentRule[];
145
- }
146
- /**
147
- * User-agent specific robots.txt rule
148
- */
149
- interface UserAgentRule {
150
- userAgent: string;
151
- disallow?: string[];
152
- allow?: string[];
153
- }
154
- /**
155
- * Site identity and SEO defaults shared across all Rimelight Astro sites. The UI-specific overrides
156
- * (component themes, shortcuts, etc.) live in UIConfig (packages/ui) and are passed directly to the
157
- * ui() integration.
158
- */
159
- interface SiteConfig {
160
- id: string;
161
- name: string;
162
- description: string;
163
- url: string;
164
- ogImage: string;
165
- author: string;
166
- email: string;
167
- branding: {
168
- logo: {
169
- alt: string;
170
- };
171
- favicon: {
172
- svg: string;
173
- };
174
- colors: {
175
- themeColor: string;
176
- backgroundColor: string;
177
- };
178
- };
179
- seo: {
180
- titleTemplate: string;
181
- ogImageFallback: string;
182
- maxDescriptionLength: number;
183
- authorHandle?: string;
184
- };
185
- }
186
- /**
187
- * Open Graph image with alt text
188
- */
189
- interface OGImage {
190
- /**
191
- * Image URL (relative or absolute)
192
- */
193
- src: string;
194
- /**
195
- * Alt text for the image
196
- */
197
- alt: string;
198
- }
199
- /**
200
- * RSS feed link configuration
201
- */
202
- interface RSSFeed {
203
- /**
204
- * RSS feed URL
205
- */
206
- href: string;
207
- /**
208
- * Feed title (defaults to "RSS")
209
- */
210
- title?: string;
211
- }
212
- /**
213
- * Props for the RLAHead Astro component. Defined here so consumers can construct head props without
214
- * importing @rimelight/ui.
215
- */
216
- interface HeadProps {
217
- /**
218
- * Page title
219
- */
220
- title?: string;
221
- /**
222
- * Page description
223
- */
224
- description?: string;
225
- /**
226
- * Open Graph image with alt text
227
- */
228
- ogImage?: OGImage;
229
- /**
230
- * Open Graph type (e.g., "website", "article")
231
- */
232
- ogType?: string;
233
- /**
234
- * Open Graph locale (defaults to "en_US")
235
- */
236
- ogLocale?: string;
237
- /**
238
- * Whether to prevent indexing
239
- */
240
- noindex?: boolean;
241
- /**
242
- * Whether this is a 404 page
243
- */
244
- is404?: boolean;
245
- /**
246
- * Robots metadata (overrides noindex/is404 logic if provided)
247
- */
248
- robots?: string;
249
- /**
250
- * Canonical URL
251
- */
252
- canonical?: string;
253
- /**
254
- * Language alternates for i18n
255
- */
256
- languageAlternates?: Array<{
257
- hreflang: string;
258
- href: string;
259
- }>;
260
- /**
261
- * Custom favicon SVG or image path (overrides config.branding.favicon.svg)
262
- */
263
- favicon?: string;
264
- /**
265
- * Site configuration for defaults
266
- */
267
- config?: SiteConfig;
268
- /**
269
- * Whether to enable View Transitions (ClientRouter)
270
- */
271
- transitions?: boolean;
272
- /**
273
- * Meta charset
274
- */
275
- charset?: string;
276
- /**
277
- * Meta viewport
278
- */
279
- viewport?: string;
280
- /**
281
- * RSS feed configuration
282
- */
283
- rssFeed?: RSSFeed;
284
- /**
285
- * Article or BlogPosting JSON-LD configuration
286
- */
287
- articleSchema?: ArticleSchemaOptions;
288
- /**
289
- * Breadcrumb list for JSON-LD structured data
290
- */
291
- breadcrumbs?: BreadcrumbItem[];
292
- /**
293
- * URL for this page's raw Markdown alternate twin
294
- */
295
- markdownUrl?: string;
296
- /**
297
- * Version alternates for multi-version documentation
298
- */
299
- versionAlternates?: Array<{
300
- version: string;
301
- href: string;
302
- }>;
303
- }
304
- //#endregion
305
- export { ArticleSchemaOptions as _, RSSFeed as a, buildBreadcrumbSchema as b, SiteConfig as c, UserAgentRule as d, LlmsFullTxtOptions as f, buildLlmsTxt as g, buildLlmsFullTxt as h, OGImage as i, SitemapAlternate as l, LlmsTxtOptions as m, HeadProps as n, RobotsOptions as o, LlmsPage as p, LocaleConfig as r, SeoEntry as s, ChangeFreq as t, SitemapUrl as u, BreadcrumbItem as v, buildArticleSchema as y };