@hkdigital/lib-core 0.5.12 → 0.5.14

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.
@@ -0,0 +1,386 @@
1
+ # Meta Utilities
2
+
3
+ SEO and meta tag utilities for robots.txt and sitemap.xml generation.
4
+
5
+ ## Overview
6
+
7
+ This module provides functions for generating robots.txt and sitemap.xml
8
+ files dynamically based on configuration. The utilities are used by the
9
+ `routes/(meta)` endpoints but can also be used independently in your own
10
+ server routes.
11
+
12
+ ## Modules
13
+
14
+ ### Robots
15
+
16
+ Generate robots.txt content with host filtering and sitemap references.
17
+
18
+ ```javascript
19
+ import { generateRobotsTxt, isHostAllowed } from '$lib/meta/robots.js';
20
+ ```
21
+
22
+ ### Sitemap
23
+
24
+ Generate sitemap.xml content from route configurations.
25
+
26
+ ```javascript
27
+ import { generateSitemap } from '$lib/meta/sitemap.js';
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Robots.txt Generation
33
+
34
+ The `generateRobotsTxt()` function creates robots.txt content based on the
35
+ request URL and configuration.
36
+
37
+ **Basic usage:**
38
+
39
+ ```javascript
40
+ import { generateRobotsTxt } from '$lib/meta/robots.js';
41
+
42
+ export const GET = async ({ url }) => {
43
+ const config = {
44
+ allowedHosts: ['example.com', 'www.example.com'],
45
+ disallowedPaths: ['/admin', '/api']
46
+ };
47
+
48
+ const robotsTxt = generateRobotsTxt(url, config);
49
+ return new Response(robotsTxt, {
50
+ headers: { 'Content-Type': 'text/plain' }
51
+ });
52
+ };
53
+ ```
54
+
55
+ **Configuration options:**
56
+
57
+ ```javascript
58
+ /**
59
+ * @typedef {Object} RobotsConfig
60
+ * @property {string[] | '*'} [allowedHosts]
61
+ * Allowed host patterns. Use '*' or omit to allow all hosts.
62
+ * Supports wildcards (e.g., '*.example.com')
63
+ * @property {string[]} [disallowedPaths]
64
+ * Paths to block from indexing (e.g., '/admin', '/api/*')
65
+ */
66
+ ```
67
+
68
+ **Host filtering:**
69
+
70
+ ```javascript
71
+ // Allow only production domain
72
+ const config = {
73
+ allowedHosts: ['example.com']
74
+ };
75
+
76
+ // example.com → User-agent: *\nAllow: /\nSitemap: ...
77
+ // test.example.com → User-agent: *\nDisallow: /
78
+ ```
79
+
80
+ **Wildcard patterns:**
81
+
82
+ ```javascript
83
+ // Allow all subdomains
84
+ const config = {
85
+ allowedHosts: ['example.com', '*.example.com']
86
+ };
87
+
88
+ // example.com → allowed
89
+ // test.example.com → allowed
90
+ // staging.example.com → allowed
91
+ ```
92
+
93
+ **Path blocking:**
94
+
95
+ ```javascript
96
+ // Block specific paths
97
+ const config = {
98
+ allowedHosts: ['example.com'],
99
+ disallowedPaths: ['/admin', '/api', '/private/*']
100
+ };
101
+
102
+ // Generates:
103
+ // User-agent: *
104
+ // Allow: /
105
+ // Disallow: /admin
106
+ // Disallow: /api
107
+ // Disallow: /private/*
108
+ // Sitemap: https://example.com/sitemap.xml
109
+ ```
110
+
111
+ **Sitemap reference:**
112
+
113
+ Sitemap reference is always included for allowed hosts:
114
+
115
+ ```
116
+ User-agent: *
117
+ Allow: /
118
+ Sitemap: https://example.com/sitemap.xml
119
+ ```
120
+
121
+ ### Sitemap.xml Generation
122
+
123
+ The `generateSitemap()` function creates sitemap.xml content from route
124
+ configurations.
125
+
126
+ **Basic usage:**
127
+
128
+ ```javascript
129
+ import { generateSitemap } from '$lib/meta/sitemap.js';
130
+
131
+ export const GET = async ({ url }) => {
132
+ const routes = ['/', '/about', '/contact'];
133
+
134
+ const sitemap = generateSitemap(url.origin, routes);
135
+ return new Response(sitemap, {
136
+ headers: { 'Content-Type': 'application/xml' }
137
+ });
138
+ };
139
+ ```
140
+
141
+ **Route configuration:**
142
+
143
+ ```javascript
144
+ /**
145
+ * @typedef {string | SitemapRouteObject} SitemapRoute
146
+ * Route can be a simple string path or an object with details
147
+ */
148
+
149
+ /**
150
+ * @typedef {Object} SitemapRouteObject
151
+ * @property {string} path - Route path (e.g., '/about')
152
+ * @property {number} [priority] - Priority (0.0 to 1.0)
153
+ * @property {'always'|'hourly'|'daily'|'weekly'|'monthly'|'yearly'|'never'}
154
+ * [changefreq] - Change frequency
155
+ */
156
+ ```
157
+
158
+ **Simple format (recommended):**
159
+
160
+ ```javascript
161
+ const routes = [
162
+ '/', // priority: 1.0, changefreq: 'daily'
163
+ '/about', // priority: 0.8, changefreq: 'weekly'
164
+ '/contact' // priority: 0.8, changefreq: 'weekly'
165
+ ];
166
+ ```
167
+
168
+ **Advanced format with custom settings:**
169
+
170
+ ```javascript
171
+ const routes = [
172
+ '/',
173
+ '/about',
174
+ { path: '/blog', priority: 0.9, changefreq: 'daily' },
175
+ { path: '/legal', priority: 0.3, changefreq: 'yearly' }
176
+ ];
177
+ ```
178
+
179
+ **Mixed format:**
180
+
181
+ ```javascript
182
+ const routes = [
183
+ '/',
184
+ '/about',
185
+ { path: '/blog', priority: 0.9, changefreq: 'daily' },
186
+ '/contact'
187
+ ];
188
+ ```
189
+
190
+ **Default values:**
191
+
192
+ - Root path (`/`): priority `1.0`, changefreq `daily`
193
+ - Other paths: priority `0.8`, changefreq `weekly`
194
+ - Root path is always included (added automatically if missing)
195
+
196
+ ## Helper Functions
197
+
198
+ ### isHostAllowed()
199
+
200
+ Check if a hostname matches the allowed hosts configuration.
201
+
202
+ ```javascript
203
+ import { isHostAllowed } from '$lib/meta/robots.js';
204
+
205
+ // Exact match
206
+ isHostAllowed('example.com', ['example.com']); // true
207
+ isHostAllowed('test.example.com', ['example.com']); // false
208
+
209
+ // Wildcard match
210
+ isHostAllowed('test.example.com', ['*.example.com']); // true
211
+ isHostAllowed('example.com', ['*.example.com']); // false
212
+
213
+ // Allow all
214
+ isHostAllowed('anything.com', '*'); // true
215
+ isHostAllowed('anything.com', undefined); // true
216
+
217
+ // Multiple patterns
218
+ isHostAllowed('example.com', ['example.com', '*.staging.com']); // true
219
+ isHostAllowed('app.staging.com', ['example.com', '*.staging.com']); // true
220
+ ```
221
+
222
+ **Case insensitive:**
223
+
224
+ ```javascript
225
+ isHostAllowed('Example.COM', ['example.com']); // true
226
+ ```
227
+
228
+ **String or array:**
229
+
230
+ ```javascript
231
+ // Single string
232
+ isHostAllowed('example.com', 'example.com'); // true
233
+
234
+ // Array
235
+ isHostAllowed('example.com', ['example.com']); // true
236
+ ```
237
+
238
+ ## Real-World Examples
239
+
240
+ ### Production-only indexing
241
+
242
+ ```javascript
243
+ // Only allow production domain to be indexed
244
+ const robotsConfig = {
245
+ allowedHosts: ['mysite.com', 'www.mysite.com'],
246
+ disallowedPaths: ['/admin', '/api']
247
+ };
248
+
249
+ // Production: mysite.com → Allow + Sitemap
250
+ // Staging: staging.mysite.com → Disallow
251
+ // Development: localhost → Disallow
252
+ ```
253
+
254
+ ### Staging and production indexing
255
+
256
+ ```javascript
257
+ // Allow both production and staging subdomains
258
+ const robotsConfig = {
259
+ allowedHosts: ['mysite.com', '*.mysite.com'],
260
+ disallowedPaths: ['/admin', '/api']
261
+ };
262
+
263
+ // Production: mysite.com → Allow + Sitemap
264
+ // Staging: staging.mysite.com → Allow + Sitemap
265
+ // Development: localhost → Disallow
266
+ ```
267
+
268
+ ### Allow all hosts (development)
269
+
270
+ ```javascript
271
+ // Allow all hosts - useful during development
272
+ const robotsConfig = {
273
+ allowedHosts: '*',
274
+ disallowedPaths: ['/admin']
275
+ };
276
+
277
+ // All hosts → Allow + Sitemap
278
+ ```
279
+
280
+ ### Complex sitemap configuration
281
+
282
+ ```javascript
283
+ const routes = [
284
+ '/',
285
+ '/about',
286
+ '/contact',
287
+
288
+ // Blog updated frequently
289
+ { path: '/blog', priority: 0.9, changefreq: 'daily' },
290
+
291
+ // Legal pages rarely change
292
+ { path: '/privacy', priority: 0.3, changefreq: 'yearly' },
293
+ { path: '/terms', priority: 0.3, changefreq: 'yearly' },
294
+
295
+ // Documentation moderately important
296
+ { path: '/docs', priority: 0.7, changefreq: 'monthly' }
297
+ ];
298
+ ```
299
+
300
+ ## Integration with Routes
301
+
302
+ These utilities are used by the `routes/(meta)` endpoints:
303
+
304
+ ### robots.txt endpoint
305
+
306
+ ```javascript
307
+ // routes/(meta)/robots.txt/+server.js
308
+ import { text } from '@sveltejs/kit';
309
+ import { generateRobotsTxt } from '$lib/meta/robots.js';
310
+ import { robotsConfig } from '../config.js';
311
+
312
+ export const GET = async ({ url }) => {
313
+ const robotsTxt = generateRobotsTxt(url, robotsConfig);
314
+ return text(robotsTxt);
315
+ };
316
+ ```
317
+
318
+ ### sitemap.xml endpoint
319
+
320
+ ```javascript
321
+ // routes/(meta)/sitemap.xml/+server.js
322
+ import { generateSitemap } from '$lib/meta/sitemap.js';
323
+ import { siteRoutes } from '../config.js';
324
+
325
+ export const GET = async ({ url }) => {
326
+ const sitemap = generateSitemap(url.origin, siteRoutes);
327
+
328
+ return new Response(sitemap, {
329
+ headers: {
330
+ 'Content-Type': 'application/xml',
331
+ 'Cache-Control': 'max-age=0, s-maxage=3600'
332
+ }
333
+ });
334
+ };
335
+ ```
336
+
337
+ ## Reverse Proxy Configuration
338
+
339
+ If your app is deployed behind a reverse proxy (nginx, Cloudflare, etc.),
340
+ ensure your SvelteKit adapter is configured to trust proxy headers for
341
+ correct origin detection:
342
+
343
+ ```javascript
344
+ // svelte.config.js
345
+ import adapter from '@sveltejs/adapter-node';
346
+
347
+ export default {
348
+ kit: {
349
+ adapter: adapter({
350
+ // Trust X-Forwarded-* headers from proxy
351
+ trustProxy: true
352
+ })
353
+ }
354
+ };
355
+ ```
356
+
357
+ Without this, `url.origin` may be `http://localhost` instead of your actual
358
+ domain, and the sitemap directive will point to the wrong URL.
359
+
360
+ ## Testing
361
+
362
+ Unit tests are included for all functions:
363
+
364
+ ```bash
365
+ # Run meta utility tests
366
+ pnpm test:file src/lib/meta/
367
+
368
+ # Test coverage includes:
369
+ # - Host pattern matching (exact, wildcard, multiple)
370
+ # - Robots.txt generation (allowed/blocked hosts, paths, sitemap)
371
+ # - Sitemap generation (simple/advanced routes, defaults, mixed formats)
372
+ ```
373
+
374
+ ## Type Definitions
375
+
376
+ TypeScript-style JSDoc type definitions are available:
377
+
378
+ ```javascript
379
+ // robots.js
380
+ import './robots/typedef.js'; // RobotsConfig
381
+
382
+ // sitemap.js
383
+ import './sitemap/typedef.js'; // SitemapRoute, SitemapRouteObject
384
+ ```
385
+
386
+ See `typedef.js` files in each subdirectory for complete type definitions.
@@ -0,0 +1,37 @@
1
+ /** @typedef {import('./typedef.js').RobotsConfig} RobotsConfig */
2
+ /**
3
+ * Check if hostname matches allowed hosts pattern
4
+ *
5
+ * @param {string} hostname - Hostname to check (e.g., test.mysite.com)
6
+ * @param {string[] | '*' | undefined} allowedHosts - Allowed host patterns
7
+ *
8
+ * @returns {boolean} True if host is allowed
9
+ */
10
+ export function isHostAllowed(hostname: string, allowedHosts: string[] | "*" | undefined): boolean;
11
+ /**
12
+ * Generate robots.txt with sitemap reference
13
+ *
14
+ * NOTE: If deployed behind a reverse proxy (nginx, Cloudflare, etc.),
15
+ * ensure your adapter is configured to trust proxy headers for correct
16
+ * origin detection:
17
+ *
18
+ * // svelte.config.js
19
+ * export default {
20
+ * kit: {
21
+ * adapter: adapter({
22
+ * // Trust X-Forwarded-* headers from proxy
23
+ * trustProxy: true
24
+ * })
25
+ * }
26
+ * };
27
+ *
28
+ * Without this, url.origin may be http://localhost instead of your
29
+ * actual domain, and the sitemap directive will be omitted.
30
+ *
31
+ * @param {URL} url - Request URL object
32
+ * @param {RobotsConfig} [config] - Robots configuration object
33
+ *
34
+ * @returns {string} robots.txt content
35
+ */
36
+ export function generateRobotsTxt(url: URL, config?: RobotsConfig): string;
37
+ export type RobotsConfig = import("./typedef.js").RobotsConfig;
@@ -0,0 +1,83 @@
1
+ /** @typedef {import('./typedef.js').RobotsConfig} RobotsConfig */
2
+
3
+ /**
4
+ * Check if hostname matches allowed hosts pattern
5
+ *
6
+ * @param {string} hostname - Hostname to check (e.g., test.mysite.com)
7
+ * @param {string[] | '*' | undefined} allowedHosts - Allowed host patterns
8
+ *
9
+ * @returns {boolean} True if host is allowed
10
+ */
11
+ export function isHostAllowed(hostname, allowedHosts) {
12
+ // If not configured or set to '*', allow all hosts
13
+ if (!allowedHosts || allowedHosts === '*') {
14
+ return true;
15
+ }
16
+
17
+ if (typeof allowedHosts === 'string') {
18
+ allowedHosts = [allowedHosts];
19
+ }
20
+
21
+ // Check if hostname matches any allowed pattern
22
+ return allowedHosts.some((pattern) => {
23
+ // Convert wildcard pattern to regex
24
+ // Example: *.mysite.com -> ^.*\.mysite\.com$
25
+ const regexPattern = pattern
26
+ .replace(/\./g, '\\.') // Escape dots
27
+ .replace(/\*/g, '.*'); // * becomes .*
28
+
29
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
30
+ return regex.test(hostname);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Generate robots.txt with sitemap reference
36
+ *
37
+ * NOTE: If deployed behind a reverse proxy (nginx, Cloudflare, etc.),
38
+ * ensure your adapter is configured to trust proxy headers for correct
39
+ * origin detection:
40
+ *
41
+ * // svelte.config.js
42
+ * export default {
43
+ * kit: {
44
+ * adapter: adapter({
45
+ * // Trust X-Forwarded-* headers from proxy
46
+ * trustProxy: true
47
+ * })
48
+ * }
49
+ * };
50
+ *
51
+ * Without this, url.origin may be http://localhost instead of your
52
+ * actual domain, and the sitemap directive will be omitted.
53
+ *
54
+ * @param {URL} url - Request URL object
55
+ * @param {RobotsConfig} [config] - Robots configuration object
56
+ *
57
+ * @returns {string} robots.txt content
58
+ */
59
+ export function generateRobotsTxt(url, config = {}) {
60
+ const hostAllowed = isHostAllowed(url.hostname, config.allowedHosts);
61
+
62
+ // Block entire site if host is not allowed
63
+ if (!hostAllowed) {
64
+ return 'User-agent: *\nDisallow: /';
65
+ }
66
+
67
+ // Allow site, but add specific path blocks
68
+ let content = 'User-agent: *\nAllow: /';
69
+
70
+ // Add disallowed paths
71
+ if (config.disallowedPaths && config.disallowedPaths.length > 0) {
72
+ config.disallowedPaths.forEach((path) => {
73
+ content += `\nDisallow: ${path}`;
74
+ });
75
+ }
76
+
77
+ // Always add sitemap reference
78
+ if (url.origin) {
79
+ content += `\nSitemap: ${url.origin}/sitemap.xml`;
80
+ }
81
+
82
+ return content;
83
+ }
@@ -0,0 +1,13 @@
1
+ declare const _default: {};
2
+ export default _default;
3
+ export type RobotsConfig = {
4
+ /**
5
+ * Allowed host patterns. Use '*' or omit to allow all hosts.
6
+ * Supports wildcards (e.g., '*.example.com')
7
+ */
8
+ allowedHosts?: string[] | "*" | undefined;
9
+ /**
10
+ * Paths to block from indexing (e.g., '/admin', '/api/*')
11
+ */
12
+ disallowedPaths?: string[] | undefined;
13
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @typedef {Object} RobotsConfig
3
+ * @property {string[] | '*'} [allowedHosts]
4
+ * Allowed host patterns. Use '*' or omit to allow all hosts.
5
+ * Supports wildcards (e.g., '*.example.com')
6
+ * @property {string[]} [disallowedPaths]
7
+ * Paths to block from indexing (e.g., '/admin', '/api/*')
8
+ */
9
+
10
+ export default {};
@@ -0,0 +1 @@
1
+ export { generateRobotsTxt, isHostAllowed } from "./robots/index.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public exports for robots.txt utilities
3
+ */
4
+
5
+ export { generateRobotsTxt, isHostAllowed } from './robots/index.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Generate sitemap XML
3
+ *
4
+ * @param {string} origin - Base URL (e.g., https://example.com)
5
+ * @param {SitemapRoute[]} [routes=[]] - Array of routes
6
+ *
7
+ * @returns {string} XML sitemap
8
+ */
9
+ export function generateSitemap(origin: string, routes?: SitemapRoute[]): string;
10
+ export type SitemapRoute = import("./typedef.js").SitemapRoute;
11
+ export type SitemapRouteObject = import("./typedef.js").SitemapRouteObject;
@@ -0,0 +1,63 @@
1
+ // @see https://www.sitemaps.org/protocol.html
2
+
3
+ /** @typedef {import('./typedef.js').SitemapRoute} SitemapRoute */
4
+ /** @typedef {import('./typedef.js').SitemapRouteObject} SitemapRouteObject */
5
+
6
+ /**
7
+ * Normalize route to full route object with defaults
8
+ *
9
+ * @param {import('./typedef.js').SitemapRoute} route - Route path string or route object
10
+ *
11
+ * @returns {SitemapRouteObject} Normalized route object
12
+ */
13
+ function normalizeRoute(route) {
14
+ // Handle simple string format
15
+ if (typeof route === 'string') {
16
+ return {
17
+ path: route,
18
+ priority: route === '/' ? 1.0 : 0.8,
19
+ changefreq: route === '/' ? 'daily' : 'weekly'
20
+ };
21
+ }
22
+
23
+ // Handle object format with defaults
24
+ return {
25
+ priority: 0.8,
26
+ changefreq: 'weekly',
27
+ ...route
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Generate sitemap XML
33
+ *
34
+ * @param {string} origin - Base URL (e.g., https://example.com)
35
+ * @param {SitemapRoute[]} [routes=[]] - Array of routes
36
+ *
37
+ * @returns {string} XML sitemap
38
+ */
39
+ export function generateSitemap(origin, routes = []) {
40
+ // Ensure root path is always included (failsafe)
41
+ const hasRoot = routes.some((route) => {
42
+ const path = typeof route === 'string' ? route : route.path;
43
+ return path === '/';
44
+ });
45
+
46
+ const normalizedRoutes = hasRoot ? routes : ['/', ...routes];
47
+
48
+ const urls = normalizedRoutes
49
+ .map(normalizeRoute)
50
+ .map(
51
+ (route) => `
52
+ <url>
53
+ <loc>${origin}${route.path}</loc>
54
+ <changefreq>${route.changefreq}</changefreq>
55
+ <priority>${route.priority}</priority>
56
+ </url>`
57
+ )
58
+ .join('');
59
+
60
+ return `<?xml version="1.0" encoding="UTF-8"?>
61
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
62
+ </urlset>`;
63
+ }
@@ -0,0 +1,20 @@
1
+ declare const _default: {};
2
+ export default _default;
3
+ export type SitemapRouteObject = {
4
+ /**
5
+ * - Route path (e.g., '/about')
6
+ */
7
+ path: string;
8
+ /**
9
+ * - Priority (0.0 to 1.0)
10
+ */
11
+ priority?: number | undefined;
12
+ /**
13
+ * - Change frequency
14
+ */
15
+ changefreq?: "hourly" | "daily" | "weekly" | "always" | "monthly" | "yearly" | "never" | undefined;
16
+ };
17
+ /**
18
+ * Route can be a simple string path or an object with details
19
+ */
20
+ export type SitemapRoute = string | SitemapRouteObject;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @typedef {Object} SitemapRouteObject
3
+ * @property {string} path - Route path (e.g., '/about')
4
+ * @property {number} [priority] - Priority (0.0 to 1.0)
5
+ * @property {'always'|'hourly'|'daily'|'weekly'|'monthly'|'yearly'|'never'}
6
+ * [changefreq] - Change frequency
7
+ */
8
+
9
+ /**
10
+ * @typedef {string | SitemapRouteObject} SitemapRoute
11
+ * Route can be a simple string path or an object with details
12
+ */
13
+
14
+ export default {};
@@ -0,0 +1 @@
1
+ export { generateSitemap } from "./sitemap/index.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public exports for sitemap utilities
3
+ */
4
+
5
+ export { generateSitemap } from './sitemap/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"