@rimelight/seo 0.0.5 → 0.0.7

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.
Files changed (74) hide show
  1. package/dist/assets.d.mts +39 -0
  2. package/dist/assets.mjs +96 -0
  3. package/dist/catalog.d.mts +7 -0
  4. package/dist/catalog.mjs +10 -0
  5. package/dist/index.d.mts +9 -3
  6. package/dist/index.mjs +10 -2
  7. package/dist/integration.d.mts +3 -20
  8. package/dist/integration.mjs +257 -7
  9. package/dist/manifest.d.mts +7 -0
  10. package/dist/manifest.mjs +44 -0
  11. package/dist/og.d.mts +117 -0
  12. package/dist/og.mjs +302 -0
  13. package/dist/routes/api-catalog.d.mts +5 -0
  14. package/dist/routes/api-catalog.mjs +38 -0
  15. package/dist/routes/apple-touch-icon.png.d.mts +5 -0
  16. package/dist/routes/apple-touch-icon.png.mjs +17 -0
  17. package/dist/routes/favicon.ico.d.mts +5 -0
  18. package/dist/routes/favicon.ico.mjs +14 -0
  19. package/dist/routes/favicon.svg.d.mts +5 -0
  20. package/dist/routes/favicon.svg.mjs +12 -0
  21. package/dist/routes/icon-192.png.d.mts +5 -0
  22. package/dist/routes/icon-192.png.mjs +17 -0
  23. package/dist/routes/icon-512.png.d.mts +5 -0
  24. package/dist/routes/icon-512.png.mjs +17 -0
  25. package/dist/routes/icon-maskable-512.png.d.mts +5 -0
  26. package/dist/routes/icon-maskable-512.png.mjs +18 -0
  27. package/dist/routes/llms-full.txt.d.mts +5 -0
  28. package/dist/routes/llms-full.txt.mjs +57 -0
  29. package/dist/routes/llms.txt.d.mts +1 -0
  30. package/dist/routes/llms.txt.mjs +60 -12
  31. package/dist/routes/robots.txt.mjs +32 -4
  32. package/dist/routes/rss.xml.d.mts +4 -0
  33. package/dist/routes/rss.xml.mjs +79 -0
  34. package/dist/routes/security.txt.d.mts +5 -0
  35. package/dist/routes/security.txt.mjs +24 -0
  36. package/dist/routes/site.webmanifest.d.mts +5 -0
  37. package/dist/routes/site.webmanifest.mjs +23 -0
  38. package/dist/routes/sitemap.xml.mjs +18 -4
  39. package/dist/rss.d.mts +8 -0
  40. package/dist/rss.mjs +44 -0
  41. package/dist/security.d.mts +7 -0
  42. package/dist/security.mjs +34 -0
  43. package/dist/types.d.mts +263 -3
  44. package/package.json +38 -3
  45. package/src/assets.test.ts +70 -0
  46. package/src/assets.ts +137 -0
  47. package/src/catalog.test.ts +26 -0
  48. package/src/catalog.ts +11 -0
  49. package/src/components/SEOHead.astro +1 -1
  50. package/src/env.d.ts +51 -3
  51. package/src/index.ts +33 -2
  52. package/src/integration.ts +307 -28
  53. package/src/manifest.test.ts +41 -0
  54. package/src/manifest.ts +59 -0
  55. package/src/og.ts +436 -0
  56. package/src/routes/api-catalog.ts +55 -0
  57. package/src/routes/apple-touch-icon.png.ts +19 -0
  58. package/src/routes/favicon.ico.ts +19 -0
  59. package/src/routes/favicon.svg.ts +17 -0
  60. package/src/routes/icon-192.png.ts +19 -0
  61. package/src/routes/icon-512.png.ts +19 -0
  62. package/src/routes/icon-maskable-512.png.ts +24 -0
  63. package/src/routes/llms-full.txt.ts +77 -0
  64. package/src/routes/llms.txt.ts +84 -13
  65. package/src/routes/robots.txt.ts +47 -4
  66. package/src/routes/rss.xml.ts +129 -0
  67. package/src/routes/security.txt.ts +30 -0
  68. package/src/routes/site.webmanifest.ts +28 -0
  69. package/src/routes/sitemap.xml.ts +32 -5
  70. package/src/rss.test.ts +28 -0
  71. package/src/rss.ts +63 -0
  72. package/src/security.test.ts +46 -0
  73. package/src/security.ts +78 -0
  74. package/src/types.ts +286 -3
