@rimelight/seo 0.0.1 → 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rimelight Entertainment
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/seo",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's SEO Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -30,12 +30,9 @@
30
30
  "publishConfig": {
31
31
  "access": "public"
32
32
  },
33
- "scripts": {
34
- "check": "pnpm audit --audit-level=moderate && vp check --fix && astro check"
35
- },
36
33
  "devDependencies": {
37
- "@rimelight/config": "workspace:*",
38
- "astro": "7.2.2",
34
+ "@rimelight/config": "0.0.3",
35
+ "astro": "7.2.7",
39
36
  "typescript": "6.0.3"
40
37
  },
41
38
  "peerDependencies": {
@@ -44,5 +41,7 @@
44
41
  "engines": {
45
42
  "node": ">=26.7.0"
46
43
  },
47
- "packageManager": "pnpm@11.22.0"
48
- }
44
+ "scripts": {
45
+ "check": "vp check --fix && astro check"
46
+ }
47
+ }
@@ -0,0 +1,25 @@
1
+ ---
2
+ interface Props {
3
+ /** Absolute or site-relative URL for this page's markdown version. */
4
+ markdownUrl?: string | undefined
5
+ /** Absolute or site-relative URL for the top-level llms.txt index. */
6
+ llmsUrl?: string | undefined
7
+ class?: string | undefined
8
+ }
9
+
10
+ const {
11
+ markdownUrl,
12
+ llmsUrl = "/llms.txt",
13
+ class: className = "sr-only"
14
+ } = Astro.props
15
+ ---
16
+
17
+ <aside class={className} data-ai-agent-directive>
18
+ This page is available in Markdown format. Markdown is recommended for AI and agentic consumption.
19
+ {markdownUrl && (
20
+ <>
21
+ See <a href={markdownUrl}>{markdownUrl}</a> for this page, or{" "}
22
+ </>
23
+ )}
24
+ <a href={llmsUrl}>{llmsUrl}</a> for the full documentation and content index.
25
+ </aside>
@@ -7,6 +7,7 @@ import {
7
7
  buildRobotsMetaContent,
8
8
  buildWebSiteSchema
9
9
  } from "../meta.js"
10
+ import { buildArticleSchema, buildBreadcrumbSchema } from "../schema.js"
10
11
 
11
12
  const {
12
13
  title,
@@ -23,6 +24,10 @@ const {
23
24
  charset = 'utf-8',
24
25
  viewport = 'width=device-width, initial-scale=1',
25
26
  rssFeed,
27
+ articleSchema,
28
+ breadcrumbs,
29
+ markdownUrl,
30
+ versionAlternates = [],
26
31
  } = Astro.props as HeadProps
27
32
 
28
33
  // Resolve defaults from config if available
@@ -85,6 +90,12 @@ const robotsContent = buildRobotsMetaContent({
85
90
  <link rel="alternate" hreflang={alt.hreflang} href={alt.href} />
86
91
  ))}
87
92
 
93
+ <!-- Agent & Version Alternates -->
94
+ {markdownUrl && <link rel="alternate" type="text/markdown" href={markdownUrl} title="Markdown Format" />}
95
+ {versionAlternates.map((alt) => (
96
+ <link rel="alternate" data-version={alt.version} href={alt.href} />
97
+ ))}
98
+
88
99
  <!-- Open Graph / Facebook -->
89
100
  <meta property="og:type" content={ogType} />
90
101
  <meta property="og:locale" content={ogLocale} />
@@ -122,3 +133,16 @@ const robotsContent = buildRobotsMetaContent({
122
133
  buildWebSiteSchema(config, Astro.site?.toString())
123
134
  )} />
124
135
  )}
136
+
137
+ {articleSchema && (
138
+ <script type="application/ld+json" is:inline set:html={JSON.stringify(
139
+ buildArticleSchema(articleSchema)
140
+ )} />
141
+ )}
142
+
143
+ {breadcrumbs && breadcrumbs.length > 0 && (
144
+ <script type="application/ld+json" is:inline set:html={JSON.stringify(
145
+ buildBreadcrumbSchema(breadcrumbs)
146
+ )} />
147
+ )}
148
+
package/src/index.ts CHANGED
@@ -9,7 +9,12 @@ export type {
9
9
  SiteConfig,
10
10
  OGImage,
11
11
  RSSFeed,
12
- HeadProps
12
+ HeadProps,
13
+ LlmsPage,
14
+ LlmsTxtOptions,
15
+ LlmsFullTxtOptions,
16
+ ArticleSchemaOptions,
17
+ BreadcrumbItem
13
18
  } from "./types.js"
