@rimelight/seo 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/seo",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's SEO Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -16,14 +16,27 @@
16
16
  "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
17
17
  },
18
18
  "files": [
19
- "src"
19
+ "dist",
20
+ "src/components"
20
21
  ],
21
22
  "type": "module",
22
23
  "exports": {
23
- ".": "./src/index.ts",
24
- "./types": "./src/types.ts",
25
- "./sitemap": "./src/sitemap.ts",
26
- "./robots": "./src/robots.ts",
24
+ ".": {
25
+ "types": "./dist/index.d.mts",
26
+ "import": "./dist/index.mjs"
27
+ },
28
+ "./types": {
29
+ "types": "./dist/types.d.mts",
30
+ "import": "./dist/types.mjs"
31
+ },
32
+ "./sitemap": {
33
+ "types": "./dist/sitemap.d.mts",
34
+ "import": "./dist/sitemap.mjs"
35
+ },
36
+ "./robots": {
37
+ "types": "./dist/robots.d.mts",
38
+ "import": "./dist/robots.mjs"
39
+ },
27
40
  "./components/*": "./src/components/*",
28
41
  "./*": "./src/*"
29
42
  },
@@ -31,8 +44,8 @@
31
44
  "access": "public"
32
45
  },
33
46
  "devDependencies": {
34
- "@rimelight/config": "0.0.3",
35
- "astro": "7.2.7",
47
+ "@rimelight/config": "0.0.4",
48
+ "astro": "7.3.1",
36
49
  "typescript": "6.0.3"
37
50
  },
38
51
  "peerDependencies": {
@@ -42,6 +55,7 @@
42
55
  "node": ">=26.7.0"
43
56
  },
44
57
  "scripts": {
58
+ "build": "vp pack",
45
59
  "check": "vp check --fix && astro check"
46
60
  }
47
61
  }
