@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/dist/index.d.mts +6 -38
- package/dist/index.mjs +5 -176
- package/dist/integration.d.mts +23 -0
- package/dist/integration.mjs +33 -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/robots.d.mts +3 -4
- package/dist/routes/llms.txt.d.mts +4 -0
- package/dist/routes/llms.txt.mjs +20 -0
- package/dist/routes/robots.txt.d.mts +4 -0
- package/dist/routes/robots.txt.mjs +18 -0
- package/dist/routes/sitemap.xml.d.mts +4 -0
- package/dist/routes/sitemap.xml.mjs +13 -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 +277 -2
- package/package.json +9 -4
- package/src/components/SEOHead.astro +65 -18
- package/src/env.d.ts +5 -0
- package/src/index.ts +5 -1
- package/src/integration.ts +74 -0
- package/src/meta.test.ts +25 -1
- package/src/meta.ts +32 -2
- package/src/routes/llms.txt.ts +22 -0
- package/src/routes/robots.txt.ts +20 -0
- package/src/routes/sitemap.xml.ts +16 -0
- package/src/types.ts +39 -0
- package/dist/types-e7gqYZwc.d.mts +0 -305
package/dist/schema.mjs
ADDED
|
@@ -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 };
|
package/dist/sitemap.d.mts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
2
|
-
|
|
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.
|
|
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.
|
|
48
|
-
"astro": "7.3.
|
|
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.
|
|
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={
|
|
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
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", () => {
|