14
19
 
15
20
  export {
@@ -28,3 +33,9 @@ export {
28
33
  buildRobotsMetaContent,
29
34
  buildWebSiteSchema
30
35
  } from "./meta.js"
36
+
37
+ export { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
38
+ export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
39
+
40
+ export { default as SEOHead } from "./components/SEOHead.astro"
41
+ export { default as AgentDirective } from "./components/AgentDirective.astro"
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
3
+
4
+ describe("seo-llms", () => {
5
+ it("builds standard /llms.txt with sections and pages", () => {
6
+ const output = buildLlmsTxt({
7
+ site: "https://rimelight.com",
8
+ title: "Rimelight Documentation",
9
+ description: "Docs for developers and AI agents.",
10
+ pages: [
11
+ {
12
+ title: "Introduction",
13
+ url: "https://rimelight.com/en/docs/overview/introduction",
14
+ markdownUrl: "https://rimelight.com/en/docs/overview/introduction.md",
15
+ description: "Getting started guide",
16
+ section: "Overview"
17
+ },
18
+ {
19
+ title: "Architecture",
20
+ url: "https://rimelight.com/en/docs/overview/architecture",
21
+ markdownUrl: "https://rimelight.com/en/docs/overview/architecture.md",
22
+ section: "Overview"
23
+ }
24
+ ]
25
+ })
26
+
27
+ expect(output).toContain("# Rimelight Documentation")
28
+ expect(output).toContain(
29
+ "Full corpus (all pages, one document): https://rimelight.com/llms-full.txt"
30
+ )
31
+ expect(output).toContain("### Overview")
32
+ expect(output).toContain(
33
+ "- [Introduction](https://rimelight.com/en/docs/overview/introduction.md) — Getting started guide"
34
+ )
35
+ expect(output).toContain(
36
+ "- [Architecture](https://rimelight.com/en/docs/overview/architecture.md)"
37
+ )
38
+ })
39
+
40
+ it("builds full corpus /llms-full.txt", () => {
41
+ const output = buildLlmsFullTxt({
42
+ site: "https://rimelight.com",
43
+ title: "Rimelight Docs",
44
+ pages: [
45
+ {
46
+ title: "Quickstart",
47
+ url: "https://rimelight.com/en/docs/quickstart",
48
+ description: "Step-by-step tutorial",
49
+ version: "v1",
50
+ markdown: "Run `vp dev` to start the development server."
51
+ }
52
+ ]
53
+ })
54
+
55
+ expect(output).toContain("# Rimelight Docs — Full Documentation Corpus")
56
+ expect(output).toContain("## Quickstart")
57
+ expect(output).toContain("URL: https://rimelight.com/en/docs/quickstart")
58
+ expect(output).toContain("Version: v1")
59
+ expect(output).toContain("Run `vp dev` to start the development server.")
60
+ })
61
+ })
package/src/llms.ts ADDED
@@ -0,0 +1,124 @@
1
+ export interface LlmsPage {
2
+ title: string
3
+ url: string
4
+ description?: string
5
+ markdownUrl?: string
6
+ section?: string
7
+ }
8
+
9
+ export interface LlmsTxtOptions {
10
+ site: string
11
+ title: string
12
+ description?: string
13
+ fullCorpusUrl?: string
14
+ pages: LlmsPage[]
15
+ sections?: Array<{ label: string; url: string }>
16
+ }
17
+
18
+ export interface LlmsFullTxtOptions {
19
+ site: string
20
+ title: string
21
+ description?: string
22
+ pages: Array<{
23
+ title: string
24
+ url: string
25
+ description?: string
26
+ markdown: string
27
+ version?: string
28
+ }>
29
+ }
30
+
31
+ /**
32
+ * Builds standard /llms.txt document format for AI discovery.
33
+ */
34
+ export function buildLlmsTxt(options: LlmsTxtOptions): string {
35
+ const {
36
+ site,
37
+ title,
38
+ description,
39
+ fullCorpusUrl = `${site.replace(/\/$/, "")}/llms-full.txt`,
40
+ pages,
41
+ sections = []
42
+ } = options
43
+
44
+ const lines = [
45
+ `# ${title}`,
46
+ "",
47
+ description || "Documentation index for AI agents.",
48
+ "",
49
+ `Full corpus (all pages, one document): ${fullCorpusUrl}`,
50
+ ""
51
+ ]
52
+
53
+ if (sections.length > 0) {
54
+ lines.push("## Sections", "")
55
+ for (const sec of sections) {
56
+ lines.push(`- [${sec.label}](${sec.url})`)
57
+ }
58
+ lines.push("")
59
+ }
60
+
61
+ lines.push("## Pages", "")
62
+
63
+ // Group pages by section if present, or flat
64
+ const grouped = new Map<string, LlmsPage[]>()
65
+ for (const page of pages) {
66
+ const sec = page.section || "General"
67
+ if (!grouped.has(sec)) grouped.set(sec, [])
68
+ grouped.get(sec)!.push(page)
69
+ }
70
+
71
+ if (grouped.size === 1 && grouped.has("General")) {
72
+ for (const page of pages) {
73
+ const pageUrl = page.markdownUrl || page.url
74
+ const desc = page.description ? ` — ${page.description}` : ""
75
+ lines.push(`- [${page.title}](${pageUrl})${desc}`)
76
+ }
77
+ } else {
78
+ for (const [secName, secPages] of grouped.entries()) {
79
+ lines.push(`### ${secName}`, "")
80
+ for (const page of secPages) {
81
+ const pageUrl = page.markdownUrl || page.url
82
+ const desc = page.description ? ` — ${page.description}` : ""
83
+ lines.push(`- [${page.title}](${pageUrl})${desc}`)
84
+ }
85
+ lines.push("")
86
+ }
87
+ }
88
+
89
+ lines.push("")
90
+ return lines.join("\n")
91
+ }
92
+
93
+ /**
94
+ * Builds concatenated /llms-full.txt single corpus document for AI agents.
95
+ */
96
+ export function buildLlmsFullTxt(options: LlmsFullTxtOptions): string {
97
+ const { title, description, pages } = options
98
+ const parts = [
99
+ `# ${title} — Full Documentation Corpus`,
100
+ "",
101
+ description || "Complete documentation corpus for AI agents.",
102
+ "",
103
+ "---",
104
+ ""
105
+ ]
106
+
107
+ for (const page of pages) {
108
+ const metaHeader = [
109
+ `## ${page.title}`,
110
+ "",
111
+ `URL: ${page.url}`,
112
+ ...(page.description ? [`Description: ${page.description}`] : []),
113
+ ...(page.version ? [`Version: ${page.version}`] : []),
114
+ "",
115
+ page.markdown.trim(),
116
+ "",
117
+ "---",
118
+ ""
119
+ ]
120
+ parts.push(...metaHeader)
121
+ }
122
+
123
+ return parts.join("\n")
124
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
3
+
4
+ describe("seo-schema", () => {
5
+ it("builds TechArticle JSON-LD schema", () => {
6
+ const schema = buildArticleSchema({
7
+ type: "TechArticle",
8
+ title: "Building on Cloudflare Workers",
9
+ description: "A complete guide to edge rendering",
10
+ url: "https://rimelight.com/blog/cloudflare-workers",
11
+ datePublished: new Date("2026-08-01T12:00:00Z"),
12
+ authorName: "Idan",
13
+ image: "https://rimelight.com/og/article.png"
14
+ })
15
+
16
+ expect(schema["@context"]).toBe("https://schema.org")
17
+ expect(schema["@type"]).toBe("TechArticle")
18
+ expect(schema.headline).toBe("Building on Cloudflare Workers")
19
+ expect(schema.author).toEqual({ "@type": "Person", "name": "Idan" })
20
+ expect(schema.datePublished).toBe("2026-08-01T12:00:00.000Z")
21
+ })
22
+
23
+ it("builds BreadcrumbList JSON-LD schema", () => {
24
+ const schema = buildBreadcrumbSchema([
25
+ { name: "Home", url: "https://rimelight.com" },
26
+ { name: "Docs", url: "https://rimelight.com/en/docs" },
27
+ { name: "Installation", url: "https://rimelight.com/en/docs/installation" }
28
+ ])
29
+
30
+ expect(schema["@type"]).toBe("BreadcrumbList")
31
+ expect(schema.itemListElement).toHaveLength(3)
32
+ expect(schema.itemListElement[0]).toEqual({
33
+ "@type": "ListItem",
34
+ "position": 1,
35
+ "name": "Home",
36
+ "item": "https://rimelight.com"
37
+ })
38
+ expect(schema.itemListElement[2]).toEqual({
39
+ "@type": "ListItem",
40
+ "position": 3,
41
+ "name": "Installation",
42
+ "item": "https://rimelight.com/en/docs/installation"
43
+ })
44
+ })
45
+ })
package/src/schema.ts ADDED
@@ -0,0 +1,95 @@
1
+ export interface ArticleSchemaOptions {
2
+ type?: "TechArticle" | "Article" | "BlogPosting"
3
+ title: string
4
+ description?: string
5
+ url: string
6
+ datePublished?: string | Date
7
+ dateModified?: string | Date
8
+ authorName?: string
9
+ authorUrl?: string
10
+ image?: string
11
+ publisherName?: string
12
+ publisherLogo?: string
13
+ }
14
+
15
+ export interface BreadcrumbItem {
16
+ name: string
17
+ url: string
18
+ }
19
+
20
+ /**
21
+ * Builds Schema.org Article / TechArticle / BlogPosting JSON-LD.
22
+ */
23
+ export function buildArticleSchema(options: ArticleSchemaOptions): Record<string, any> {
24
+ const {
25
+ type = "TechArticle",
26
+ title,
27
+ description,
28
+ url,
29
+ datePublished,
30
+ dateModified,
31
+ authorName,
32
+ authorUrl,
33
+ image,
34
+ publisherName = "Rimelight Entertainment",
35
+ publisherLogo
36
+ } = options
37
+
38
+ const schema: Record<string, any> = {
39
+ "@context": "https://schema.org",
40
+ "@type": type,
41
+ "headline": title,
42
+ "mainEntityOfPage": {
43
+ "@type": "WebPage",
44
+ "@id": url
45
+ }
46
+ }
47
+
48
+ if (description) schema.description = description
49
+ if (image) schema.image = image
50
+
51
+ if (datePublished) {
52
+ schema.datePublished =
53
+ datePublished instanceof Date ? datePublished.toISOString() : datePublished
54
+ }
55
+
56
+ if (dateModified) {
57
+ schema.dateModified = dateModified instanceof Date ? dateModified.toISOString() : dateModified
58
+ } else if (datePublished) {
59
+ schema.dateModified = schema.datePublished
60
+ }
61
+
62
+ if (authorName) {
63
+ schema.author = {
64
+ "@type": "Person",
65
+ "name": authorName,
66
+ ...(authorUrl ? { url: authorUrl } : {})
67
+ }
68
+ }
69
+
70
+ if (publisherName) {
71
+ schema.publisher = {
72
+ "@type": "Organization",
73
+ "name": publisherName,
74
+ ...(publisherLogo ? { logo: { "@type": "ImageObject", "url": publisherLogo } } : {})
75
+ }
76
+ }
77
+
78
+ return schema
79
+ }
80
+
81
+ /**
82
+ * Builds Schema.org BreadcrumbList JSON-LD.
83
+ */
84
+ export function buildBreadcrumbSchema(items: BreadcrumbItem[]): Record<string, any> {
85
+ return {
86
+ "@context": "https://schema.org",
87
+ "@type": "BreadcrumbList",
88
+ "itemListElement": items.map((item, index) => ({
89
+ "@type": "ListItem",
90
+ "position": index + 1,
91
+ "name": item.name,
92
+ "item": item.url
93
+ }))
94
+ }
95
+ }
package/src/types.ts CHANGED
@@ -216,4 +216,23 @@ export interface HeadProps {
216
216
  * RSS feed configuration
217
217
  */
218
218
  rssFeed?: RSSFeed
219
+ /**
220
+ * Article or BlogPosting JSON-LD configuration
221
+ */
222
+ articleSchema?: import("./schema.js").ArticleSchemaOptions
223
+ /**
224
+ * Breadcrumb list for JSON-LD structured data
225
+ */
226
+ breadcrumbs?: import("./schema.js").BreadcrumbItem[]
227
+ /**
228
+ * URL for this page's raw Markdown alternate twin
229
+ */
230
+ markdownUrl?: string
231
+ /**
232
+ * Version alternates for multi-version documentation
233
+ */
234
+ versionAlternates?: Array<{ version: string; href: string }>
219
235
  }
236
+
237
+ export type { LlmsPage, LlmsTxtOptions, LlmsFullTxtOptions } from "./llms.js"
238
+ export type { ArticleSchemaOptions, BreadcrumbItem } from "./schema.js"