@harpy-js/core 0.5.1 → 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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.js +12 -0
- package/dist/seo/robots.controller.d.ts +6 -0
- package/dist/seo/robots.controller.js +37 -0
- package/dist/seo/seo.module.d.ts +7 -0
- package/dist/seo/seo.module.js +54 -0
- package/dist/seo/seo.service.d.ts +16 -0
- package/dist/seo/seo.service.js +136 -0
- package/dist/seo/seo.types.d.ts +29 -0
- package/dist/seo/seo.types.js +2 -0
- package/dist/seo/sitemap.controller.d.ts +6 -0
- package/dist/seo/sitemap.controller.js +37 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/seo/index.ts +5 -0
- package/src/seo/robots.controller.ts +15 -0
- package/src/seo/seo.module.ts +59 -0
- package/src/seo/seo.service.ts +161 -0
- package/src/seo/seo.types.ts +40 -0
- package/src/seo/sitemap.controller.ts +15 -0
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,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, '&')
|
|
90
|
+
.replace(/</g, '<')
|
|
91
|
+
.replace(/>/g, '>')
|
|
92
|
+
.replace(/"/g, '"')
|
|
93
|
+
.replace(/'/g, ''');
|
|
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,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
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
|
|
package/src/seo/index.ts
ADDED
|
@@ -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, '&')
|
|
114
|
+
.replace(/</g, '<')
|
|
115
|
+
.replace(/>/g, '>')
|
|
116
|
+
.replace(/"/g, '"')
|
|
117
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|