@rimelight/seo 0.0.4 → 0.0.6
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 +8 -38
- package/dist/index.mjs +8 -175
- package/dist/integration.d.mts +6 -0
- package/dist/integration.mjs +101 -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/og.d.mts +117 -0
- package/dist/og.mjs +302 -0
- package/dist/robots.d.mts +3 -4
- package/dist/routes/llms-full.txt.d.mts +5 -0
- package/dist/routes/llms-full.txt.mjs +57 -0
- package/dist/routes/llms.txt.d.mts +5 -0
- package/dist/routes/llms.txt.mjs +68 -0
- package/dist/routes/robots.txt.d.mts +4 -0
- package/dist/routes/robots.txt.mjs +46 -0
- package/dist/routes/rss.xml.d.mts +4 -0
- package/dist/routes/rss.xml.mjs +51 -0
- package/dist/routes/sitemap.xml.d.mts +4 -0
- package/dist/routes/sitemap.xml.mjs +27 -0
- package/dist/rss.d.mts +8 -0
- package/dist/rss.mjs +44 -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 +354 -2
- package/package.json +24 -5
- package/src/components/SEOHead.astro +65 -18
- package/src/env.d.ts +33 -3
- package/src/index.ts +19 -1
- package/src/integration.ts +172 -0
- package/src/meta.test.ts +25 -1
- package/src/meta.ts +32 -2
- package/src/og.ts +444 -0
- package/src/routes/llms-full.txt.ts +77 -0
- package/src/routes/llms.txt.ts +90 -0
- package/src/routes/robots.txt.ts +63 -0
- package/src/routes/rss.xml.ts +87 -0
- package/src/routes/sitemap.xml.ts +43 -0
- package/src/rss.test.ts +28 -0
- package/src/rss.ts +63 -0
- package/src/types.ts +367 -242
- package/dist/types-e7gqYZwc.d.mts +0 -305
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { APIRoute } from "astro"
|
|
2
|
+
import { buildRssXml } from "../rss.js"
|
|
3
|
+
import * as seoConfig from "virtual:rimelight-seo/config"
|
|
4
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config"
|
|
5
|
+
import * as dbMod from "virtual:rimelight-seo/db"
|
|
6
|
+
import seoOptions from "virtual:rimelight-seo/options"
|
|
7
|
+
import type { RssItem } from "../types.js"
|
|
8
|
+
|
|
9
|
+
export const GET: APIRoute = async ({ site, url }) => {
|
|
10
|
+
const rssOpts = typeof seoOptions?.rss === "object" ? seoOptions.rss : {}
|
|
11
|
+
const siteConfig = siteConfigMod.siteConfig || {}
|
|
12
|
+
const siteUrl = site ? site.toString() : (rssOpts.site || siteConfig.url || url.origin)
|
|
13
|
+
|
|
14
|
+
let items: RssItem[] = []
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(rssOpts.items)) {
|
|
17
|
+
items = rssOpts.items
|
|
18
|
+
} else if (typeof (seoConfig as any).rssEntries === "function") {
|
|
19
|
+
items = await (seoConfig as any).rssEntries()
|
|
20
|
+
} else if (dbMod.db) {
|
|
21
|
+
try {
|
|
22
|
+
const { pages } = await import("#db/schema").catch(() => ({ pages: null }))
|
|
23
|
+
const { and, isNull, sql } = await import("drizzle-orm")
|
|
24
|
+
if (pages) {
|
|
25
|
+
const blogPages = await dbMod.db
|
|
26
|
+
.select()
|
|
27
|
+
.from(pages)
|
|
28
|
+
.where(and(sql`${pages.type} = 'blog'`, isNull(pages.deletedAt)))
|
|
29
|
+
.catch(() => [])
|
|
30
|
+
|
|
31
|
+
items = blogPages.map((p: any) => {
|
|
32
|
+
const title =
|
|
33
|
+
typeof p.title === "string"
|
|
34
|
+
? p.title
|
|
35
|
+
: p.title?.en || (p.title && Object.values(p.title)[0]) || p.slug
|
|
36
|
+
const description =
|
|
37
|
+
typeof p.description === "string"
|
|
38
|
+
? p.description
|
|
39
|
+
: p.description?.en || (p.description && Object.values(p.description)[0]) || ""
|
|
40
|
+
return {
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
link: `/blog/${p.slug}`,
|
|
44
|
+
pubDate: p.postedAt || p.createdAt || new Date()
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Fallback
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
try {
|
|
55
|
+
const { getCollection } = await import("astro:content")
|
|
56
|
+
const blog = await getCollection("blog")
|
|
57
|
+
if (Array.isArray(blog)) {
|
|
58
|
+
items = blog.map((post: any) => ({
|
|
59
|
+
title: post.data?.title || post.id,
|
|
60
|
+
pubDate: post.data?.pubDate || new Date(),
|
|
61
|
+
description: post.data?.description,
|
|
62
|
+
link: `/blog/${post.id}/`
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// No collections found
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const feedTitle = rssOpts.title || siteConfig.name || "RSS Feed"
|
|
71
|
+
const feedDesc = rssOpts.description || siteConfig.description || ""
|
|
72
|
+
|
|
73
|
+
const xml = buildRssXml({
|
|
74
|
+
title: feedTitle,
|
|
75
|
+
description: feedDesc,
|
|
76
|
+
site: siteUrl,
|
|
77
|
+
language: rssOpts.language || "en-US",
|
|
78
|
+
items
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return new Response(xml, {
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
84
|
+
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { APIRoute } from "astro"
|
|
2
|
+
import { buildSitemapUrls, buildSitemapXml } from "../sitemap.js"
|
|
3
|
+
import * as seoConfig from "virtual:rimelight-seo/config"
|
|
4
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config"
|
|
5
|
+
import seoOptions from "virtual:rimelight-seo/options"
|
|
6
|
+
|
|
7
|
+
export const GET: APIRoute = async ({ site, url }) => {
|
|
8
|
+
const sitemapOpts = typeof seoOptions?.sitemap === "object" ? seoOptions.sitemap : {}
|
|
9
|
+
const siteUrl = site ? site.toString() : (siteConfigMod.siteConfig?.url || url.origin)
|
|
10
|
+
|
|
11
|
+
let entries = []
|
|
12
|
+
if (typeof sitemapOpts.entries === "function") {
|
|
13
|
+
entries = await sitemapOpts.entries()
|
|
14
|
+
} else if (typeof seoConfig.seoEntries === "function") {
|
|
15
|
+
entries = await seoConfig.seoEntries()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const locales =
|
|
19
|
+
sitemapOpts.locales ||
|
|
20
|
+
seoConfig.SEO_LOCALES ||
|
|
21
|
+
(siteUrl
|
|
22
|
+
? {
|
|
23
|
+
site: siteUrl.replace(/\/$/, ""),
|
|
24
|
+
locales: { en: "en-US" },
|
|
25
|
+
defaultLocale: "en",
|
|
26
|
+
trailingSlash: "never" as const
|
|
27
|
+
}
|
|
28
|
+
: undefined)
|
|
29
|
+
|
|
30
|
+
let urls = sitemapOpts.urls || []
|
|
31
|
+
if (urls.length === 0 && entries.length > 0 && locales) {
|
|
32
|
+
urls = buildSitemapUrls(entries, locales)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const xml = buildSitemapXml(urls)
|
|
36
|
+
|
|
37
|
+
return new Response(xml, {
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
40
|
+
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
package/src/rss.test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { buildRssXml } from "./rss.js"
|
|
3
|
+
|
|
4
|
+
describe("buildRssXml", () => {
|
|
5
|
+
it("should generate valid RSS XML with channel and items", () => {
|
|
6
|
+
const xml = buildRssXml({
|
|
7
|
+
title: "My Site",
|
|
8
|
+
description: "My site description",
|
|
9
|
+
site: "https://example.com",
|
|
10
|
+
items: [
|
|
11
|
+
{
|
|
12
|
+
title: "Post 1 & More",
|
|
13
|
+
link: "/blog/post-1",
|
|
14
|
+
pubDate: new Date("2026-01-01T00:00:00Z"),
|
|
15
|
+
description: "Hello & welcome"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(xml).toContain("<rss version=\"2.0\"")
|
|
21
|
+
expect(xml).toContain("<title>My Site</title>")
|
|
22
|
+
expect(xml).toContain("<description>My site description</description>")
|
|
23
|
+
expect(xml).toContain("<link>https://example.com</link>")
|
|
24
|
+
expect(xml).toContain("<title>Post 1 & More</title>")
|
|
25
|
+
expect(xml).toContain("<link>https://example.com/blog/post-1</link>")
|
|
26
|
+
expect(xml).toContain("<description>Hello & welcome</description>")
|
|
27
|
+
})
|
|
28
|
+
})
|
package/src/rss.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { RssItem, RssFeedOptions } from "./types.js"
|
|
2
|
+
|
|
3
|
+
export function escapeXml(str: string): string {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/'/g, "'")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds standard RSS 2.0 feed XML document with Atom namespace link.
|
|
14
|
+
*/
|
|
15
|
+
export function buildRssXml(options: RssFeedOptions): string {
|
|
16
|
+
const siteUrl = (options.site || "").replace(/\/$/, "")
|
|
17
|
+
const feedUrl = options.feedUrl || `${siteUrl}/rss.xml`
|
|
18
|
+
const language = options.language || "en-US"
|
|
19
|
+
const lastBuildDate = new Date().toUTCString()
|
|
20
|
+
|
|
21
|
+
const itemXmls = (options.items || []).map((item) => {
|
|
22
|
+
const itemLink =
|
|
23
|
+
item.link.startsWith("http://") || item.link.startsWith("https://")
|
|
24
|
+
? item.link
|
|
25
|
+
: `${siteUrl}${item.link.startsWith("/") ? "" : "/"}${item.link}`
|
|
26
|
+
const pubDate = new Date(item.pubDate).toUTCString()
|
|
27
|
+
const guid = item.guid || itemLink
|
|
28
|
+
|
|
29
|
+
const lines: string[] = [
|
|
30
|
+
" <item>",
|
|
31
|
+
` <title>${escapeXml(item.title)}</title>`,
|
|
32
|
+
` <link>${escapeXml(itemLink)}</link>`,
|
|
33
|
+
` <guid isPermaLink="${guid.startsWith("http") ? "true" : "false"}">${escapeXml(guid)}</guid>`,
|
|
34
|
+
` <pubDate>${pubDate}</pubDate>`
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
if (item.description) {
|
|
38
|
+
lines.push(` <description>${escapeXml(item.description)}</description>`)
|
|
39
|
+
}
|
|
40
|
+
if (item.author) {
|
|
41
|
+
lines.push(` <author>${escapeXml(item.author)}</author>`)
|
|
42
|
+
}
|
|
43
|
+
if (item.content) {
|
|
44
|
+
lines.push(` <content:encoded><![CDATA[${item.content}]]></content:encoded>`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines.push(" </item>")
|
|
48
|
+
return lines.join("\n")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
52
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
53
|
+
<channel>
|
|
54
|
+
<title>${escapeXml(options.title || "")}</title>
|
|
55
|
+
<description>${escapeXml(options.description || "")}</description>
|
|
56
|
+
<link>${escapeXml(siteUrl)}</link>
|
|
57
|
+
<language>${escapeXml(language)}</language>
|
|
58
|
+
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
59
|
+
<atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml"/>
|
|
60
|
+
${itemXmls.join("\n")}
|
|
61
|
+
</channel>
|
|
62
|
+
</rss>`
|
|
63
|
+
}
|