@harpy-js/core 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { Logger } from "./logger";
5
+
6
+ const logger = new Logger("Builder");
7
+
8
+ async function runCommand(cmd: string, args: string[] = []): Promise<void> {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn(cmd, args, { stdio: "inherit", shell: true });
11
+ proc.on("close", (code) => {
12
+ if (code !== 0) {
13
+ reject(new Error(`Command failed with code ${code}`));
14
+ } else {
15
+ resolve();
16
+ }
17
+ });
18
+ });
19
+ }
20
+
21
+ async function buildNestApp(): Promise<void> {
22
+ logger.log("Building NestJS application...");
23
+ try {
24
+ await runCommand("nest", ["build"]);
25
+ } catch (error) {
26
+ logger.error(`NestJS build failed: ${error}`);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ async function buildHydration(): Promise<void> {
32
+ logger.log("Building hydration components...");
33
+ try {
34
+ const tsxPath = require("child_process")
35
+ .execSync("which tsx", { encoding: "utf-8" })
36
+ .trim();
37
+ await runCommand(tsxPath, [
38
+ require("path").join(__dirname, "build-hydration.ts"),
39
+ ]);
40
+ } catch (error) {
41
+ logger.error(`Hydration build failed: ${error}`);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function autoWrap(): Promise<void> {
47
+ logger.log("Auto-wrapping client components...");
48
+ try {
49
+ const tsxPath = require("child_process")
50
+ .execSync("which tsx", { encoding: "utf-8" })
51
+ .trim();
52
+ await runCommand(tsxPath, [
53
+ require("path").join(__dirname, "auto-wrap-exports.ts"),
54
+ ]);
55
+ } catch (error) {
56
+ logger.error(`Auto-wrap failed: ${error}`);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ async function buildStyles(): Promise<void> {
62
+ logger.log("Building styles...");
63
+ try {
64
+ const tsxPath = require("child_process")
65
+ .execSync("which tsx", { encoding: "utf-8" })
66
+ .trim();
67
+ await runCommand(tsxPath, [
68
+ require("path").join(__dirname, "build-page-styles.ts"),
69
+ ]);
70
+ } catch (error) {
71
+ logger.error(`Styles build failed: ${error}`);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function main(): Promise<void> {
77
+ try {
78
+ logger.log("Starting production build...");
79
+
80
+ await buildNestApp();
81
+ await buildHydration();
82
+ await autoWrap();
83
+ await buildStyles();
84
+
85
+ logger.log("Production build complete!");
86
+ process.exit(0);
87
+ } catch (error) {
88
+ logger.error(`Build failed: ${error}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ main();
package/scripts/dev.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  import { spawn } from "child_process";
4
4
  import * as fs from "fs";
5
5
  import * as http from "http";
6
+ import { Logger } from "./logger";
7
+
8
+ const logger = new Logger("DevServer");
6
9
 
7
10
  let nestProcess: any = null;
8
11
  let isRebuilding = false;
@@ -43,43 +46,52 @@ async function runCommand(cmd: string, args: string[] = []): Promise<void> {
43
46
  }
44
47
 
45
48
  async function buildHydration(): Promise<void> {
46
- console.log("🔧 Building hydration...");
49
+ logger.log("Building hydration components...");
47
50
  try {
48
- await runCommand("pnpm", ["build:hydration"]);
49
- console.log(" Hydration built");
51
+ const tsxPath = require("child_process")
52
+ .execSync("which tsx", { encoding: "utf-8" })
53
+ .trim();
54
+ await runCommand(tsxPath, [
55
+ require("path").join(__dirname, "build-hydration.ts"),
56
+ ]);
50
57
  } catch (error) {
51
- console.error("❌ Hydration build failed:", error);
58
+ logger.error(`Hydration build failed: ${error}`);
52
59
  throw error;
53
60
  }
54
61
  }
55
62
 
56
63
  async function autoWrap(): Promise<void> {
57
- console.log("🔄 Auto-wrapping client components...");
64
+ logger.log("Auto-wrapping client components...");
58
65
  try {
59
- await runCommand("pnpm", ["auto-wrap"]);
60
- console.log(" Auto-wrap complete");
66
+ const tsxPath = require("child_process")
67
+ .execSync("which tsx", { encoding: "utf-8" })
68
+ .trim();
69
+ await runCommand(tsxPath, [
70
+ require("path").join(__dirname, "auto-wrap-exports.ts"),
71
+ ]);
61
72
  } catch (error) {
62
- console.error("❌ Auto-wrap failed:", error);
63
- throw error;
73
+ logger.error(`Auto-wrap failed: ${error}`);
64
74
  }
65
75
  }
66
76
 
67
77
  async function buildStyles(): Promise<void> {
68
- console.log("🎨 Building styles...");
78
+ logger.log("Building styles...");
69
79
  try {
70
- await runCommand("pnpm", ["build:styles"]);
71
- console.log(" Styles built");
80
+ const tsxPath = require("child_process")
81
+ .execSync("which tsx", { encoding: "utf-8" })
82
+ .trim();
83
+ await runCommand(tsxPath, [
84
+ require("path").join(__dirname, "build-page-styles.ts"),
85
+ ]);
72
86
  } catch (error) {
73
- console.error("❌ Styles build failed:", error);
74
- throw error;
87
+ logger.error(`Styles build failed: ${error}`);
75
88
  }
76
89
  }
77
90
 
78
91
  async function startNestServer(): Promise<void> {
79
92
  return new Promise((resolve) => {
80
- console.log("🚀 Starting NestJS server from compiled dist...");
93
+ logger.log("Starting NestJS application...");
81
94
  // Run compiled dist/main.js instead of using ts-node
82
- // This ensures auto-wrapped components are used
83
95
  nestProcess = spawn("node", ["--watch", "dist/main.js"], {
84
96
  stdio: "pipe",
85
97
  shell: false,
@@ -106,15 +118,14 @@ async function startNestServer(): Promise<void> {
106
118
  setTimeout(async () => {
107
119
  if (isRebuilding) return;
108
120
  isRebuilding = true;
109
- console.log("\n🔄 NestJS rebuild detected, rebuilding assets...");
121
+ logger.log("Rebuilding assets after code change...");
110
122
  try {
111
123
  await buildHydration();
112
124
  await autoWrap();
113
125
  await buildStyles();
114
- console.log("✅ Assets rebuilt\n");
115
126
  triggerBrowserReload();
116
127
  } catch (error) {
117
- console.error("❌ Asset rebuild failed:", error);
128
+ logger.error(`Asset rebuild failed: ${error}`);
118
129
  } finally {
119
130
  isRebuilding = false;
120
131
  }
@@ -139,10 +150,9 @@ async function startNestServer(): Promise<void> {
139
150
  }
140
151
 
141
152
  function watchSourceChanges(): void {
142
- console.log("👀 Watching source files for changes...");
153
+ logger.log("Watching source files for changes...");
143
154
 
144
155
  let debounceTimer: NodeJS.Timeout | null = null;
145
- const watchedFiles = new Set<string>();
146
156
 
147
157
  // Watch src for CSS changes only (TS/TSX changes are handled by NestJS watch + stdout detection)
148
158
  fs.watch("src", { recursive: true }, async (eventType, filename) => {
@@ -168,14 +178,13 @@ function watchSourceChanges(): void {
168
178
 
169
179
  debounceTimer = setTimeout(async () => {
170
180
  isRebuilding = true;
171
- console.log(`\n📝 CSS file changed: ${filename}`);
181
+ logger.log(`CSS file changed: ${filename}`);
172
182
 
173
183
  try {
174
184
  await buildStyles();
175
- console.log("✅ Styles rebuilt\n");
176
185
  triggerBrowserReload();
177
186
  } catch (error) {
178
- console.error("Build error:", error);
187
+ logger.error(`Style rebuild failed: ${error}`);
179
188
  } finally {
180
189
  watchedFiles.delete(filename);
181
190
  isRebuilding = false;
@@ -188,7 +197,7 @@ let tscProcess: any = null;
188
197
 
189
198
  async function startTypeScriptWatch(): Promise<void> {
190
199
  return new Promise((resolve) => {
191
- console.log("⚙️ Starting TypeScript compiler in watch mode...");
200
+ logger.log("Starting TypeScript compiler in watch mode...");
192
201
  tscProcess = spawn("pnpm", ["nest", "build", "--watch"], {
193
202
  stdio: "pipe",
194
203
  shell: true,
@@ -215,13 +224,13 @@ async function startTypeScriptWatch(): Promise<void> {
215
224
 
216
225
  async function main(): Promise<void> {
217
226
  try {
218
- console.log("📦 Initializing development environment...\n");
227
+ logger.log("Initializing development environment...");
219
228
 
220
229
  // First: Start TypeScript compiler in watch mode
221
230
  await startTypeScriptWatch();
222
231
 
223
232
  // Build initial assets after first compilation
224
- console.log("\n🔧 Building hydration assets...");
233
+ logger.log("Building initial assets...");
225
234
  await buildHydration();
226
235
  await autoWrap();
227
236
  await buildStyles();
@@ -229,27 +238,27 @@ async function main(): Promise<void> {
229
238
  // Now start the node server with compiled dist files
230
239
  await startNestServer();
231
240
 
232
- console.log("\n✅ Development server ready!\n");
241
+ logger.log("Development server ready!");
233
242
 
234
243
  // Watch for source changes
235
244
  watchSourceChanges();
236
245
 
237
246
  // Handle graceful shutdown
238
247
  process.on("SIGINT", async () => {
239
- console.log("\n\n🛑 Stopping development server...");
248
+ logger.log("Stopping development server...");
240
249
  if (tscProcess) tscProcess.kill();
241
250
  if (nestProcess) nestProcess.kill();
242
251
  process.exit(0);
243
252
  });
244
253
 
245
254
  process.on("SIGTERM", async () => {
246
- console.log("\n\n🛑 Stopping development server...");
255
+ logger.log("Stopping development server...");
247
256
  if (tscProcess) tscProcess.kill();
248
257
  if (nestProcess) nestProcess.kill();
249
258
  process.exit(0);
250
259
  });
251
260
  } catch (error) {
252
- console.error("Fatal error:", error);
261
+ logger.error(`Fatal error: ${error}`);
253
262
  process.exit(1);
254
263
  }
255
264
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Logger utility to format logs matching NestJS style
3
+ * Format: [Harpy] PID - DATE, TIME LEVEL [Context] Message +Xms
4
+ */
5
+
6
+ const pid = process.pid;
7
+
8
+ function getTimestamp(): string {
9
+ const now = new Date();
10
+ const date = now.toLocaleDateString("en-US");
11
+ const time = now.toLocaleTimeString("en-US");
12
+ return `${date}, ${time}`;
13
+ }
14
+
15
+ export class Logger {
16
+ private context: string;
17
+ private lastLogTime: number = Date.now();
18
+
19
+ constructor(context: string) {
20
+ this.context = context;
21
+ }
22
+
23
+ private formatMessage(level: string, message: string, levelColor: string): string {
24
+ const timestamp = getTimestamp();
25
+ const elapsed = Date.now() - this.lastLogTime;
26
+ this.lastLogTime = Date.now();
27
+
28
+ const harpyPid = `\x1b[32m[Harpy] ${pid}\x1b[0m`; // green
29
+ const timestampFormatted = `\x1b[37m${timestamp}\x1b[0m`; // white
30
+ const levelColored = `\x1b[${levelColor}m${level.padEnd(5)}\x1b[0m`;
31
+ const contextColored = `\x1b[33m[${this.context}]\x1b[0m`; // yellow
32
+ const messageColored = `\x1b[${levelColor}m${message}\x1b[0m`;
33
+ const elapsedColored = `\x1b[33m+${elapsed}ms\x1b[0m`; // yellow
34
+
35
+ return `${harpyPid} - ${timestampFormatted} ${levelColored} ${contextColored} ${messageColored} ${elapsedColored}`;
36
+ }
37
+
38
+ log(message: string): void {
39
+ console.log(this.formatMessage("LOG", message, "32")); // green
40
+ }
41
+
42
+ error(message: string): void {
43
+ console.error(this.formatMessage("ERROR", message, "31")); // red
44
+ }
45
+
46
+ warn(message: string): void {
47
+ console.warn(this.formatMessage("WARN", message, "33")); // yellow
48
+ }
49
+
50
+ verbose(message: string): void {
51
+ console.log(this.formatMessage("LOG", message, "36")); // cyan
52
+ }
53
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { Logger } from "./logger";
5
+
6
+ const logger = new Logger("Server");
7
+
8
+ function startServer(): void {
9
+ logger.log("Starting production server...");
10
+
11
+ const serverProcess = spawn("node", ["dist/main.js"], {
12
+ stdio: "inherit",
13
+ shell: false,
14
+ cwd: process.cwd(),
15
+ });
16
+
17
+ serverProcess.on("error", (error) => {
18
+ logger.error(`Failed to start server: ${error.message}`);
19
+ process.exit(1);
20
+ });
21
+
22
+ serverProcess.on("exit", (code) => {
23
+ if (code !== 0) {
24
+ logger.error(`Server exited with code ${code}`);
25
+ process.exit(code || 1);
26
+ }
27
+ });
28
+
29
+ // Handle termination signals
30
+ process.on("SIGINT", () => {
31
+ logger.log("Shutting down server...");
32
+ serverProcess.kill("SIGINT");
33
+ });
34
+
35
+ process.on("SIGTERM", () => {
36
+ logger.log("Shutting down server...");
37
+ serverProcess.kill("SIGTERM");
38
+ });
39
+ }
40
+
41
+ startServer();
package/src/cli.ts CHANGED
@@ -10,12 +10,14 @@ const scripts: Record<string, string> = {
10
10
  "build-hydration": path.join(__dirname, "../scripts/build-hydration.ts"),
11
11
  "auto-wrap": path.join(__dirname, "../scripts/auto-wrap-exports.ts"),
12
12
  "build-styles": path.join(__dirname, "../scripts/build-page-styles.ts"),
13
+ build: path.join(__dirname, "../scripts/build.ts"),
14
+ start: path.join(__dirname, "../scripts/start.ts"),
13
15
  dev: path.join(__dirname, "../scripts/dev.ts"),
14
16
  };
15
17
 
16
18
  if (!command || !scripts[command]) {
17
19
  console.error("Usage: harpy <command>");
18
- console.error("Commands: build-hydration, auto-wrap, build-styles, dev");
20
+ console.error("Commands: build, start, dev, build-hydration, auto-wrap, build-styles");
19
21
  process.exit(1);
20
22
  }
21
23
 
package/src/index.ts CHANGED
@@ -11,6 +11,10 @@ export { JsxRender } from "./decorators/jsx.decorator";
11
11
  export { WithLayout } from "./decorators/layout.decorator";
12
12
  export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
13
13
 
14
+ // SEO Module
15
+ export { SeoModule, BaseSeoService, DefaultSeoService } from "./seo";
16
+ export type { SitemapUrl, RobotsConfig, SeoModuleOptions } from "./seo";
17
+
14
18
  // I18n is provided in a separate package: @harpy-js/i18n
15
19
  // Consumers should import i18n types and modules from that package.
16
20
 
@@ -29,3 +33,4 @@ export {
29
33
  getActiveItemIdFromIndex,
30
34
  getActiveItemIdFromManifest,
31
35
  } from "./client/getActiveItemId";
36
+ export { useI18n } from "./client/use-i18n";
@@ -0,0 +1,5 @@
1
+ export { SeoModule } from './seo.module';
2
+ export { BaseSeoService, DefaultSeoService } from './seo.service';
3
+ export { RobotsController } from './robots.controller';
4
+ export { SitemapController } from './sitemap.controller';
5
+ export type { SitemapUrl, RobotsConfig, SeoModuleOptions } from './seo.types';
@@ -0,0 +1,15 @@
1
+ import { Controller, Get, Header } from '@nestjs/common';
2
+ import { BaseSeoService } from './seo.service';
3
+
4
+ @Controller('robots.txt')
5
+ export class RobotsController {
6
+ constructor(private readonly seoService: BaseSeoService) {}
7
+
8
+ @Get()
9
+ @Header('Content-Type', 'text/plain')
10
+ @Header('Cache-Control', 'public, max-age=86400') // Cache for 24 hours
11
+ getRobots(): string {
12
+ const config = this.seoService.getRobotsConfig();
13
+ return this.seoService.formatRobotsTxt(config);
14
+ }
15
+ }
@@ -0,0 +1,59 @@
1
+ import { Module, DynamicModule, Type } from '@nestjs/common';
2
+ import { RobotsController } from './robots.controller';
3
+ import { SitemapController } from './sitemap.controller';
4
+ import {
5
+ BaseSeoService,
6
+ DefaultSeoService,
7
+ SEO_MODULE_OPTIONS,
8
+ } from './seo.service';
9
+ import type { SeoModuleOptions } from './seo.types';
10
+
11
+ @Module({})
12
+ export class SeoModule {
13
+ /**
14
+ * Register the SEO module with default implementation
15
+ */
16
+ static forRoot(options?: SeoModuleOptions): DynamicModule {
17
+ return {
18
+ module: SeoModule,
19
+ controllers: [RobotsController, SitemapController],
20
+ providers: [
21
+ {
22
+ provide: SEO_MODULE_OPTIONS,
23
+ useValue: options || {},
24
+ },
25
+ {
26
+ provide: BaseSeoService,
27
+ useClass: DefaultSeoService,
28
+ },
29
+ ],
30
+ exports: [BaseSeoService],
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Register the SEO module with a custom service implementation
36
+ * @param customService Your custom service that extends BaseSeoService
37
+ * @param options Optional configuration options
38
+ */
39
+ static forRootWithService(
40
+ customService: Type<BaseSeoService>,
41
+ options?: SeoModuleOptions,
42
+ ): DynamicModule {
43
+ return {
44
+ module: SeoModule,
45
+ controllers: [RobotsController, SitemapController],
46
+ providers: [
47
+ {
48
+ provide: SEO_MODULE_OPTIONS,
49
+ useValue: options || {},
50
+ },
51
+ {
52
+ provide: BaseSeoService,
53
+ useClass: customService,
54
+ },
55
+ ],
56
+ exports: [BaseSeoService],
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,161 @@
1
+ import { Injectable, Inject, Optional } from '@nestjs/common';
2
+ import type { SitemapUrl, RobotsConfig, SeoModuleOptions } from './seo.types';
3
+
4
+ export const SEO_MODULE_OPTIONS = 'SEO_MODULE_OPTIONS';
5
+
6
+ @Injectable()
7
+ export abstract class BaseSeoService {
8
+ protected readonly baseUrl: string;
9
+
10
+ constructor(
11
+ @Optional()
12
+ @Inject(SEO_MODULE_OPTIONS)
13
+ protected readonly options?: SeoModuleOptions,
14
+ ) {
15
+ this.baseUrl = options?.baseUrl || 'http://localhost:3000';
16
+ }
17
+
18
+ /**
19
+ * Override this method to provide custom sitemap URLs
20
+ * This can fetch from database, CMS, or any other source
21
+ */
22
+ abstract getSitemapUrls(): Promise<SitemapUrl[]>;
23
+
24
+ /**
25
+ * Override this method to provide custom robots.txt configuration
26
+ */
27
+ abstract getRobotsConfig(): RobotsConfig;
28
+
29
+ /**
30
+ * Format sitemap URLs to XML string
31
+ */
32
+ formatSitemapXml(urls: SitemapUrl[]): string {
33
+ const urlEntries = urls
34
+ .map((entry) => {
35
+ const lastmod = entry.lastModified
36
+ ? new Date(entry.lastModified).toISOString()
37
+ : new Date().toISOString();
38
+
39
+ return ` <url>
40
+ <loc>${this.escapeXml(entry.url)}</loc>
41
+ <lastmod>${lastmod}</lastmod>
42
+ ${entry.changeFrequency ? `<changefreq>${entry.changeFrequency}</changefreq>` : ''}
43
+ ${entry.priority !== undefined ? `<priority>${entry.priority}</priority>` : ''}
44
+ </url>`;
45
+ })
46
+ .join('\n');
47
+
48
+ return `<?xml version="1.0" encoding="UTF-8"?>
49
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
50
+ ${urlEntries}
51
+ </urlset>`;
52
+ }
53
+
54
+ /**
55
+ * Format robots.txt configuration to string
56
+ */
57
+ formatRobotsTxt(config: RobotsConfig): string {
58
+ const rules = Array.isArray(config.rules) ? config.rules : [config.rules];
59
+
60
+ const rulesText = rules
61
+ .map((rule) => {
62
+ const userAgents = Array.isArray(rule.userAgent)
63
+ ? rule.userAgent
64
+ : [rule.userAgent];
65
+ const allows = rule.allow
66
+ ? Array.isArray(rule.allow)
67
+ ? rule.allow
68
+ : [rule.allow]
69
+ : [];
70
+ const disallows = rule.disallow
71
+ ? Array.isArray(rule.disallow)
72
+ ? rule.disallow
73
+ : [rule.disallow]
74
+ : [];
75
+
76
+ let text = userAgents.map((ua) => `User-agent: ${ua}`).join('\n');
77
+
78
+ if (allows.length > 0) {
79
+ text += '\n' + allows.map((path) => `Allow: ${path}`).join('\n');
80
+ }
81
+
82
+ if (disallows.length > 0) {
83
+ text +=
84
+ '\n' + disallows.map((path) => `Disallow: ${path}`).join('\n');
85
+ }
86
+
87
+ if (rule.crawlDelay) {
88
+ text += `\nCrawl-delay: ${rule.crawlDelay}`;
89
+ }
90
+
91
+ return text;
92
+ })
93
+ .join('\n\n');
94
+
95
+ let result = rulesText;
96
+
97
+ if (config.sitemap) {
98
+ const sitemaps = Array.isArray(config.sitemap)
99
+ ? config.sitemap
100
+ : [config.sitemap];
101
+ result += '\n\n' + sitemaps.map((s) => `Sitemap: ${s}`).join('\n');
102
+ }
103
+
104
+ if (config.host) {
105
+ result += `\n\nHost: ${config.host}`;
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ protected escapeXml(str: string): string {
112
+ return str
113
+ .replace(/&/g, '&amp;')
114
+ .replace(/</g, '&lt;')
115
+ .replace(/>/g, '&gt;')
116
+ .replace(/"/g, '&quot;')
117
+ .replace(/'/g, '&apos;');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Default SEO service implementation
123
+ * Users can extend BaseSeoService to provide custom implementations
124
+ */
125
+ @Injectable()
126
+ export class DefaultSeoService extends BaseSeoService {
127
+ async getSitemapUrls(): Promise<SitemapUrl[]> {
128
+ const now = new Date();
129
+
130
+ // Default homepage only
131
+ return [
132
+ {
133
+ url: this.baseUrl,
134
+ lastModified: now,
135
+ changeFrequency: 'daily',
136
+ priority: 1.0,
137
+ },
138
+ ];
139
+ }
140
+
141
+ getRobotsConfig(): RobotsConfig {
142
+ const defaultConfig: RobotsConfig = {
143
+ rules: {
144
+ userAgent: '*',
145
+ allow: '/',
146
+ },
147
+ sitemap: `${this.baseUrl}/sitemap.xml`,
148
+ host: this.baseUrl,
149
+ };
150
+
151
+ // Merge with options if provided
152
+ if (this.options?.robotsConfig) {
153
+ return {
154
+ ...defaultConfig,
155
+ ...this.options.robotsConfig,
156
+ };
157
+ }
158
+
159
+ return defaultConfig;
160
+ }
161
+ }
@@ -0,0 +1,40 @@
1
+ export interface SitemapUrl {
2
+ url: string;
3
+ lastModified?: Date | string;
4
+ changeFrequency?:
5
+ | 'always'
6
+ | 'hourly'
7
+ | 'daily'
8
+ | 'weekly'
9
+ | 'monthly'
10
+ | 'yearly'
11
+ | 'never';
12
+ priority?: number;
13
+ alternates?: {
14
+ languages?: Record<string, string>;
15
+ };
16
+ }
17
+
18
+ export interface RobotsConfig {
19
+ rules:
20
+ | {
21
+ userAgent: string | string[];
22
+ allow?: string | string[];
23
+ disallow?: string | string[];
24
+ crawlDelay?: number;
25
+ }
26
+ | Array<{
27
+ userAgent: string | string[];
28
+ allow?: string | string[];
29
+ disallow?: string | string[];
30
+ crawlDelay?: number;
31
+ }>;
32
+ sitemap?: string | string[];
33
+ host?: string;
34
+ }
35
+
36
+ export interface SeoModuleOptions {
37
+ baseUrl: string;
38
+ robotsConfig?: Partial<RobotsConfig>;
39
+ sitemapGenerator?: () => Promise<SitemapUrl[]>;
40
+ }