@kamod-ch/otok-seo 2.0.0

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/dist/define-meta.d.ts +23 -0
  4. package/dist/define-meta.d.ts.map +1 -0
  5. package/dist/define-meta.js +36 -0
  6. package/dist/define-meta.js.map +1 -0
  7. package/dist/feeds.d.ts +13 -0
  8. package/dist/feeds.d.ts.map +1 -0
  9. package/dist/feeds.js +75 -0
  10. package/dist/feeds.js.map +1 -0
  11. package/dist/i18n.d.ts +16 -0
  12. package/dist/i18n.d.ts.map +1 -0
  13. package/dist/i18n.js +31 -0
  14. package/dist/i18n.js.map +1 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +11 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/loader.d.ts +15 -0
  20. package/dist/loader.d.ts.map +1 -0
  21. package/dist/loader.js +24 -0
  22. package/dist/loader.js.map +1 -0
  23. package/dist/middleware.d.ts +5 -0
  24. package/dist/middleware.d.ts.map +1 -0
  25. package/dist/middleware.js +89 -0
  26. package/dist/middleware.js.map +1 -0
  27. package/dist/plugin.d.ts +16 -0
  28. package/dist/plugin.d.ts.map +1 -0
  29. package/dist/plugin.js +41 -0
  30. package/dist/plugin.js.map +1 -0
  31. package/dist/registry.d.ts +14 -0
  32. package/dist/registry.d.ts.map +1 -0
  33. package/dist/registry.js +35 -0
  34. package/dist/registry.js.map +1 -0
  35. package/dist/resolve.d.ts +14 -0
  36. package/dist/resolve.d.ts.map +1 -0
  37. package/dist/resolve.js +95 -0
  38. package/dist/resolve.js.map +1 -0
  39. package/dist/robots.d.ts +9 -0
  40. package/dist/robots.d.ts.map +1 -0
  41. package/dist/robots.js +21 -0
  42. package/dist/robots.js.map +1 -0
  43. package/dist/sitemap.d.ts +20 -0
  44. package/dist/sitemap.d.ts.map +1 -0
  45. package/dist/sitemap.js +40 -0
  46. package/dist/sitemap.js.map +1 -0
  47. package/dist/types.d.ts +123 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamod
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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @kamod-ch/otok-seo
2
+
3
+ Typed route metadata, sitemaps, feeds, and SEO helpers for [Otok](https://github.com/kamod-ch/otok).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @kamod-ch/otok-seo hono @kamod-ch/otok
9
+ ```
10
+
11
+ ## Plugin setup
12
+
13
+ ```ts
14
+ import { defineConfig } from "@kamod-ch/otok";
15
+ import seo from "@kamod-ch/otok-seo";
16
+
17
+ export default defineConfig({
18
+ plugins: [
19
+ seo({
20
+ origin: "https://example.com",
21
+ titleTemplate: "%s | Example",
22
+ siteName: "Example",
23
+ sitemapPaths: ["/", "/about", "/products"],
24
+ robots: true,
25
+ icons: [{ href: "/favicon.ico" }],
26
+ manifest: { href: "/site.webmanifest" },
27
+ }),
28
+ ],
29
+ });
30
+ ```
31
+
32
+ Serves `/robots.txt` and `/sitemap.xml` automatically. With `@kamod-ch/otok-i18n`, sitemaps include hreflang alternates.
33
+
34
+ ## Route metadata
35
+
36
+ ```ts
37
+ import { defineMeta } from "@kamod-ch/otok-seo";
38
+
39
+ export const head = defineMeta(({ data, locale }) => ({
40
+ title: data.product.name,
41
+ description: data.product.description,
42
+ canonical: `/products/${data.product.slug}`,
43
+ lang: locale,
44
+ openGraph: { type: "product" },
45
+ twitter: { card: "summary_large_image" },
46
+ jsonLd: {
47
+ "@context": "https://schema.org",
48
+ "@type": "Product",
49
+ name: data.product.name,
50
+ },
51
+ }));
52
+ ```
53
+
54
+ ## i18n integration
55
+
56
+ ```ts
57
+ import { seoI18nHead } from "@kamod-ch/otok-seo";
58
+
59
+ export async function head({ data, params }) {
60
+ return seoI18nHead({
61
+ locale: data.locale,
62
+ locales: ["de", "en"],
63
+ defaultLocale: "de",
64
+ origin: "https://example.com",
65
+ pathname: `/products/${params.slug}`,
66
+ meta: {
67
+ title: data.product.name,
68
+ description: data.product.description,
69
+ },
70
+ });
71
+ }
72
+ ```
73
+
74
+ ## Feeds
75
+
76
+ ```ts
77
+ seo({
78
+ origin: "https://example.com",
79
+ rss: {
80
+ path: "/feed.xml",
81
+ title: "Blog",
82
+ items: () => loadPosts(),
83
+ },
84
+ });
85
+ ```
86
+
87
+ ## Dynamic OG images
88
+
89
+ ```ts
90
+ seo({
91
+ origin: "https://example.com",
92
+ ogImage: ({ route, title }) => `/api/og?route=${encodeURIComponent(route)}&title=${encodeURIComponent(title ?? "")}`,
93
+ });
94
+ ```
95
+
96
+ ## Middleware order
97
+
98
+ Register `seo()` after `i18n()` so locale is available in loaders and OG hooks. Security middleware from `@kamod-ch/otok-security` should run before SEO utility routes.
99
+
100
+ ## API
101
+
102
+ | Export | Purpose |
103
+ |--------|---------|
104
+ | `defineMeta` / `meta` | Typed route `head` factory |
105
+ | `resolveMetaToHead` | Convert `RouteMeta` → `OtokHead` |
106
+ | `seoI18nHead` | Merge metadata with i18n hreflang |
107
+ | `createSitemapEntries` / `renderSitemapXml` | Sitemap helpers |
108
+ | `renderRobotsTxt` | robots.txt renderer |
109
+ | `renderRssFeed` / `renderAtomFeed` | Feed renderers |
110
+ | `defineLoader` | Loader with `seo.origin` / locale |
@@ -0,0 +1,23 @@
1
+ import type { LoaderResult, OtokHead, OtokPageProps } from "@kamod-ch/otok/server";
2
+ import { type ResolveMetaOptions } from "./resolve.js";
3
+ import type { MetaContext, RouteMeta } from "./types.js";
4
+ export interface DefineMetaOptions extends ResolveMetaOptions {
5
+ /** Resolve locale from loader data or Hono context. */
6
+ getLocale?: (ctx: MetaContext) => string | undefined;
7
+ }
8
+ /**
9
+ * Define typed route metadata that resolves into an Otok `head` export.
10
+ *
11
+ * ```ts
12
+ * export const head = defineMeta(({ data, locale }) => ({
13
+ * title: data.product.name,
14
+ * description: data.product.description,
15
+ * canonical: `/products/${data.product.slug}`,
16
+ * openGraph: { type: "product" },
17
+ * }));
18
+ * ```
19
+ */
20
+ export declare function defineMeta<Data extends LoaderResult = LoaderResult>(resolver: (ctx: MetaContext<Data>) => RouteMeta | Promise<RouteMeta>, options?: DefineMetaOptions): (props: OtokPageProps<Data>) => Promise<OtokHead>;
21
+ /** Alias for route modules that export `meta` instead of `head`. */
22
+ export declare const meta: typeof defineMeta;
23
+ //# sourceMappingURL=define-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-meta.d.ts","sourceRoot":"","sources":["../src/define-meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,uDAAuD;IACvD,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAC;CACtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,SAAS,YAAY,GAAG,YAAY,EACjE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,EACpE,OAAO,GAAE,iBAAsB,GAC9B,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoBnD;AAED,oEAAoE;AACpE,eAAO,MAAM,IAAI,mBAAa,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { resolveMetaToHead } from "./resolve.js";
2
+ /**
3
+ * Define typed route metadata that resolves into an Otok `head` export.
4
+ *
5
+ * ```ts
6
+ * export const head = defineMeta(({ data, locale }) => ({
7
+ * title: data.product.name,
8
+ * description: data.product.description,
9
+ * canonical: `/products/${data.product.slug}`,
10
+ * openGraph: { type: "product" },
11
+ * }));
12
+ * ```
13
+ */
14
+ export function defineMeta(resolver, options = {}) {
15
+ return async (props) => {
16
+ const ctx = {
17
+ data: props.data,
18
+ params: props.params,
19
+ route: props.route,
20
+ locale: options.getLocale?.({
21
+ data: props.data,
22
+ params: props.params,
23
+ route: props.route,
24
+ origin: options.origin,
25
+ }),
26
+ origin: options.origin,
27
+ };
28
+ const meta = await resolver(ctx);
29
+ if (ctx.locale && !meta.lang)
30
+ meta.lang = ctx.locale;
31
+ return resolveMetaToHead(meta, options);
32
+ };
33
+ }
34
+ /** Alias for route modules that export `meta` instead of `head`. */
35
+ export const meta = defineMeta;
36
+ //# sourceMappingURL=define-meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-meta.js","sourceRoot":"","sources":["../src/define-meta.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAA2B,MAAM,cAAc,CAAC;AAQ1E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,QAAoE,EACpE,UAA6B,EAAE;IAE/B,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,GAAG,GAAsB;YAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;gBAC1B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC;YACF,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC;QAEF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC;QAErD,OAAO,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,MAAM,IAAI,GAAG,UAAU,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { FeedConfig } from "./types.js";
2
+ export interface RenderFeedOptions {
3
+ origin: string;
4
+ feed: FeedConfig;
5
+ format: "rss" | "atom";
6
+ }
7
+ /** Render an RSS 2.0 feed. */
8
+ export declare function renderRssFeed(options: RenderFeedOptions): Promise<string>;
9
+ /** Render an Atom 1.0 feed. */
10
+ export declare function renderAtomFeed(options: RenderFeedOptions): Promise<string>;
11
+ /** Render RSS or Atom based on format. */
12
+ export declare function renderFeed(options: RenderFeedOptions): Promise<string>;
13
+ //# sourceMappingURL=feeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB;AAoBD,8BAA8B;AAC9B,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B/E;AAED,+BAA+B;AAC/B,wBAAsB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BhF;AAED,0CAA0C;AAC1C,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5E"}
package/dist/feeds.js ADDED
@@ -0,0 +1,75 @@
1
+ function escapeXml(value) {
2
+ return value
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&apos;");
8
+ }
9
+ function resolveLink(origin, link) {
10
+ if (/^https?:\/\//i.test(link))
11
+ return link;
12
+ return `${origin.replace(/\/$/, "")}${link.startsWith("/") ? link : `/${link}`}`;
13
+ }
14
+ async function resolveItems(feed) {
15
+ return typeof feed.items === "function" ? await feed.items() : feed.items;
16
+ }
17
+ /** Render an RSS 2.0 feed. */
18
+ export async function renderRssFeed(options) {
19
+ const { origin, feed } = options;
20
+ const items = await resolveItems(feed);
21
+ const channelLink = resolveLink(origin, feed.path);
22
+ const itemXml = items
23
+ .map((item) => {
24
+ const pubDate = item.publishedAt ? `\n <pubDate>${escapeXml(new Date(item.publishedAt).toUTCString())}</pubDate>` : "";
25
+ const description = item.description ? `\n <description>${escapeXml(item.description)}</description>` : "";
26
+ const author = item.author ? `\n <author>${escapeXml(item.author)}</author>` : "";
27
+ return ` <item>
28
+ <title>${escapeXml(item.title)}</title>
29
+ <link>${escapeXml(resolveLink(origin, item.link))}</link>
30
+ <guid isPermaLink="false">${escapeXml(item.id)}</guid>${description}${author}${pubDate}
31
+ </item>`;
32
+ })
33
+ .join("\n");
34
+ return `<?xml version="1.0" encoding="UTF-8"?>
35
+ <rss version="2.0">
36
+ <channel>
37
+ <title>${escapeXml(feed.title)}</title>
38
+ <link>${escapeXml(channelLink)}</link>
39
+ ${feed.description ? `<description>${escapeXml(feed.description)}</description>` : ""}
40
+ ${itemXml}
41
+ </channel>
42
+ </rss>`;
43
+ }
44
+ /** Render an Atom 1.0 feed. */
45
+ export async function renderAtomFeed(options) {
46
+ const { origin, feed } = options;
47
+ const items = await resolveItems(feed);
48
+ const feedLink = resolveLink(origin, feed.path);
49
+ const entries = items
50
+ .map((item) => {
51
+ const updated = item.updatedAt ?? item.publishedAt;
52
+ return ` <entry>
53
+ <title>${escapeXml(item.title)}</title>
54
+ <link href="${escapeXml(resolveLink(origin, item.link))}" />
55
+ <id>${escapeXml(item.id)}</id>
56
+ ${updated ? `<updated>${escapeXml(new Date(updated).toISOString())}</updated>` : ""}
57
+ ${item.description ? `<summary>${escapeXml(item.description)}</summary>` : ""}
58
+ ${item.author ? `<author><name>${escapeXml(item.author)}</name></author>` : ""}
59
+ </entry>`;
60
+ })
61
+ .join("\n");
62
+ return `<?xml version="1.0" encoding="UTF-8"?>
63
+ <feed xmlns="http://www.w3.org/2005/Atom">
64
+ <title>${escapeXml(feed.title)}</title>
65
+ <link href="${escapeXml(feedLink)}" rel="self" />
66
+ <id>${escapeXml(feedLink)}</id>
67
+ ${feed.description ? `<subtitle>${escapeXml(feed.description)}</subtitle>` : ""}
68
+ ${entries}
69
+ </feed>`;
70
+ }
71
+ /** Render RSS or Atom based on format. */
72
+ export async function renderFeed(options) {
73
+ return options.format === "atom" ? renderAtomFeed(options) : renderRssFeed(options);
74
+ }
75
+ //# sourceMappingURL=feeds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.js","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { I18nHeadOptions } from "@kamod-ch/otok-i18n";
2
+ import type { OtokHead } from "@kamod-ch/otok/server";
3
+ import type { RouteMeta } from "./types.js";
4
+ export interface SeoI18nHeadOptions extends I18nHeadOptions {
5
+ meta?: RouteMeta;
6
+ titleTemplate?: string;
7
+ siteName?: string;
8
+ defaultOgImage?: string;
9
+ twitterSite?: string;
10
+ }
11
+ /**
12
+ * Merge route metadata with i18n head output (lang, hreflang, canonical).
13
+ * Requires `@kamod-ch/otok-i18n` at runtime.
14
+ */
15
+ export declare function seoI18nHead(options: SeoI18nHeadOptions): OtokHead;
16
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,QAAQ,CAyBjE"}
package/dist/i18n.js ADDED
@@ -0,0 +1,31 @@
1
+ import { i18nHead } from "@kamod-ch/otok-i18n";
2
+ import { resolveMetaToHead } from "./resolve.js";
3
+ /**
4
+ * Merge route metadata with i18n head output (lang, hreflang, canonical).
5
+ * Requires `@kamod-ch/otok-i18n` at runtime.
6
+ */
7
+ export function seoI18nHead(options) {
8
+ const { meta, titleTemplate, siteName, defaultOgImage, twitterSite, ...i18nOptions } = options;
9
+ const i18n = i18nHead(i18nOptions);
10
+ if (!meta)
11
+ return i18n;
12
+ const resolved = resolveMetaToHead(meta, {
13
+ origin: i18nOptions.origin,
14
+ titleTemplate,
15
+ siteName,
16
+ defaultOgImage,
17
+ twitterSite,
18
+ });
19
+ const links = [...(resolved.links ?? [])];
20
+ for (const link of i18n.links ?? []) {
21
+ if (!links.some((existing) => existing.rel === link.rel && existing.href === link.href)) {
22
+ links.push(link);
23
+ }
24
+ }
25
+ return {
26
+ ...resolved,
27
+ lang: i18n.lang ?? resolved.lang,
28
+ links,
29
+ };
30
+ }
31
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAWjD;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;IAC/F,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,EAAE;QACvC,MAAM,EAAE,WAAW,CAAC,MAAM;QAC1B,aAAa;QACb,QAAQ;QACR,cAAc;QACd,WAAW;KACZ,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACxF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,QAAQ;QACX,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI;QAChC,KAAK;KACN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { defineMeta, meta, type DefineMetaOptions } from "./define-meta.js";
2
+ export { seoI18nHead, type SeoI18nHeadOptions } from "./i18n.js";
3
+ export { createMetaContext, defineLoader } from "./loader.js";
4
+ export { configureSeoApp } from "./middleware.js";
5
+ export { default } from "./plugin.js";
6
+ export { getSeoRuntime, normalizeSeoOptions, readSeoOrigin, registerSeoRuntime, resetSeoRuntimeForTests, tryGetSeoRuntime, type NormalizedSeoOptions, } from "./registry.js";
7
+ export { resolveMetaToHead, type ResolveMetaOptions } from "./resolve.js";
8
+ export { renderAtomFeed, renderFeed, renderRssFeed, type RenderFeedOptions } from "./feeds.js";
9
+ export { renderRobotsTxt, type RenderRobotsOptions } from "./robots.js";
10
+ export { createSitemapEntries, renderSitemapXml, type CreateSitemapOptions, type SitemapEntry } from "./sitemap.js";
11
+ export type { FeedConfig, FeedItem, HreflangAlternate, MetaContext, OgImageContext, OgImageHook, OpenGraphImage, OpenGraphMeta, OpenGraphType, RobotsConfig, RouteIcon, RouteManifestRef, RouteMeta, SeoPluginOptions, TwitterCardMeta, } from "./types.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,uBAAuB,EACvB,gBAAgB,EAChB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACpH,YAAY,EACV,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,SAAS,EACT,gBAAgB,EAChB,eAAe,GAChB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { defineMeta, meta } from "./define-meta.js";
2
+ export { seoI18nHead } from "./i18n.js";
3
+ export { createMetaContext, defineLoader } from "./loader.js";
4
+ export { configureSeoApp } from "./middleware.js";
5
+ export { default } from "./plugin.js";
6
+ export { getSeoRuntime, normalizeSeoOptions, readSeoOrigin, registerSeoRuntime, resetSeoRuntimeForTests, tryGetSeoRuntime, } from "./registry.js";
7
+ export { resolveMetaToHead } from "./resolve.js";
8
+ export { renderAtomFeed, renderFeed, renderRssFeed } from "./feeds.js";
9
+ export { renderRobotsTxt } from "./robots.js";
10
+ export { createSitemapEntries, renderSitemapXml } from "./sitemap.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAA0B,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,WAAW,EAA2B,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,uBAAuB,EACvB,gBAAgB,GAEjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAA2B,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAA0B,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,eAAe,EAA4B,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAgD,MAAM,cAAc,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Context } from "hono";
2
+ import type { LoaderResult, OtokContext, OtokLoader } from "@kamod-ch/otok/server";
3
+ import type { MetaContext } from "./types.js";
4
+ type SeoLoaderContext = {
5
+ seo: {
6
+ origin: string;
7
+ locale?: string;
8
+ };
9
+ };
10
+ /** Wrap a loader with typed SEO context (origin, locale). */
11
+ export declare function defineLoader<Data extends LoaderResult>(handler: (ctx: OtokContext & SeoLoaderContext) => Data | Promise<Data>): OtokLoader<Data>;
12
+ /** Build meta context from a Hono request — useful in OG image routes. */
13
+ export declare function createMetaContext(c: Context, partial?: Partial<MetaContext>): MetaContext;
14
+ export {};
15
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,KAAK,gBAAgB,GAAG;IACtB,GAAG,EAAE;QACH,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AASF,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,SAAS,YAAY,EACpD,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACrE,UAAU,CAAC,IAAI,CAAC,CAElB;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAU7F"}
package/dist/loader.js ADDED
@@ -0,0 +1,24 @@
1
+ import { getSeoRuntime, readSeoOrigin } from "./registry.js";
2
+ function resolveSeoContext(hono) {
3
+ const runtime = getSeoRuntime();
4
+ const origin = readSeoOrigin(hono) ?? runtime.origin;
5
+ const i18n = hono.get("i18n");
6
+ return { origin, locale: i18n?.locale };
7
+ }
8
+ /** Wrap a loader with typed SEO context (origin, locale). */
9
+ export function defineLoader(handler) {
10
+ return (context) => handler({ ...context, seo: resolveSeoContext(context.hono) });
11
+ }
12
+ /** Build meta context from a Hono request — useful in OG image routes. */
13
+ export function createMetaContext(c, partial = {}) {
14
+ const runtime = getSeoRuntime();
15
+ const i18n = c.get("i18n");
16
+ return {
17
+ data: partial.data ?? {},
18
+ params: partial.params ?? {},
19
+ route: partial.route ?? new URL(c.req.url).pathname,
20
+ locale: partial.locale ?? i18n?.locale,
21
+ origin: partial.origin ?? readSeoOrigin(c) ?? runtime.origin,
22
+ };
23
+ }
24
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAU7D,SAAS,iBAAiB,CAAC,IAAyB;IAClD,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAe,CAAoC,CAAC;IAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1C,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAC1B,OAAsE;IAEtE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,CAAU,EAAE,UAAgC,EAAE;IAC9E,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAe,CAAoC,CAAC;IACvE,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;QAC5B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ;QACnD,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,MAAM;QACtC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM;KAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Hono } from "hono";
2
+ import type { NormalizedSeoOptions } from "./registry.js";
3
+ /** Register SEO utility routes on the Hono app. */
4
+ export declare function configureSeoApp(app: Hono, options: NormalizedSeoOptions): void;
5
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAIjC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AA8C1D,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAoD9E"}
@@ -0,0 +1,89 @@
1
+ import { renderFeed } from "./feeds.js";
2
+ import { renderRobotsTxt } from "./robots.js";
3
+ import { createSitemapEntries, renderSitemapXml } from "./sitemap.js";
4
+ async function buildLocalizedSitemap(options) {
5
+ const i18nRuntime = await tryImportI18n();
6
+ if (i18nRuntime) {
7
+ const { createLocalizedSitemapEntries, renderSitemapXml: renderI18nSitemap } = i18nRuntime;
8
+ const entries = createLocalizedSitemapEntries(options.sitemapPaths ?? [], {
9
+ origin: options.origin,
10
+ locales: i18nRuntime.locales,
11
+ defaultLocale: i18nRuntime.defaultLocale,
12
+ routing: i18nRuntime.routing,
13
+ });
14
+ return renderI18nSitemap(entries);
15
+ }
16
+ const entries = createSitemapEntries({
17
+ origin: options.origin,
18
+ paths: options.sitemapPaths ?? [],
19
+ });
20
+ return renderSitemapXml(entries);
21
+ }
22
+ async function tryImportI18n() {
23
+ try {
24
+ const { tryGetI18nRuntime } = await import("@kamod-ch/otok-i18n");
25
+ const runtime = tryGetI18nRuntime();
26
+ if (!runtime)
27
+ return null;
28
+ const sitemap = await import("@kamod-ch/otok-i18n/sitemap");
29
+ return {
30
+ createLocalizedSitemapEntries: sitemap.createLocalizedSitemapEntries,
31
+ renderSitemapXml: sitemap.renderSitemapXml,
32
+ locales: runtime.options.locales,
33
+ defaultLocale: runtime.options.defaultLocale,
34
+ routing: runtime.options.routing,
35
+ };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /** Register SEO utility routes on the Hono app. */
42
+ export function configureSeoApp(app, options) {
43
+ const contextKey = "seoOrigin";
44
+ app.use("*", async (c, next) => {
45
+ c.set(contextKey, options.origin);
46
+ await next();
47
+ });
48
+ if (options.robotsEnabled) {
49
+ app.get("/robots.txt", (c) => {
50
+ const body = renderRobotsTxt({
51
+ config: options.robots ?? true,
52
+ origin: options.origin,
53
+ });
54
+ return c.text(body, 200, { "content-type": "text/plain; charset=utf-8" });
55
+ });
56
+ }
57
+ if (options.sitemapEnabled) {
58
+ app.get("/sitemap.xml", async (c) => {
59
+ const body = await buildLocalizedSitemap(options);
60
+ return c.text(body, 200, { "content-type": "application/xml; charset=utf-8" });
61
+ });
62
+ }
63
+ if (options.rss) {
64
+ app.get(options.rss.path, async (c) => {
65
+ const body = await renderFeed({ origin: options.origin, feed: options.rss, format: "rss" });
66
+ return c.text(body, 200, { "content-type": "application/rss+xml; charset=utf-8" });
67
+ });
68
+ }
69
+ if (options.atom) {
70
+ app.get(options.atom.path, async (c) => {
71
+ const body = await renderFeed({ origin: options.origin, feed: options.atom, format: "atom" });
72
+ return c.text(body, 200, { "content-type": "application/atom+xml; charset=utf-8" });
73
+ });
74
+ }
75
+ if (options.ogImage) {
76
+ app.get("/og-image", async (c) => {
77
+ const url = new URL(c.req.url);
78
+ const ctx = {
79
+ route: url.searchParams.get("route") ?? "/",
80
+ params: Object.fromEntries(url.searchParams.entries()),
81
+ locale: url.searchParams.get("locale") ?? undefined,
82
+ title: url.searchParams.get("title") ?? undefined,
83
+ };
84
+ const imageUrl = await options.ogImage(ctx);
85
+ return c.redirect(imageUrl, 302);
86
+ });
87
+ }
88
+ }
89
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { configureSeoApp } from "./middleware.js";
2
+ import type { SeoPluginOptions } from "./types.js";
3
+ /**
4
+ * Otok SEO plugin factory.
5
+ *
6
+ * ```ts
7
+ * import seo from "@kamod-ch/otok-seo";
8
+ *
9
+ * export default defineConfig({
10
+ * plugins: [seo({ origin: "https://example.com", titleTemplate: "%s | Example" })],
11
+ * });
12
+ * ```
13
+ */
14
+ export default function seo(options: SeoPluginOptions): import("@kamod-ch/otok-config").OtokPlugin<unknown>;
15
+ export { configureSeoApp };
16
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAmBnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,OAAO,EAAE,gBAAgB,uDASpD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/plugin.js ADDED
@@ -0,0 +1,41 @@
1
+ import { definePlugin } from "@kamod-ch/otok";
2
+ import { configureSeoApp } from "./middleware.js";
3
+ import { normalizeSeoOptions, registerSeoRuntime } from "./registry.js";
4
+ const seoPluginFactory = definePlugin({
5
+ name: "@kamod-ch/otok-seo",
6
+ version: "1.0.0",
7
+ schema: {
8
+ parse(input) {
9
+ if (!input || typeof input !== "object") {
10
+ throw new Error("seo() options must be an object");
11
+ }
12
+ const record = input;
13
+ if (typeof record.origin !== "string" || !record.origin) {
14
+ throw new Error("seo() requires origin: string");
15
+ }
16
+ return normalizeSeoOptions(input);
17
+ },
18
+ },
19
+ });
20
+ /**
21
+ * Otok SEO plugin factory.
22
+ *
23
+ * ```ts
24
+ * import seo from "@kamod-ch/otok-seo";
25
+ *
26
+ * export default defineConfig({
27
+ * plugins: [seo({ origin: "https://example.com", titleTemplate: "%s | Example" })],
28
+ * });
29
+ * ```
30
+ */
31
+ export default function seo(options) {
32
+ const normalized = normalizeSeoOptions(options);
33
+ registerSeoRuntime(normalized);
34
+ const plugin = seoPluginFactory(normalized);
35
+ plugin.configureApp = ({ app }) => {
36
+ configureSeoApp(app, normalized);
37
+ };
38
+ return plugin;
39
+ }
40
+ export { configureSeoApp };
41
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxE,MAAM,gBAAgB,GAAG,YAAY,CAAmB;IACtD,IAAI,EAAE,oBAAoB;IAC1B,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE;QACN,KAAK,CAAC,KAAK;YACT,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;YACrD,CAAC;YACD,MAAM,MAAM,GAAG,KAAgC,CAAC;YAChD,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACxD,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,CAAC;YACD,OAAO,mBAAmB,CAAC,KAAyB,CAAC,CAAC;QACxD,CAAC;KACF;CACF,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,OAAyB;IACnD,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAChD,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAE/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QAChC,eAAe,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACnC,CAAC,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Context } from "hono";
2
+ import type { SeoPluginOptions } from "./types.js";
3
+ export interface NormalizedSeoOptions extends SeoPluginOptions {
4
+ origin: string;
5
+ robotsEnabled: boolean;
6
+ sitemapEnabled: boolean;
7
+ }
8
+ export declare function normalizeSeoOptions(options: SeoPluginOptions): NormalizedSeoOptions;
9
+ export declare function registerSeoRuntime(options: NormalizedSeoOptions): void;
10
+ export declare function getSeoRuntime(): NormalizedSeoOptions;
11
+ export declare function tryGetSeoRuntime(): NormalizedSeoOptions | null;
12
+ export declare function readSeoOrigin(c: Context, contextKey?: string): string | undefined;
13
+ export declare function resetSeoRuntimeForTests(): void;
14
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,oBAAoB,CAgBnF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAEtE;AAED,wBAAgB,aAAa,IAAI,oBAAoB,CAKpD;AAED,wBAAgB,gBAAgB,IAAI,oBAAoB,GAAG,IAAI,CAE9D;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,SAAc,GAAG,MAAM,GAAG,SAAS,CAEtF;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
@@ -0,0 +1,35 @@
1
+ let runtime = null;
2
+ export function normalizeSeoOptions(options) {
3
+ if (!options.origin || typeof options.origin !== "string") {
4
+ throw new Error("seo() requires origin: string (absolute site URL)");
5
+ }
6
+ const sitemapPaths = options.sitemapPaths ?? [];
7
+ const sitemapEnabled = options.sitemap !== false && sitemapPaths.length > 0;
8
+ const robotsEnabled = options.robots !== false;
9
+ return {
10
+ ...options,
11
+ origin: options.origin.replace(/\/$/, ""),
12
+ sitemapPaths,
13
+ robotsEnabled,
14
+ sitemapEnabled,
15
+ };
16
+ }
17
+ export function registerSeoRuntime(options) {
18
+ runtime = options;
19
+ }
20
+ export function getSeoRuntime() {
21
+ if (!runtime) {
22
+ throw new Error("otok-seo: plugin not registered. Add seo() to otok.config.ts plugins.");
23
+ }
24
+ return runtime;
25
+ }
26
+ export function tryGetSeoRuntime() {
27
+ return runtime;
28
+ }
29
+ export function readSeoOrigin(c, contextKey = "seoOrigin") {
30
+ return c.get(contextKey);
31
+ }
32
+ export function resetSeoRuntimeForTests() {
33
+ runtime = null;
34
+ }
35
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,IAAI,OAAO,GAAgC,IAAI,CAAC;AAQhD,MAAM,UAAU,mBAAmB,CAAC,OAAyB;IAC3D,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;IAChD,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5E,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC;IAE/C,OAAO;QACL,GAAG,OAAO;QACV,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QACzC,YAAY;QACZ,aAAa;QACb,cAAc;KACf,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC9D,OAAO,GAAG,OAAO,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAU,EAAE,UAAU,GAAG,WAAW;IAChE,OAAO,CAAC,CAAC,GAAG,CAAC,UAAU,CAAuB,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { OtokHead } from "@kamod-ch/otok/server";
2
+ import type { RouteMeta, SeoPluginOptions } from "./types.js";
3
+ export interface ResolveMetaOptions {
4
+ origin?: string;
5
+ titleTemplate?: string;
6
+ siteName?: string;
7
+ defaultOgImage?: string;
8
+ twitterSite?: string;
9
+ globalIcons?: SeoPluginOptions["icons"];
10
+ globalManifest?: SeoPluginOptions["manifest"];
11
+ }
12
+ /** Convert typed route metadata into an OtokHead result. */
13
+ export declare function resolveMetaToHead(meta: RouteMeta, options?: ResolveMetaOptions): OtokHead;
14
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAwB9D,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACxC,cAAc,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC;CAC/C;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,kBAAuB,GAAG,QAAQ,CAoE7F"}
@@ -0,0 +1,95 @@
1
+ function joinOrigin(origin, path) {
2
+ if (/^https?:\/\//i.test(path))
3
+ return path;
4
+ if (!origin)
5
+ return path;
6
+ const base = origin.replace(/\/$/, "");
7
+ const normalized = path.startsWith("/") ? path : `/${path}`;
8
+ return `${base}${normalized}`;
9
+ }
10
+ function applyTitleTemplate(title, template) {
11
+ if (!title)
12
+ return undefined;
13
+ if (!template)
14
+ return title;
15
+ return template.includes("%s") ? template.replace("%s", title) : `${title}${template}`;
16
+ }
17
+ function normalizeOgImages(image, defaultImage) {
18
+ const raw = Array.isArray(image) ? image : image ? [image] : defaultImage ? [defaultImage] : [];
19
+ return raw.map((entry) => (typeof entry === "string" ? { url: entry } : entry));
20
+ }
21
+ /** Convert typed route metadata into an OtokHead result. */
22
+ export function resolveMetaToHead(meta, options = {}) {
23
+ const title = applyTitleTemplate(meta.title, options.titleTemplate);
24
+ const canonicalHref = meta.canonical ? joinOrigin(options.origin ?? meta.canonical, meta.canonical) : undefined;
25
+ const propertyMeta = {};
26
+ const nameMeta = { ...(meta.meta ?? {}) };
27
+ if (meta.robots)
28
+ nameMeta.robots = meta.robots;
29
+ const og = meta.openGraph ?? {};
30
+ const ogTitle = og.title ?? meta.title;
31
+ const ogDescription = og.description ?? meta.description;
32
+ const ogUrl = og.url ?? canonicalHref;
33
+ if (og.type)
34
+ propertyMeta["og:type"] = og.type;
35
+ if (ogTitle)
36
+ propertyMeta["og:title"] = ogTitle;
37
+ if (ogDescription)
38
+ propertyMeta["og:description"] = ogDescription;
39
+ if (ogUrl)
40
+ propertyMeta["og:url"] = joinOrigin(options.origin, ogUrl);
41
+ if (options.siteName ?? og.siteName)
42
+ propertyMeta["og:site_name"] = options.siteName ?? og.siteName;
43
+ if (og.locale)
44
+ propertyMeta["og:locale"] = og.locale;
45
+ const images = normalizeOgImages(og.images ?? og.image ?? undefined, options.defaultOgImage);
46
+ if (images[0])
47
+ propertyMeta["og:image"] = joinOrigin(options.origin, images[0].url);
48
+ const twitter = meta.twitter ?? {};
49
+ const twitterCard = twitter.card ?? (images[0] ? "summary_large_image" : "summary");
50
+ nameMeta["twitter:card"] = twitterCard;
51
+ if (twitter.site ?? options.twitterSite)
52
+ nameMeta["twitter:site"] = twitter.site ?? options.twitterSite;
53
+ if (twitter.creator)
54
+ nameMeta["twitter:creator"] = twitter.creator;
55
+ if (twitter.title ?? ogTitle)
56
+ nameMeta["twitter:title"] = twitter.title ?? ogTitle;
57
+ if (twitter.description ?? ogDescription)
58
+ nameMeta["twitter:description"] = twitter.description ?? ogDescription;
59
+ const twitterImage = twitter.image ?? images[0]?.url;
60
+ if (twitterImage)
61
+ nameMeta["twitter:image"] = joinOrigin(options.origin, twitterImage);
62
+ const links = [
63
+ ...(canonicalHref ? [{ rel: "canonical", href: canonicalHref }] : []),
64
+ ...(meta.hreflang ?? []).map((alt) => ({
65
+ rel: "alternate",
66
+ href: alt.href,
67
+ hreflang: alt.hreflang,
68
+ })),
69
+ ...(meta.icons ?? options.globalIcons ?? []).map((icon) => ({
70
+ rel: icon.rel ?? "icon",
71
+ href: joinOrigin(options.origin, icon.href),
72
+ type: icon.type,
73
+ })),
74
+ ...(meta.manifest ?? options.globalManifest
75
+ ? [{ rel: "manifest", href: joinOrigin(options.origin, (meta.manifest ?? options.globalManifest).href) }]
76
+ : []),
77
+ ...(meta.rss ? [{ rel: "alternate", href: joinOrigin(options.origin, meta.rss), type: "application/rss+xml" }] : []),
78
+ ...(meta.atom ? [{ rel: "alternate", href: joinOrigin(options.origin, meta.atom), type: "application/atom+xml" }] : []),
79
+ ];
80
+ const jsonLd = Array.isArray(meta.jsonLd)
81
+ ? meta.jsonLd.length === 1
82
+ ? meta.jsonLd[0]
83
+ : { "@context": "https://schema.org", "@graph": meta.jsonLd }
84
+ : meta.jsonLd;
85
+ return {
86
+ title,
87
+ description: meta.description,
88
+ lang: meta.lang,
89
+ meta: nameMeta,
90
+ propertyMeta,
91
+ links,
92
+ jsonLd,
93
+ };
94
+ }
95
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { RobotsConfig } from "./types.js";
2
+ export interface RenderRobotsOptions {
3
+ config?: boolean | RobotsConfig;
4
+ origin?: string;
5
+ sitemapPath?: string;
6
+ }
7
+ /** Render a robots.txt document from plugin options. */
8
+ export declare function renderRobotsTxt(options?: RenderRobotsOptions): string;
9
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAmBzE"}
package/dist/robots.js ADDED
@@ -0,0 +1,21 @@
1
+ /** Render a robots.txt document from plugin options. */
2
+ export function renderRobotsTxt(options = {}) {
3
+ const { config = true, origin, sitemapPath = "/sitemap.xml" } = options;
4
+ if (config === false)
5
+ return "User-agent: *\nDisallow: /";
6
+ const rules = typeof config === "object" ? config : {};
7
+ const lines = ["User-agent: *"];
8
+ for (const path of rules.allow ?? ["/"]) {
9
+ lines.push(`Allow: ${path}`);
10
+ }
11
+ for (const path of rules.disallow ?? []) {
12
+ lines.push(`Disallow: ${path}`);
13
+ }
14
+ const sitemap = rules.sitemap ?? (origin ? `${origin.replace(/\/$/, "")}${sitemapPath}` : undefined);
15
+ if (sitemap)
16
+ lines.push("", `Sitemap: ${sitemap}`);
17
+ if (rules.host)
18
+ lines.push(`Host: ${rules.host}`);
19
+ return `${lines.join("\n")}\n`;
20
+ }
21
+ //# sourceMappingURL=robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAQA,wDAAwD;AACxD,MAAM,UAAU,eAAe,CAAC,UAA+B,EAAE;IAC/D,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;IACxE,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,4BAA4B,CAAC;IAE1D,MAAM,KAAK,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,MAAM,KAAK,GAAa,CAAC,eAAe,CAAC,CAAC;IAE1C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACrG,IAAI,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,OAAO,EAAE,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAElD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC"}
@@ -0,0 +1,20 @@
1
+ export interface SitemapEntry {
2
+ loc: string;
3
+ lastmod?: string;
4
+ changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
5
+ priority?: number;
6
+ alternates?: Array<{
7
+ hreflang: string;
8
+ href: string;
9
+ }>;
10
+ }
11
+ export interface CreateSitemapOptions {
12
+ origin: string;
13
+ paths: readonly string[];
14
+ lastmod?: string;
15
+ }
16
+ /** Build basic sitemap entries from pathnames. */
17
+ export declare function createSitemapEntries(options: CreateSitemapOptions): SitemapEntry[];
18
+ /** Render sitemap XML, optionally with xhtml:link alternates. */
19
+ export declare function renderSitemapXml(entries: SitemapEntry[]): string;
20
+ //# sourceMappingURL=sitemap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,kDAAkD;AAClD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG,YAAY,EAAE,CAMlF;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA2BhE"}
@@ -0,0 +1,40 @@
1
+ /** Build basic sitemap entries from pathnames. */
2
+ export function createSitemapEntries(options) {
3
+ const base = options.origin.replace(/\/$/, "");
4
+ return options.paths.map((pathname) => ({
5
+ loc: `${base}${pathname.startsWith("/") ? pathname : `/${pathname}`}`,
6
+ lastmod: options.lastmod,
7
+ }));
8
+ }
9
+ /** Render sitemap XML, optionally with xhtml:link alternates. */
10
+ export function renderSitemapXml(entries) {
11
+ const xmlns = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
12
+ const xhtml = entries.some((e) => e.alternates?.length) ? 'xmlns:xhtml="http://www.w3.org/1999/xhtml"' : "";
13
+ const urls = entries
14
+ .map((entry) => {
15
+ const altLinks = (entry.alternates ?? [])
16
+ .map((alt) => ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.href)}" />`)
17
+ .join("\n");
18
+ const lastmod = entry.lastmod ? `\n <lastmod>${escapeXml(entry.lastmod)}</lastmod>` : "";
19
+ const changefreq = entry.changefreq ? `\n <changefreq>${entry.changefreq}</changefreq>` : "";
20
+ const priority = entry.priority != null ? `\n <priority>${entry.priority.toFixed(1)}</priority>` : "";
21
+ return ` <url>
22
+ <loc>${escapeXml(entry.loc)}</loc>${lastmod}${changefreq}${priority}
23
+ ${altLinks}
24
+ </url>`;
25
+ })
26
+ .join("\n");
27
+ return `<?xml version="1.0" encoding="UTF-8"?>
28
+ <urlset ${xmlns}${xhtml ? ` ${xhtml}` : ""}>
29
+ ${urls}
30
+ </urlset>`;
31
+ }
32
+ function escapeXml(value) {
33
+ return value
34
+ .replaceAll("&", "&amp;")
35
+ .replaceAll("<", "&lt;")
36
+ .replaceAll(">", "&gt;")
37
+ .replaceAll('"', "&quot;")
38
+ .replaceAll("'", "&apos;");
39
+ }
40
+ //# sourceMappingURL=sitemap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AAcA,kDAAkD;AAClD,MAAM,UAAU,oBAAoB,CAAC,OAA6B;IAChE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACtC,GAAG,EAAE,GAAG,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,EAAE;QACrE,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,OAAuB;IACtD,MAAM,KAAK,GAAG,qDAAqD,CAAC;IACpE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,4CAA4C,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5G,MAAM,IAAI,GAAG,OAAO;SACjB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;aACtC,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CACN,6CAA6C,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAC3G;aACA,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,qBAAqB,KAAK,CAAC,UAAU,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QAChG,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QAEzG,OAAO;WACF,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,OAAO,GAAG,UAAU,GAAG,QAAQ;EACrE,QAAQ;SACD,CAAC;IACN,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO;UACC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE;EACxC,IAAI;UACI,CAAC;AACX,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK;SACT,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,123 @@
1
+ import type { LoaderResult, RouteParams } from "@kamod-ch/otok/server";
2
+ export type JsonLdValue = string | number | boolean | null | JsonLdValue[] | {
3
+ [key: string]: JsonLdValue;
4
+ };
5
+ export type OpenGraphType = "website" | "article" | "product" | "profile" | "book" | "music.song" | "music.album" | "music.playlist" | "music.radio_station" | "video.movie" | "video.episode" | "video.tv_show" | "video.other";
6
+ export interface OpenGraphMeta {
7
+ type?: OpenGraphType;
8
+ title?: string;
9
+ description?: string;
10
+ url?: string;
11
+ siteName?: string;
12
+ locale?: string;
13
+ image?: string | OpenGraphImage;
14
+ images?: Array<string | OpenGraphImage>;
15
+ }
16
+ export interface OpenGraphImage {
17
+ url: string;
18
+ width?: number;
19
+ height?: number;
20
+ alt?: string;
21
+ type?: string;
22
+ }
23
+ export interface TwitterCardMeta {
24
+ card?: "summary" | "summary_large_image" | "app" | "player";
25
+ site?: string;
26
+ creator?: string;
27
+ title?: string;
28
+ description?: string;
29
+ image?: string;
30
+ }
31
+ export interface RouteIcon {
32
+ rel?: "icon" | "apple-touch-icon" | "mask-icon";
33
+ href: string;
34
+ sizes?: string;
35
+ type?: string;
36
+ color?: string;
37
+ }
38
+ export interface RouteManifestRef {
39
+ href: string;
40
+ }
41
+ export interface HreflangAlternate {
42
+ hreflang: string;
43
+ href: string;
44
+ }
45
+ export interface RouteMeta {
46
+ title?: string;
47
+ description?: string;
48
+ lang?: string;
49
+ canonical?: string;
50
+ robots?: string;
51
+ openGraph?: OpenGraphMeta;
52
+ twitter?: TwitterCardMeta;
53
+ jsonLd?: Record<string, JsonLdValue> | Array<Record<string, JsonLdValue>>;
54
+ icons?: RouteIcon[];
55
+ manifest?: RouteManifestRef;
56
+ hreflang?: HreflangAlternate[];
57
+ rss?: string;
58
+ atom?: string;
59
+ meta?: Record<string, string>;
60
+ }
61
+ export interface MetaContext<Data extends LoaderResult = LoaderResult> {
62
+ data: Data;
63
+ params: RouteParams;
64
+ route: string;
65
+ locale?: string;
66
+ origin?: string;
67
+ }
68
+ export interface SeoPluginOptions {
69
+ /** Absolute site origin, e.g. `https://example.com`. Required for canonical URLs and sitemaps. */
70
+ origin: string;
71
+ /** Global title template. `%s` is replaced by the page title. */
72
+ titleTemplate?: string;
73
+ /** Default site name for Open Graph. */
74
+ siteName?: string;
75
+ /** Default Open Graph image URL. */
76
+ defaultOgImage?: string;
77
+ /** Default Twitter handle, e.g. `@example`. */
78
+ twitterSite?: string;
79
+ /** Paths included in the auto-generated sitemap. */
80
+ sitemapPaths?: string[];
81
+ /** Serve `/robots.txt`. Default: true. */
82
+ robots?: boolean | RobotsConfig;
83
+ /** Serve `/sitemap.xml`. Default: true when sitemapPaths is set. */
84
+ sitemap?: boolean;
85
+ /** RSS feed configuration. */
86
+ rss?: FeedConfig;
87
+ /** Atom feed configuration. */
88
+ atom?: FeedConfig;
89
+ /** Dynamic OG image hook — receives route context and returns an image URL. */
90
+ ogImage?: OgImageHook;
91
+ /** Icons and manifest injected on every page unless overridden. */
92
+ icons?: RouteIcon[];
93
+ manifest?: RouteManifestRef;
94
+ }
95
+ export interface RobotsConfig {
96
+ allow?: string[];
97
+ disallow?: string[];
98
+ sitemap?: string;
99
+ host?: string;
100
+ }
101
+ export interface FeedItem {
102
+ id: string;
103
+ title: string;
104
+ link: string;
105
+ description?: string;
106
+ publishedAt?: string;
107
+ updatedAt?: string;
108
+ author?: string;
109
+ }
110
+ export interface FeedConfig {
111
+ path: string;
112
+ title: string;
113
+ description?: string;
114
+ items: FeedItem[] | (() => FeedItem[] | Promise<FeedItem[]>);
115
+ }
116
+ export type OgImageHook = (ctx: OgImageContext) => string | Promise<string>;
117
+ export interface OgImageContext {
118
+ route: string;
119
+ params: RouteParams;
120
+ locale?: string;
121
+ title?: string;
122
+ }
123
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@kamod-ch/otok-seo",
3
+ "version": "2.0.0",
4
+ "description": "Typed route metadata, sitemaps, feeds, and SEO helpers for Otok apps.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/kamod-ch/otok/tree/main/packages/otok-seo#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/kamod-ch/otok/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/kamod-ch/otok.git",
13
+ "directory": "packages/otok-seo"
14
+ },
15
+ "type": "module",
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./sitemap": {
26
+ "types": "./dist/sitemap.d.ts",
27
+ "default": "./dist/sitemap.js"
28
+ },
29
+ "./feeds": {
30
+ "types": "./dist/feeds.d.ts",
31
+ "default": "./dist/feeds.js"
32
+ },
33
+ "./robots": {
34
+ "types": "./dist/robots.d.ts",
35
+ "default": "./dist/robots.js"
36
+ },
37
+ "./loader": {
38
+ "types": "./dist/loader.d.ts",
39
+ "default": "./dist/loader.js"
40
+ }
41
+ },
42
+ "peerDependencies": {
43
+ "@kamod-ch/otok": ">=0.6.1",
44
+ "hono": ">=4"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@kamod-ch/otok-i18n": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.12.2",
53
+ "hono": "^4.12.25",
54
+ "preact": "^10.28.2",
55
+ "typescript": "~6.0.2",
56
+ "vitest": "^4.1.2",
57
+ "@kamod-ch/otok": "0.6.1",
58
+ "@kamod-ch/otok-i18n": "3.0.0",
59
+ "@kamod-ch/otok-test": "0.5.1"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "scripts": {
68
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
69
+ "typecheck": "tsc -p tsconfig.json --noEmit",
70
+ "test": "vitest run"
71
+ }
72
+ }