@harpy-js/core 0.5.0 → 0.5.2

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.
@@ -2,6 +2,7 @@ import { JsxLayout } from "../core/jsx.engine";
2
2
  export interface MetaOptions {
3
3
  title?: string;
4
4
  description?: string;
5
+ keywords?: string;
5
6
  canonical?: string;
6
7
  openGraph?: {
7
8
  title?: string;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { StaticAssetsController } from "./core/static-assets.controller";
7
7
  export { JsxRender } from "./decorators/jsx.decorator";
8
8
  export { WithLayout } from "./decorators/layout.decorator";
9
9
  export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
10
+ export { SeoModule, BaseSeoService, DefaultSeoService } from "./seo";
11
+ export type { SitemapUrl, RobotsConfig, SeoModuleOptions } from "./seo";
10
12
  export type { JsxLayout, JsxLayoutProps, PageProps } from "./types/jsx.types";
11
13
  export { RouterModule } from "./core/router.module";
12
14
  export { NavigationService } from "./core/navigation.service";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
6
+ exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
7
7
  var client_component_wrapper_1 = require("./core/client-component-wrapper");
8
8
  Object.defineProperty(exports, "autoWrapClientComponent", { enumerable: true, get: function () { return client_component_wrapper_1.autoWrapClientComponent; } });
9
9
  var hydration_1 = require("./core/hydration");
@@ -22,6 +22,10 @@ var jsx_decorator_1 = require("./decorators/jsx.decorator");
22
22
  Object.defineProperty(exports, "JsxRender", { enumerable: true, get: function () { return jsx_decorator_1.JsxRender; } });
23
23
  var layout_decorator_1 = require("./decorators/layout.decorator");
24
24
  Object.defineProperty(exports, "WithLayout", { enumerable: true, get: function () { return layout_decorator_1.WithLayout; } });
25
+ var seo_1 = require("./seo");
26
+ Object.defineProperty(exports, "SeoModule", { enumerable: true, get: function () { return seo_1.SeoModule; } });
27
+ Object.defineProperty(exports, "BaseSeoService", { enumerable: true, get: function () { return seo_1.BaseSeoService; } });
28
+ Object.defineProperty(exports, "DefaultSeoService", { enumerable: true, get: function () { return seo_1.DefaultSeoService; } });
25
29
  var router_module_1 = require("./core/router.module");
26
30
  Object.defineProperty(exports, "RouterModule", { enumerable: true, get: function () { return router_module_1.RouterModule; } });
27
31
  var navigation_service_1 = require("./core/navigation.service");
@@ -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,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SitemapController = exports.RobotsController = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = void 0;
4
+ var seo_module_1 = require("./seo.module");
5
+ Object.defineProperty(exports, "SeoModule", { enumerable: true, get: function () { return seo_module_1.SeoModule; } });
6
+ var seo_service_1 = require("./seo.service");
7
+ Object.defineProperty(exports, "BaseSeoService", { enumerable: true, get: function () { return seo_service_1.BaseSeoService; } });
8
+ Object.defineProperty(exports, "DefaultSeoService", { enumerable: true, get: function () { return seo_service_1.DefaultSeoService; } });
9
+ var robots_controller_1 = require("./robots.controller");
10
+ Object.defineProperty(exports, "RobotsController", { enumerable: true, get: function () { return robots_controller_1.RobotsController; } });
11
+ var sitemap_controller_1 = require("./sitemap.controller");
12
+ Object.defineProperty(exports, "SitemapController", { enumerable: true, get: function () { return sitemap_controller_1.SitemapController; } });
@@ -0,0 +1,6 @@
1
+ import { BaseSeoService } from './seo.service';
2
+ export declare class RobotsController {
3
+ private readonly seoService;
4
+ constructor(seoService: BaseSeoService);
5
+ getRobots(): string;
6
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RobotsController = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const seo_service_1 = require("./seo.service");
15
+ let RobotsController = class RobotsController {
16
+ seoService;
17
+ constructor(seoService) {
18
+ this.seoService = seoService;
19
+ }
20
+ getRobots() {
21
+ const config = this.seoService.getRobotsConfig();
22
+ return this.seoService.formatRobotsTxt(config);
23
+ }
24
+ };
25
+ exports.RobotsController = RobotsController;
26
+ __decorate([
27
+ (0, common_1.Get)(),
28
+ (0, common_1.Header)('Content-Type', 'text/plain'),
29
+ (0, common_1.Header)('Cache-Control', 'public, max-age=86400'),
30
+ __metadata("design:type", Function),
31
+ __metadata("design:paramtypes", []),
32
+ __metadata("design:returntype", String)
33
+ ], RobotsController.prototype, "getRobots", null);
34
+ exports.RobotsController = RobotsController = __decorate([
35
+ (0, common_1.Controller)('robots.txt'),
36
+ __metadata("design:paramtypes", [seo_service_1.BaseSeoService])
37
+ ], RobotsController);
@@ -0,0 +1,7 @@
1
+ import { DynamicModule, Type } from '@nestjs/common';
2
+ import { BaseSeoService } from './seo.service';
3
+ import type { SeoModuleOptions } from './seo.types';
4
+ export declare class SeoModule {
5
+ static forRoot(options?: SeoModuleOptions): DynamicModule;
6
+ static forRootWithService(customService: Type<BaseSeoService>, options?: SeoModuleOptions): DynamicModule;
7
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var SeoModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SeoModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const robots_controller_1 = require("./robots.controller");
13
+ const sitemap_controller_1 = require("./sitemap.controller");
14
+ const seo_service_1 = require("./seo.service");
15
+ let SeoModule = SeoModule_1 = class SeoModule {
16
+ static forRoot(options) {
17
+ return {
18
+ module: SeoModule_1,
19
+ controllers: [robots_controller_1.RobotsController, sitemap_controller_1.SitemapController],
20
+ providers: [
21
+ {
22
+ provide: seo_service_1.SEO_MODULE_OPTIONS,
23
+ useValue: options || {},
24
+ },
25
+ {
26
+ provide: seo_service_1.BaseSeoService,
27
+ useClass: seo_service_1.DefaultSeoService,
28
+ },
29
+ ],
30
+ exports: [seo_service_1.BaseSeoService],
31
+ };
32
+ }
33
+ static forRootWithService(customService, options) {
34
+ return {
35
+ module: SeoModule_1,
36
+ controllers: [robots_controller_1.RobotsController, sitemap_controller_1.SitemapController],
37
+ providers: [
38
+ {
39
+ provide: seo_service_1.SEO_MODULE_OPTIONS,
40
+ useValue: options || {},
41
+ },
42
+ {
43
+ provide: seo_service_1.BaseSeoService,
44
+ useClass: customService,
45
+ },
46
+ ],
47
+ exports: [seo_service_1.BaseSeoService],
48
+ };
49
+ }
50
+ };
51
+ exports.SeoModule = SeoModule;
52
+ exports.SeoModule = SeoModule = SeoModule_1 = __decorate([
53
+ (0, common_1.Module)({})
54
+ ], SeoModule);
@@ -0,0 +1,16 @@
1
+ import type { SitemapUrl, RobotsConfig, SeoModuleOptions } from './seo.types';
2
+ export declare const SEO_MODULE_OPTIONS = "SEO_MODULE_OPTIONS";
3
+ export declare abstract class BaseSeoService {
4
+ protected readonly options?: SeoModuleOptions | undefined;
5
+ protected readonly baseUrl: string;
6
+ constructor(options?: SeoModuleOptions | undefined);
7
+ abstract getSitemapUrls(): Promise<SitemapUrl[]>;
8
+ abstract getRobotsConfig(): RobotsConfig;
9
+ formatSitemapXml(urls: SitemapUrl[]): string;
10
+ formatRobotsTxt(config: RobotsConfig): string;
11
+ protected escapeXml(str: string): string;
12
+ }
13
+ export declare class DefaultSeoService extends BaseSeoService {
14
+ getSitemapUrls(): Promise<SitemapUrl[]>;
15
+ getRobotsConfig(): RobotsConfig;
16
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DefaultSeoService = exports.BaseSeoService = exports.SEO_MODULE_OPTIONS = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ exports.SEO_MODULE_OPTIONS = 'SEO_MODULE_OPTIONS';
18
+ let BaseSeoService = class BaseSeoService {
19
+ options;
20
+ baseUrl;
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.baseUrl = options?.baseUrl || 'http://localhost:3000';
24
+ }
25
+ formatSitemapXml(urls) {
26
+ const urlEntries = urls
27
+ .map((entry) => {
28
+ const lastmod = entry.lastModified
29
+ ? new Date(entry.lastModified).toISOString()
30
+ : new Date().toISOString();
31
+ return ` <url>
32
+ <loc>${this.escapeXml(entry.url)}</loc>
33
+ <lastmod>${lastmod}</lastmod>
34
+ ${entry.changeFrequency ? `<changefreq>${entry.changeFrequency}</changefreq>` : ''}
35
+ ${entry.priority !== undefined ? `<priority>${entry.priority}</priority>` : ''}
36
+ </url>`;
37
+ })
38
+ .join('\n');
39
+ return `<?xml version="1.0" encoding="UTF-8"?>
40
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
41
+ ${urlEntries}
42
+ </urlset>`;
43
+ }
44
+ formatRobotsTxt(config) {
45
+ const rules = Array.isArray(config.rules) ? config.rules : [config.rules];
46
+ const rulesText = rules
47
+ .map((rule) => {
48
+ const userAgents = Array.isArray(rule.userAgent)
49
+ ? rule.userAgent
50
+ : [rule.userAgent];
51
+ const allows = rule.allow
52
+ ? Array.isArray(rule.allow)
53
+ ? rule.allow
54
+ : [rule.allow]
55
+ : [];
56
+ const disallows = rule.disallow
57
+ ? Array.isArray(rule.disallow)
58
+ ? rule.disallow
59
+ : [rule.disallow]
60
+ : [];
61
+ let text = userAgents.map((ua) => `User-agent: ${ua}`).join('\n');
62
+ if (allows.length > 0) {
63
+ text += '\n' + allows.map((path) => `Allow: ${path}`).join('\n');
64
+ }
65
+ if (disallows.length > 0) {
66
+ text +=
67
+ '\n' + disallows.map((path) => `Disallow: ${path}`).join('\n');
68
+ }
69
+ if (rule.crawlDelay) {
70
+ text += `\nCrawl-delay: ${rule.crawlDelay}`;
71
+ }
72
+ return text;
73
+ })
74
+ .join('\n\n');
75
+ let result = rulesText;
76
+ if (config.sitemap) {
77
+ const sitemaps = Array.isArray(config.sitemap)
78
+ ? config.sitemap
79
+ : [config.sitemap];
80
+ result += '\n\n' + sitemaps.map((s) => `Sitemap: ${s}`).join('\n');
81
+ }
82
+ if (config.host) {
83
+ result += `\n\nHost: ${config.host}`;
84
+ }
85
+ return result;
86
+ }
87
+ escapeXml(str) {
88
+ return str
89
+ .replace(/&/g, '&amp;')
90
+ .replace(/</g, '&lt;')
91
+ .replace(/>/g, '&gt;')
92
+ .replace(/"/g, '&quot;')
93
+ .replace(/'/g, '&apos;');
94
+ }
95
+ };
96
+ exports.BaseSeoService = BaseSeoService;
97
+ exports.BaseSeoService = BaseSeoService = __decorate([
98
+ (0, common_1.Injectable)(),
99
+ __param(0, (0, common_1.Optional)()),
100
+ __param(0, (0, common_1.Inject)(exports.SEO_MODULE_OPTIONS)),
101
+ __metadata("design:paramtypes", [Object])
102
+ ], BaseSeoService);
103
+ let DefaultSeoService = class DefaultSeoService extends BaseSeoService {
104
+ async getSitemapUrls() {
105
+ const now = new Date();
106
+ return [
107
+ {
108
+ url: this.baseUrl,
109
+ lastModified: now,
110
+ changeFrequency: 'daily',
111
+ priority: 1.0,
112
+ },
113
+ ];
114
+ }
115
+ getRobotsConfig() {
116
+ const defaultConfig = {
117
+ rules: {
118
+ userAgent: '*',
119
+ allow: '/',
120
+ },
121
+ sitemap: `${this.baseUrl}/sitemap.xml`,
122
+ host: this.baseUrl,
123
+ };
124
+ if (this.options?.robotsConfig) {
125
+ return {
126
+ ...defaultConfig,
127
+ ...this.options.robotsConfig,
128
+ };
129
+ }
130
+ return defaultConfig;
131
+ }
132
+ };
133
+ exports.DefaultSeoService = DefaultSeoService;
134
+ exports.DefaultSeoService = DefaultSeoService = __decorate([
135
+ (0, common_1.Injectable)()
136
+ ], DefaultSeoService);
@@ -0,0 +1,29 @@
1
+ export interface SitemapUrl {
2
+ url: string;
3
+ lastModified?: Date | string;
4
+ changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
5
+ priority?: number;
6
+ alternates?: {
7
+ languages?: Record<string, string>;
8
+ };
9
+ }
10
+ export interface RobotsConfig {
11
+ rules: {
12
+ userAgent: string | string[];
13
+ allow?: string | string[];
14
+ disallow?: string | string[];
15
+ crawlDelay?: number;
16
+ } | Array<{
17
+ userAgent: string | string[];
18
+ allow?: string | string[];
19
+ disallow?: string | string[];
20
+ crawlDelay?: number;
21
+ }>;
22
+ sitemap?: string | string[];
23
+ host?: string;
24
+ }
25
+ export interface SeoModuleOptions {
26
+ baseUrl: string;
27
+ robotsConfig?: Partial<RobotsConfig>;
28
+ sitemapGenerator?: () => Promise<SitemapUrl[]>;
29
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { BaseSeoService } from './seo.service';
2
+ export declare class SitemapController {
3
+ private readonly seoService;
4
+ constructor(seoService: BaseSeoService);
5
+ getSitemap(): Promise<string>;
6
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SitemapController = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const seo_service_1 = require("./seo.service");
15
+ let SitemapController = class SitemapController {
16
+ seoService;
17
+ constructor(seoService) {
18
+ this.seoService = seoService;
19
+ }
20
+ async getSitemap() {
21
+ const urls = await this.seoService.getSitemapUrls();
22
+ return this.seoService.formatSitemapXml(urls);
23
+ }
24
+ };
25
+ exports.SitemapController = SitemapController;
26
+ __decorate([
27
+ (0, common_1.Get)(),
28
+ (0, common_1.Header)('Content-Type', 'application/xml'),
29
+ (0, common_1.Header)('Cache-Control', 'public, max-age=3600, s-maxage=3600'),
30
+ __metadata("design:type", Function),
31
+ __metadata("design:paramtypes", []),
32
+ __metadata("design:returntype", Promise)
33
+ ], SitemapController.prototype, "getSitemap", null);
34
+ exports.SitemapController = SitemapController = __decorate([
35
+ (0, common_1.Controller)('sitemap.xml'),
36
+ __metadata("design:paramtypes", [seo_service_1.BaseSeoService])
37
+ ], SitemapController);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harpy-js/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Harpy - A powerful NestJS + React/JSX SSR framework with automatic hydration and i18n support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -4,6 +4,7 @@ import { JsxLayout } from "../core/jsx.engine";
4
4
  export interface MetaOptions {
5
5
  title?: string;
6
6
  description?: string;
7
+ keywords?: string;
7
8
  canonical?: string;
8
9
  openGraph?: {
9
10
  title?: string;
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
 
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ import { Controller, Get, Header } from '@nestjs/common';
2
+ import { BaseSeoService } from './seo.service';
3
+
4
+ @Controller('sitemap.xml')
5
+ export class SitemapController {
6
+ constructor(private readonly seoService: BaseSeoService) {}
7
+
8
+ @Get()
9
+ @Header('Content-Type', 'application/xml')
10
+ @Header('Cache-Control', 'public, max-age=3600, s-maxage=3600') // Cache for 1 hour
11
+ async getSitemap(): Promise<string> {
12
+ const urls = await this.seoService.getSitemapUrls();
13
+ return this.seoService.formatSitemapXml(urls);
14
+ }
15
+ }