@@ -1,20 +1,68 @@
1
1
  import { buildLlmsTxt } from "../llms.mjs";
2
- import seoConfig from "virtual:rimelight-seo-config";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ import * as dbMod from "virtual:rimelight-seo/db";
5
+ import * as corpusMod from "virtual:rimelight-seo/corpus";
3
6
  //#region src/routes/llms.txt.ts
4
- const GET = ({ site }) => {
5
- const llmsOpts = typeof seoConfig?.llms === "object" ? seoConfig.llms : {};
6
- const siteStr = site ? site.toString().replace(/\/$/, "") : "";
7
- const hostname = site ? new URL(site).hostname : "Site";
8
- const content = buildLlmsTxt({
9
- site: siteStr,
10
- title: hostname,
11
- pages: [],
7
+ const prerender = false;
8
+ const GET = async ({ site, url }) => {
9
+ const siteConfig = siteConfigMod.siteConfig || {};
10
+ const siteUrl = site ? site.toString() : siteConfig.url || `${url.protocol}//${url.host}`;
11
+ const llmsOpts = typeof seoOptions?.llms === "object" ? seoOptions.llms : {};
12
+ let pages = [];
13
+ if (Array.isArray(llmsOpts.pages) && llmsOpts.pages.length > 0) pages = llmsOpts.pages;
14
+ else if (Array.isArray(corpusMod.corpusPages) && corpusMod.corpusPages.length > 0) pages = corpusMod.corpusPages.map((p) => {
15
+ const slug = p.slug === "index" ? "" : p.slug;
16
+ const pageUrl = `${siteUrl.replace(/\/$/, "")}/${slug}`.replace(/\/+$/, "");
17
+ return {
18
+ title: p.title,
19
+ description: p.description,
20
+ url: pageUrl,
21
+ section: p.section ? p.section.toUpperCase() : "GENERAL"
22
+ };
23
+ });
24
+ else if (dbMod.db) try {
25
+ const { pages: pagesTable } = await import("#db/schema").catch(() => ({ pages: null }));
26
+ const { and, isNotNull, isNull } = await import("drizzle-orm");
27
+ if (pagesTable) {
28
+ const docRows = await dbMod.db.select().from(pagesTable).where(and(isNull(pagesTable.deletedAt), isNotNull(pagesTable.publishedVersionId))).catch(() => []);
29
+ const resolveLocalized = (val) => {
30
+ if (typeof val === "string") return val;
31
+ if (typeof val === "object" && val !== null) {
32
+ const rec = val;
33
+ return rec["en"] || Object.values(rec)[0] || "";
34
+ }
35
+ return typeof val === "number" ? String(val) : "";
36
+ };
37
+ pages = docRows.map((p) => {
38
+ const title = resolveLocalized(p.title) || p.slug;
39
+ const description = resolveLocalized(p.description);
40
+ const slug = p.slug === "index" ? "" : p.slug;
41
+ const pathPrefix = p.type === "doc" ? "docs" : p.type || "docs";
42
+ const pageUrl = `${siteUrl.replace(/\/$/, "")}/en/${pathPrefix}/${slug}`.replace(/\/+$/, "");
43
+ return {
44
+ title,
45
+ description,
46
+ url: pageUrl,
47
+ markdownUrl: `${pageUrl}.md`,
48
+ section: p.type ? p.type.toUpperCase() : "GENERAL"
49
+ };
50
+ });
51
+ }
52
+ } catch {}
53
+ const title = llmsOpts.title || siteConfig.name || "Documentation";
54
+ const description = llmsOpts.description || siteConfig.description || "Documentation index for AI agents.";
55
+ const body = buildLlmsTxt({
56
+ site: siteUrl,
57
+ title,
58
+ description,
59
+ pages,
12
60
  ...llmsOpts
13
61
  });
14
- return new Response(content, { headers: {
62
+ return new Response(body, { headers: {
15
63
  "Content-Type": "text/plain; charset=utf-8",
16
- "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
64
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
17
65
  } });
18
66
  };
19
67
  //#endregion
20
- export { GET };
68
+ export { GET, prerender };
@@ -1,12 +1,40 @@
1
1
  import { buildRobotsTxt } from "../robots.mjs";
2
- import seoConfig from "virtual:rimelight-seo-config";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ import * as seoConfig from "virtual:rimelight-seo/config";
3
5
  //#region src/routes/robots.txt.ts
4
- const GET = ({ site }) => {
5
- const sitemapURL = site ? new URL("sitemap.xml", site).toString() : "";
6
- const robotsOpts = typeof seoConfig?.robots === "object" ? seoConfig.robots : {};
6
+ const GET = ({ site, url }) => {
7
+ const siteUrl = site ? site.toString() : siteConfigMod.siteConfig?.url || url.origin;
8
+ const sitemapURL = new URL("sitemap.xml", siteUrl).toString();
9
+ const defaultPrivatePrefixes = [
10
+ "/dashboard",
11
+ "/admin",
12
+ "/cms",
13
+ "/internal",
14
+ "/api",
15
+ "/dev",
16
+ "/og",
17
+ "/open-graph",
18
+ "/auth"
19
+ ];
20
+ const customPrivatePrefixes = Array.isArray(seoConfig.PRIVATE_PATH_PREFIXES) ? seoConfig.PRIVATE_PATH_PREFIXES : [];
21
+ const privatePrefixes = Array.from(/* @__PURE__ */ new Set([...defaultPrivatePrefixes, ...customPrivatePrefixes]));
22
+ const disallowPatterns = [...privatePrefixes, ...privatePrefixes.map((p) => `/*${p.startsWith("/") ? p : `/${p}`}/`)];
23
+ const robotsOpts = typeof seoOptions?.robots === "object" ? seoOptions.robots : {};
24
+ const defaultAgentRules = [{
25
+ userAgent: "GPTBot",
26
+ disallow: privatePrefixes.map((p) => `${p.startsWith("/") ? p : `/${p}`}/`),
27
+ allow: ["/"]
28
+ }, {
29
+ userAgent: "ClaudeBot",
30
+ disallow: privatePrefixes.map((p) => `${p.startsWith("/") ? p : `/${p}`}/`),
31
+ allow: ["/"]
32
+ }];
7
33
  const robots = buildRobotsTxt({
8
34
  sitemapUrl: sitemapURL,
35
+ disallow: disallowPatterns,
9
36
  allow: ["/"],
37
+ userAgentRules: defaultAgentRules,
10
38
  ...robotsOpts
11
39
  });
12
40
  return new Response(robots, { headers: {
@@ -0,0 +1,4 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/rss.xml.d.ts
3
+ export declare const GET: APIRoute;
4
+ //#endregion
@@ -0,0 +1,79 @@
1
+ import { buildRssXml } from "../rss.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ import * as dbMod from "virtual:rimelight-seo/db";
5
+ import * as seoConfig from "virtual:rimelight-seo/config";
6
+ //#region src/routes/rss.xml.ts
7
+ const GET = async ({ site, url }) => {
8
+ const rssOpts = typeof seoOptions?.rss === "object" ? seoOptions.rss : {};
9
+ const siteConfig = siteConfigMod.siteConfig || {};
10
+ const siteUrl = site ? site.toString() : rssOpts.site || siteConfig.url || url.origin;
11
+ let items = [];
12
+ if (Array.isArray(rssOpts.items)) items = rssOpts.items;
13
+ else if (typeof rssOpts.items === "function") items = await rssOpts.items();
14
+ else if (typeof seoConfig.rssEntries === "function") items = await seoConfig.rssEntries();
15
+ else {
16
+ const normalizedCollections = (rssOpts.collections && rssOpts.collections.length > 0 ? rssOpts.collections : [
17
+ "blog",
18
+ "posts",
19
+ "news",
20
+ "articles"
21
+ ]).map((c) => {
22
+ if (typeof c === "string") return {
23
+ name: c,
24
+ pathPrefix: `/${c.replace(/^\/+|\/+$/g, "")}`
25
+ };
26
+ return {
27
+ name: c.name,
28
+ pathPrefix: c.pathPrefix ? `/${c.pathPrefix.replace(/^\/+|\/+$/g, "")}` : `/${c.name.replace(/^\/+|\/+$/g, "")}`
29
+ };
30
+ });
31
+ if (dbMod.db) try {
32
+ const { pages } = await import("#db/schema").catch(() => ({ pages: null }));
33
+ const { and, isNull } = await import("drizzle-orm");
34
+ if (pages) {
35
+ const collectionNames = normalizedCollections.map((c) => c.name);
36
+ const prefixMap = new Map(normalizedCollections.map((c) => [c.name, c.pathPrefix]));
37
+ const matchingPages = (await dbMod.db.select().from(pages).where(and(isNull(pages.deletedAt))).catch(() => [])).filter((p) => collectionNames.includes(p.type));
38
+ for (const p of matchingPages) {
39
+ const prefix = prefixMap.get(p.type) || `/${p.type}`;
40
+ const title = typeof p.title === "string" ? p.title : p.title?.en || p.title && Object.values(p.title)[0] || p.slug;
41
+ const description = typeof p.description === "string" ? p.description : p.description?.en || p.description && Object.values(p.description)[0] || "";
42
+ items.push({
43
+ title,
44
+ description,
45
+ link: `${prefix}/${p.slug}`,
46
+ pubDate: p.postedAt || p.createdAt || /* @__PURE__ */ new Date()
47
+ });
48
+ }
49
+ }
50
+ } catch {}
51
+ if (items.length === 0) try {
52
+ const { getCollection } = await import("astro:content");
53
+ for (const coll of normalizedCollections) try {
54
+ const collEntries = await getCollection(coll.name);
55
+ if (Array.isArray(collEntries)) for (const post of collEntries) items.push({
56
+ title: post.data?.title || post.id,
57
+ pubDate: post.data?.pubDate || post.data?.date || /* @__PURE__ */ new Date(),
58
+ description: post.data?.description || post.data?.summary,
59
+ link: `${coll.pathPrefix}/${post.id}/`
60
+ });
61
+ } catch {}
62
+ } catch {}
63
+ }
64
+ const feedTitle = rssOpts.title || siteConfig.name || "RSS Feed";
65
+ const feedDesc = rssOpts.description || siteConfig.description || "";
66
+ const xml = buildRssXml({
67
+ title: feedTitle,
68
+ description: feedDesc,
69
+ site: siteUrl,
70
+ language: rssOpts.language || "en-US",
71
+ items
72
+ });
73
+ return new Response(xml, { headers: {
74
+ "Content-Type": "application/xml; charset=utf-8",
75
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
76
+ } });
77
+ };
78
+ //#endregion
79
+ export { GET };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/security.txt.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,24 @@
1
+ import { buildSecurityTxt } from "../security.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ //#region src/routes/security.txt.ts
5
+ const prerender = false;
6
+ const GET = ({ site, url }) => {
7
+ const siteConfig = siteConfigMod.siteConfig || {};
8
+ const siteUrl = site ? site.toString() : siteConfig.url || url.origin;
9
+ const secOpts = typeof seoOptions?.security === "object" ? seoOptions.security : {};
10
+ const defaultContact = siteConfig.email ? `mailto:${siteConfig.email}` : void 0;
11
+ const contacts = secOpts.contact || defaultContact || `mailto:security@${new URL(siteUrl).hostname}`;
12
+ const canonical = secOpts.canonical || new URL("/.well-known/security.txt", siteUrl).toString();
13
+ const content = buildSecurityTxt({
14
+ contact: contacts,
15
+ canonical,
16
+ ...secOpts
17
+ });
18
+ return new Response(content, { headers: {
19
+ "Content-Type": "text/plain; charset=utf-8",
20
+ "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
21
+ } });
22
+ };
23
+ //#endregion
24
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/site.webmanifest.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,23 @@
1
+ import { buildWebManifest } from "../manifest.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ //#region src/routes/site.webmanifest.ts
5
+ const prerender = false;
6
+ const GET = () => {
7
+ const siteConfig = siteConfigMod.siteConfig || {};
8
+ const manifestOpts = typeof seoOptions?.manifest === "object" ? seoOptions.manifest : {};
9
+ const manifest = buildWebManifest({
10
+ name: manifestOpts.name || siteConfig.name || "Site",
11
+ shortName: manifestOpts.shortName || siteConfig.name || "Site",
12
+ description: manifestOpts.description || siteConfig.description || "",
13
+ backgroundColor: manifestOpts.backgroundColor || siteConfig.branding?.colors?.backgroundColor || "#ffffff",
14
+ themeColor: manifestOpts.themeColor || siteConfig.branding?.colors?.themeColor || "#ffffff",
15
+ ...manifestOpts
16
+ });
17
+ return new Response(JSON.stringify(manifest, null, 2), { headers: {
18
+ "Content-Type": "application/manifest+json; charset=utf-8",
19
+ "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
20
+ } });
21
+ };
22
+ //#endregion
23
+ export { GET, prerender };
@@ -1,8 +1,22 @@
1
- import { buildSitemapXml } from "../sitemap.mjs";
2
- import seoConfig from "virtual:rimelight-seo-config";
1
+ import { buildSitemapUrls, buildSitemapXml } from "../sitemap.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ import * as seoConfig from "virtual:rimelight-seo/config";
3
5
  //#region src/routes/sitemap.xml.ts
4
- const GET = () => {
5
- const urls = (typeof seoConfig?.sitemap === "object" ? seoConfig.sitemap : {}).urls || [];
6
+ const GET = async ({ site, url }) => {
7
+ const sitemapOpts = typeof seoOptions?.sitemap === "object" ? seoOptions.sitemap : {};
8
+ const siteUrl = site ? site.toString() : siteConfigMod.siteConfig?.url || url.origin;
9
+ let entries = [];
10
+ if (typeof sitemapOpts.entries === "function") entries = await sitemapOpts.entries();
11
+ else if (typeof seoConfig.seoEntries === "function") entries = await seoConfig.seoEntries();
12
+ const locales = sitemapOpts.locales || seoConfig.SEO_LOCALES || (siteUrl ? {
13
+ site: siteUrl.replace(/\/$/, ""),
14
+ locales: { en: "en-US" },
15
+ defaultLocale: "en",
16
+ trailingSlash: "never"
17
+ } : void 0);
18
+ let urls = sitemapOpts.urls || [];
19
+ if (urls.length === 0 && entries.length > 0 && locales) urls = buildSitemapUrls(entries, locales);
6
20
  const xml = buildSitemapXml(urls);
7
21
  return new Response(xml, { headers: {
8
22
  "Content-Type": "application/xml; charset=utf-8",
package/dist/rss.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ import { RssFeedOptions } from "./types.mjs";
2
+ //#region src/rss.d.ts
3
+ export declare function escapeXml(str: string): string;
4
+ /**
5
+ * Builds standard RSS 2.0 feed XML document with Atom namespace link.
6
+ */
7
+ export declare function buildRssXml(options: RssFeedOptions): string;
8
+ //#endregion
package/dist/rss.mjs ADDED
@@ -0,0 +1,44 @@
1
+ //#region src/rss.ts
2
+ function escapeXml(str) {
3
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4
+ }
5
+ /**
6
+ * Builds standard RSS 2.0 feed XML document with Atom namespace link.
7
+ */
8
+ function buildRssXml(options) {
9
+ const siteUrl = (options.site || "").replace(/\/$/, "");
10
+ const feedUrl = options.feedUrl || `${siteUrl}/rss.xml`;
11
+ const language = options.language || "en-US";
12
+ const lastBuildDate = (/* @__PURE__ */ new Date()).toUTCString();
13
+ const itemXmls = (options.items || []).map((item) => {
14
+ const itemLink = item.link.startsWith("http://") || item.link.startsWith("https://") ? item.link : `${siteUrl}${item.link.startsWith("/") ? "" : "/"}${item.link}`;
15
+ const pubDate = new Date(item.pubDate).toUTCString();
16
+ const guid = item.guid || itemLink;
17
+ const lines = [
18
+ " <item>",
19
+ ` <title>${escapeXml(item.title)}</title>`,
20
+ ` <link>${escapeXml(itemLink)}</link>`,
21
+ ` <guid isPermaLink="${guid.startsWith("http") ? "true" : "false"}">${escapeXml(guid)}</guid>`,
22
+ ` <pubDate>${pubDate}</pubDate>`
23
+ ];
24
+ if (item.description) lines.push(` <description>${escapeXml(item.description)}</description>`);
25
+ if (item.author) lines.push(` <author>${escapeXml(item.author)}</author>`);
26
+ if (item.content) lines.push(` <content:encoded><![CDATA[${item.content}]]></content:encoded>`);
27
+ lines.push(" </item>");
28
+ return lines.join("\n");
29
+ });
30
+ return `<?xml version="1.0" encoding="UTF-8"?>
31
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
32
+ <channel>
33
+ <title>${escapeXml(options.title || "")}</title>
34
+ <description>${escapeXml(options.description || "")}</description>
35
+ <link>${escapeXml(siteUrl)}</link>
36
+ <language>${escapeXml(language)}</language>
37
+ <lastBuildDate>${lastBuildDate}</lastBuildDate>
38
+ <atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml"/>
39
+ ${itemXmls.join("\n")}
40
+ </channel>
41
+ </rss>`;
42
+ }
43
+ //#endregion
44
+ export { buildRssXml, escapeXml };
@@ -0,0 +1,7 @@
1
+ import { SecurityTxtOptions } from "./types.mjs";
2
+ //#region src/security.d.ts
3
+ /**
4
+ * Build RFC 9116 compliant security.txt content. https://www.rfc-editor.org/rfc/rfc9116
5
+ */
6
+ export declare function buildSecurityTxt(options: SecurityTxtOptions): string;
7
+ //#endregion
@@ -0,0 +1,34 @@
1
+ //#region src/security.ts
2
+ /**
3
+ * Build RFC 9116 compliant security.txt content. https://www.rfc-editor.org/rfc/rfc9116
4
+ */
5
+ function buildSecurityTxt(options) {
6
+ const lines = [];
7
+ const contacts = Array.isArray(options.contact) ? options.contact : options.contact ? [options.contact] : [];
8
+ for (const contact of contacts) lines.push(`Contact: ${contact}`);
9
+ let expiresStr;
10
+ if (options.expires instanceof Date) expiresStr = options.expires.toISOString();
11
+ else if (typeof options.expires === "string" && options.expires.trim().length > 0) expiresStr = options.expires;
12
+ else {
13
+ const oneYearFromNow = /* @__PURE__ */ new Date();
14
+ oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
15
+ expiresStr = oneYearFromNow.toISOString();
16
+ }
17
+ lines.push(`Expires: ${expiresStr}`);
18
+ if (options.encryption) {
19
+ const encList = Array.isArray(options.encryption) ? options.encryption : [options.encryption];
20
+ for (const enc of encList) lines.push(`Encryption: ${enc}`);
21
+ }
22
+ if (options.acknowledgments) {
23
+ const acks = Array.isArray(options.acknowledgments) ? options.acknowledgments : [options.acknowledgments];
24
+ for (const ack of acks) lines.push(`Acknowledgments: ${ack}`);
25
+ }
26
+ if (options.preferredLanguages) lines.push(`Preferred-Languages: ${options.preferredLanguages}`);
27
+ if (options.canonical) lines.push(`Canonical: ${options.canonical}`);
28
+ if (options.policy) lines.push(`Policy: ${options.policy}`);
29
+ if (options.hiring) lines.push(`Hiring: ${options.hiring}`);
30
+ if (options.csaf) lines.push(`CSAF: ${options.csaf}`);
31
+ return lines.join("\n") + "\n";
32
+ }
33
+ //#endregion
34
+ export { buildSecurityTxt };