package/src/env.d.ts DELETED
@@ -1,3 +0,0 @@
1
- declare module "*.astro" {
2
- export default {} as import("astro/runtime/server").AstroComponentFactory
3
- }
package/src/index.ts DELETED
@@ -1,41 +0,0 @@
1
- export type {
2
- ChangeFreq,
3
- LocaleConfig,
4
- RobotsOptions,
5
- SeoEntry,
6
- SitemapAlternate,
7
- SitemapUrl,
8
- UserAgentRule,
9
- SiteConfig,
10
- OGImage,
11
- RSSFeed,
12
- HeadProps,
13
- LlmsPage,
14
- LlmsTxtOptions,
15
- LlmsFullTxtOptions,
16
- ArticleSchemaOptions,
17
- BreadcrumbItem
18
- } from "./types.js"
19
-
20
- export {
21
- buildSitemapUrls,
22
- buildSitemapXml,
23
- buildSitemapIndexXml,
24
- chunkSitemapUrls
25
- } from "./sitemap.js"
26
-
27
- export { buildRobotsTxt } from "./robots.js"
28
-
29
- export {
30
- buildCanonicalUrl,
31
- buildPageTitle,
32
- buildSafeDescription,
33
- buildRobotsMetaContent,
34
- buildWebSiteSchema
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"
package/src/llms.test.ts DELETED
@@ -1,61 +0,0 @@
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 DELETED
@@ -1,124 +0,0 @@
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
- }
package/src/meta.test.ts DELETED
@@ -1,110 +0,0 @@
1
- import { describe, it, expect } from "vite-plus/test"
2
- import {
3
- buildCanonicalUrl,
4
- buildPageTitle,
5
- buildSafeDescription,
6
- buildRobotsMetaContent,
7
- buildWebSiteSchema
8
- } from "./meta.js"
9
-
10
- const BASE = "https://example.com"
11
-
12
- describe("buildCanonicalUrl", () => {
13
- it("adds trailing slash to clean paths", () => {
14
- expect(buildCanonicalUrl("/about", BASE)).toBe("https://example.com/about/")
15
- })
16
-
17
- it("strips trailing slash from query-param paths", () => {
18
- expect(buildCanonicalUrl("/search?q=hello", BASE)).toBe("https://example.com/search?q=hello")
19
- })
20
-
21
- it("handles root path", () => {
22
- expect(buildCanonicalUrl("/", BASE)).toBe("https://example.com/")
23
- })
24
-
25
- it("resolves relative paths against base", () => {
26
- expect(buildCanonicalUrl(new URL("https://example.com/docs"), BASE)).toBe(
27
- "https://example.com/docs/"
28
- )
29
- })
30
- })
31
-
32
- describe("buildPageTitle", () => {
33
- const opts = { siteName: "My Site", titleTemplate: "%s | My Site" }
34
-
35
- it("applies title template", () => {
36
- expect(buildPageTitle({ ...opts, title: "About" })).toBe("About | My Site")
37
- })
38
-
39
- it("returns siteName when no title", () => {
40
- expect(buildPageTitle(opts)).toBe("My Site")
41
- })
42
-
43
- it("returns 404 label for error pages", () => {
44
- expect(buildPageTitle({ ...opts, is404: true })).toBe("404 - Not Found | My Site")
45
- })
46
-
47
- it("404 overrides title if both provided", () => {
48
- expect(buildPageTitle({ ...opts, title: "Oops", is404: true })).toBe(
49
- "404 - Not Found | My Site"
50
- )
51
- })
52
- })
53
-
54
- describe("buildSafeDescription", () => {
55
- it("returns description unchanged when within limit", () => {
56
- expect(buildSafeDescription("Short desc", 160)).toBe("Short desc")
57
- })
58
-
59
- it("truncates and appends ellipsis when over limit", () => {
60
- const long = "a".repeat(200)
61
- const result = buildSafeDescription(long, 160)
62
- expect(result).toHaveLength(160)
63
- expect(result.endsWith("...")).toBe(true)
64
- })
65
-
66
- it("handles empty string", () => {
67
- expect(buildSafeDescription("", 160)).toBe("")
68
- })
69
-
70
- it("truncates at exact boundary", () => {
71
- const text = "a".repeat(160)
72
- expect(buildSafeDescription(text, 160)).toBe(text)
73
- expect(buildSafeDescription(text + "b", 160)).toHaveLength(160)
74
- })
75
- })
76
-
77
- describe("buildRobotsMetaContent", () => {
78
- it("returns indexable string by default", () => {
79
- expect(buildRobotsMetaContent({})).toBe("index, follow, max-image-preview:large")
80
- })
81
-
82
- it("returns noindex for noindex=true", () => {
83
- expect(buildRobotsMetaContent({ noindex: true })).toBe("noindex, nofollow")
84
- })
85
-
86
- it("returns noindex for is404=true", () => {
87
- expect(buildRobotsMetaContent({ is404: true })).toBe("noindex, nofollow")
88
- })
89
-
90
- it("respects override", () => {
91
- expect(buildRobotsMetaContent({ override: "noarchive" })).toBe("noarchive")
92
- })
93
- })
94
-
95
- describe("buildWebSiteSchema", () => {
96
- const config = { name: "My Site", url: "https://example.com", description: "A test site" }
97
-
98
- it("builds schema.org WebSite object", () => {
99
- const schema = buildWebSiteSchema(config)
100
- expect(schema["@context"]).toBe("https://schema.org")
101
- expect(schema["@type"]).toBe("WebSite")
102
- expect(schema.name).toBe("My Site")
103
- expect(schema.url).toBe("https://example.com")
104
- })
105
-
106
- it("prefers site param over config.url", () => {
107
- const schema = buildWebSiteSchema(config, "https://override.com")
108
- expect(schema.url).toBe("https://override.com")
109
- })
110
- })
package/src/meta.ts DELETED
@@ -1,70 +0,0 @@
1
- import type { SiteConfig } from "./types.js"
2
-
3
- /**
4
- * Build a canonical URL from a page URL and site base. - Strips trailing slash unless the path has
5
- * query params (where trailing slash is removed too). - Returns undefined-safe: pass Astro.url and
6
- * Astro.site directly.
7
- */
8
- export function buildCanonicalUrl(url: string | URL, base: string | URL): string {
9
- const resolved = new URL(url, base)
10
- const path = resolved.toString()
11
- // Keep query params as-is but strip trailing slash; for clean paths also ensure trailing slash
12
- return path.includes("?") ? path.replace(/\/?$/, "") : path.replace(/\/?$/, "/")
13
- }
14
-
15
- /**
16
- * 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.
18
- */
19
- export function buildPageTitle(opts: {
20
- title?: string
21
- siteName: string
22
- titleTemplate: string
23
- is404?: boolean
24
- }): string {
25
- const { title, siteName, titleTemplate, is404 } = opts
26
- if (is404) return `404 - Not Found | ${siteName}`
27
- if (!title) return siteName
28
- return titleTemplate.replace("%s", title)
29
- }
30
-
31
- /**
32
- * Truncate a description to a maximum character length, appending "…" if trimmed.
33
- */
34
- export function buildSafeDescription(description: string, maxLength: number): string {
35
- if (!description) return description
36
- if (description.length <= maxLength) return description
37
- return description.slice(0, maxLength - 3) + "..."
38
- }
39
-
40
- /**
41
- * Build the value for the <meta name="robots"> tag. Returns the full directive string including
42
- * max-image-preview for indexable pages.
43
- */
44
- export function buildRobotsMetaContent(opts: {
45
- noindex?: boolean
46
- is404?: boolean
47
- override?: string
48
- }): string {
49
- if (opts.override) return opts.override
50
- return !opts.noindex && !opts.is404
51
- ? "index, follow, max-image-preview:large"
52
- : "noindex, nofollow"
53
- }
54
-
55
- /**
56
- * \n * Build a Schema.org WebSite JSON-LD object.\n * Pass the result to JSON.stringify and emit
57
- * via <script type="application/ld+json">.\n
58
- */
59
- export function buildWebSiteSchema(
60
- config: Pick<SiteConfig, "name" | "url" | "description">,
61
- site?: string
62
- ): Record<string, unknown> {
63
- return {
64
- "@context": "https://schema.org",
65
- "@type": "WebSite",
66
- "name": config.name,
67
- "url": site ?? config.url,
68
- "description": config.description
69
- }
70
- }
@@ -1,75 +0,0 @@
1
- import { describe, it, expect } from "vite-plus/test"
2
- import { buildRobotsTxt } from "./robots.js"
3
- import type { RobotsOptions } from "./types.js"
4
-
5
- describe("buildRobotsTxt", () => {
6
- it("generates basic robots.txt with sitemap", () => {
7
- const options: RobotsOptions = {
8
- sitemapUrl: "https://example.com/sitemap.xml"
9
- }
10
-
11
- const robots = buildRobotsTxt(options)
12
-
13
- expect(robots).toContain("User-agent: *")
14
- expect(robots).toContain("Sitemap: https://example.com/sitemap.xml")
15
- })
16
-
17
- it("includes disallow rules", () => {
18
- const options: RobotsOptions = {
19
- sitemapUrl: "https://example.com/sitemap.xml",
20
- disallow: ["/admin/", "/dashboard/"]
21
- }
22
-
23
- const robots = buildRobotsTxt(options)
24
-
25
- expect(robots).toContain("Disallow: /admin/")
26
- expect(robots).toContain("Disallow: /dashboard/")
27
- })
28
-
29
- it("includes allow rules", () => {
30
- const options: RobotsOptions = {
31
- sitemapUrl: "https://example.com/sitemap.xml",
32
- allow: ["/api/public/"]
33
- }
34
-
35
- const robots = buildRobotsTxt(options)
36
-
37
- expect(robots).toContain("Allow: /api/public/")
38
- })
39
-
40
- it("includes user-agent specific rules", () => {
41
- const options: RobotsOptions = {
42
- sitemapUrl: "https://example.com/sitemap.xml",
43
- userAgentRules: [
44
- {
45
- userAgent: "GPTBot",
46
- disallow: ["/admin/"]
47
- }
48
- ]
49
- }
50
-
51
- const robots = buildRobotsTxt(options)
52
-
53
- expect(robots).toContain("User-agent: GPTBot")
54
- expect(robots).toContain("Disallow: /admin/")
55
- })
56
-
57
- it("orders user-agent specific rules before wildcard", () => {
58
- const options: RobotsOptions = {
59
- sitemapUrl: "https://example.com/sitemap.xml",
60
- userAgentRules: [
61
- {
62
- userAgent: "GPTBot",
63
- disallow: ["/admin/"]
64
- }
65
- ]
66
- }
67
-
68
- const robots = buildRobotsTxt(options)
69
-
70
- const gptBotIndex = robots.indexOf("User-agent: GPTBot")
71
- const wildcardIndex = robots.indexOf("User-agent: *")
72
-
73
- expect(gptBotIndex).toBeLessThan(wildcardIndex)
74
- })
75
- })
package/src/robots.ts DELETED
@@ -1,59 +0,0 @@
1
- import type { RobotsOptions, UserAgentRule } from "./types.js"
2
-
3
- /**
4
- * Build robots.txt content from options
5
- */
6
- export function buildRobotsTxt(options: RobotsOptions): string {
7
- const lines: string[] = []
8
-
9
- // Add user-agent specific rules first
10
- if (options.userAgentRules && options.userAgentRules.length > 0) {
11
- for (const rule of options.userAgentRules) {
12
- lines.push(buildUserAgentSection(rule))
13
- lines.push("")
14
- }
15
- }
16
-
17
- // Add wildcard user-agent (all bots)
18
- lines.push("User-agent: *")
19
-
20
- if (options.disallow && options.disallow.length > 0) {
21
- for (const path of options.disallow) {
22
- lines.push(`Disallow: ${path}`)
23
- }
24
- }
25
-
26
- if (options.allow && options.allow.length > 0) {
27
- for (const path of options.allow) {
28
- lines.push(`Allow: ${path}`)
29
- }
30
- }
31
-
32
- lines.push("")
33
- lines.push(`Sitemap: ${options.sitemapUrl}`)
34
-
35
- return lines.join("\n")
36
- }
37
-
38
- /**
39
- * Build a user-agent specific section
40
- */
41
- function buildUserAgentSection(rule: UserAgentRule): string {
42
- const lines: string[] = []
43
-
44
- lines.push(`User-agent: ${rule.userAgent}`)
45
-
46
- if (rule.disallow && rule.disallow.length > 0) {
47
- for (const path of rule.disallow) {
48
- lines.push(`Disallow: ${path}`)
49
- }
50
- }
51
-
52
- if (rule.allow && rule.allow.length > 0) {
53
- for (const path of rule.allow) {
54
- lines.push(`Allow: ${path}`)
55
- }
56
- }
57
-
58
- return lines.join("\n")
59
- }
@@ -1,45 +0,0 @@
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
- })