@momentumcms/plugins-seo 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1377 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __decorateClass = (decorators, target, key, kind) => {
23
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
24
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
25
+ if (decorator = decorators[i])
26
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
27
+ if (kind && result)
28
+ __defProp(target, key, result);
29
+ return result;
30
+ };
31
+
32
+ // libs/plugins/seo/src/lib/dashboard/seo-dashboard.service.ts
33
+ var import_core, SeoDashboardService;
34
+ var init_seo_dashboard_service = __esm({
35
+ "libs/plugins/seo/src/lib/dashboard/seo-dashboard.service.ts"() {
36
+ "use strict";
37
+ import_core = require("@angular/core");
38
+ SeoDashboardService = class {
39
+ constructor() {
40
+ /** Loading state */
41
+ this.loading = (0, import_core.signal)(false);
42
+ /** Error state */
43
+ this.error = (0, import_core.signal)(null);
44
+ /** All analysis entries */
45
+ this.analyses = (0, import_core.signal)([]);
46
+ /** Collection summaries */
47
+ this.summaries = (0, import_core.signal)([]);
48
+ }
49
+ /**
50
+ * Fetch SEO analysis entries from the API.
51
+ */
52
+ async fetchAnalyses() {
53
+ this.loading.set(true);
54
+ this.error.set(null);
55
+ try {
56
+ const response = await fetch("/api/seo/analyses?limit=500&sort=-analyzedAt");
57
+ if (!response.ok) {
58
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
59
+ }
60
+ const data = await response.json();
61
+ const docs = data.docs ?? [];
62
+ this.analyses.set(docs);
63
+ this.summaries.set(this.buildSummaries(docs));
64
+ } catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ this.error.set(message);
67
+ } finally {
68
+ this.loading.set(false);
69
+ }
70
+ }
71
+ /**
72
+ * Build per-collection summaries from analysis entries.
73
+ */
74
+ buildSummaries(entries) {
75
+ const byCollection = /* @__PURE__ */ new Map();
76
+ for (const entry of entries) {
77
+ const existing = byCollection.get(entry.collection) ?? [];
78
+ existing.push(entry);
79
+ byCollection.set(entry.collection, existing);
80
+ }
81
+ const summaries = [];
82
+ for (const [collection, docs] of byCollection) {
83
+ const totalScore = docs.reduce((sum, d) => sum + d.score, 0);
84
+ const gradeDistribution = { good: 0, warning: 0, poor: 0 };
85
+ for (const doc of docs) {
86
+ gradeDistribution[doc.grade]++;
87
+ }
88
+ summaries.push({
89
+ collection,
90
+ totalDocuments: docs.length,
91
+ avgScore: docs.length > 0 ? Math.round(totalScore / docs.length) : 0,
92
+ gradeDistribution
93
+ });
94
+ }
95
+ return summaries.sort((a, b) => a.avgScore - b.avgScore);
96
+ }
97
+ };
98
+ SeoDashboardService = __decorateClass([
99
+ (0, import_core.Injectable)({ providedIn: "root" })
100
+ ], SeoDashboardService);
101
+ }
102
+ });
103
+
104
+ // libs/plugins/seo/src/lib/seo-utils.ts
105
+ function computeGrade(score) {
106
+ if (score >= 70)
107
+ return "good";
108
+ if (score >= 40)
109
+ return "warning";
110
+ return "poor";
111
+ }
112
+ var init_seo_utils = __esm({
113
+ "libs/plugins/seo/src/lib/seo-utils.ts"() {
114
+ "use strict";
115
+ }
116
+ });
117
+
118
+ // libs/plugins/seo/src/lib/dashboard/seo-dashboard.page.ts
119
+ var seo_dashboard_page_exports = {};
120
+ __export(seo_dashboard_page_exports, {
121
+ SeoDashboardPage: () => SeoDashboardPage
122
+ });
123
+ var import_core2, import_common, import_ui, import_core3, import_outline, SeoDashboardPage;
124
+ var init_seo_dashboard_page = __esm({
125
+ "libs/plugins/seo/src/lib/dashboard/seo-dashboard.page.ts"() {
126
+ "use strict";
127
+ import_core2 = require("@angular/core");
128
+ import_common = require("@angular/common");
129
+ import_ui = require("@momentumcms/ui");
130
+ import_core3 = require("@ng-icons/core");
131
+ import_outline = require("@ng-icons/heroicons/outline");
132
+ init_seo_dashboard_service();
133
+ init_seo_utils();
134
+ SeoDashboardPage = class {
135
+ constructor() {
136
+ this.seo = (0, import_core2.inject)(SeoDashboardService);
137
+ this.platformId = (0, import_core2.inject)(import_core2.PLATFORM_ID);
138
+ /** Overall average score across all analyses */
139
+ this.overallAvgScore = (0, import_core2.computed)(() => {
140
+ const analyses = this.seo.analyses();
141
+ if (analyses.length === 0)
142
+ return 0;
143
+ const total = analyses.reduce((sum, a) => sum + a.score, 0);
144
+ return Math.round(total / analyses.length);
145
+ });
146
+ /** First 20 analyses (server returns newest first via sort=-analyzedAt) */
147
+ this.recentAnalyses = (0, import_core2.computed)(() => {
148
+ return this.seo.analyses().slice(0, 20);
149
+ });
150
+ /** Compute grade from score — delegates to shared utility. */
151
+ this.computeGrade = computeGrade;
152
+ }
153
+ ngOnInit() {
154
+ if (!(0, import_common.isPlatformBrowser)(this.platformId))
155
+ return;
156
+ void this.refresh();
157
+ }
158
+ /**
159
+ * Refresh SEO data.
160
+ */
161
+ async refresh() {
162
+ await this.seo.fetchAnalyses();
163
+ }
164
+ /**
165
+ * Count entries with a specific grade.
166
+ */
167
+ gradeCount(grade) {
168
+ return this.seo.analyses().filter((a) => a.grade === grade).length;
169
+ }
170
+ /**
171
+ * Get badge variant for grade.
172
+ */
173
+ getGradeVariant(grade) {
174
+ switch (grade) {
175
+ case "good":
176
+ return "success";
177
+ case "warning":
178
+ return "warning";
179
+ case "poor":
180
+ return "destructive";
181
+ default:
182
+ return "secondary";
183
+ }
184
+ }
185
+ /**
186
+ * Format timestamp to relative time.
187
+ */
188
+ formatTime(timestamp) {
189
+ const now = Date.now();
190
+ const then = new Date(timestamp).getTime();
191
+ const diff = now - then;
192
+ if (diff < 6e4)
193
+ return "Just now";
194
+ if (diff < 36e5)
195
+ return `${Math.floor(diff / 6e4)}m ago`;
196
+ if (diff < 864e5)
197
+ return `${Math.floor(diff / 36e5)}h ago`;
198
+ return `${Math.floor(diff / 864e5)}d ago`;
199
+ }
200
+ };
201
+ SeoDashboardPage = __decorateClass([
202
+ (0, import_core2.Component)({
203
+ selector: "mcms-seo-dashboard",
204
+ imports: [import_ui.Card, import_ui.CardHeader, import_ui.CardTitle, import_ui.CardDescription, import_ui.CardContent, import_ui.Badge, import_ui.Skeleton, import_core3.NgIcon],
205
+ providers: [
206
+ (0, import_core3.provideIcons)({
207
+ heroMagnifyingGlass: import_outline.heroMagnifyingGlass,
208
+ heroArrowPath: import_outline.heroArrowPath,
209
+ heroCheckCircle: import_outline.heroCheckCircle,
210
+ heroExclamationTriangle: import_outline.heroExclamationTriangle,
211
+ heroXCircle: import_outline.heroXCircle,
212
+ heroDocumentText: import_outline.heroDocumentText,
213
+ heroChartBarSquare: import_outline.heroChartBarSquare
214
+ })
215
+ ],
216
+ changeDetection: import_core2.ChangeDetectionStrategy.OnPush,
217
+ host: { class: "block max-w-6xl" },
218
+ template: `
219
+ <header class="mb-10">
220
+ <div class="flex items-center justify-between">
221
+ <div>
222
+ <h1 class="text-4xl font-bold tracking-tight text-foreground">SEO</h1>
223
+ <p class="text-muted-foreground mt-3 text-lg">Monitor SEO health across your content</p>
224
+ </div>
225
+ <button
226
+ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md
227
+ bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:outline-none
228
+ focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors cursor-pointer"
229
+ (click)="refresh()"
230
+ aria-label="Refresh SEO data"
231
+ >
232
+ <ng-icon name="heroArrowPath" size="16" aria-hidden="true" />
233
+ Refresh
234
+ </button>
235
+ </div>
236
+ </header>
237
+
238
+ <!-- Overview Cards -->
239
+ <section class="mb-10" aria-live="polite">
240
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
241
+ Overview
242
+ </h2>
243
+ @if (seo.loading() && seo.analyses().length === 0) {
244
+ <div
245
+ class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"
246
+ aria-busy="true"
247
+ aria-label="Loading SEO overview"
248
+ >
249
+ @for (i of [1, 2, 3, 4]; track i) {
250
+ <mcms-card>
251
+ <mcms-card-header>
252
+ <mcms-skeleton class="h-4 w-24" />
253
+ <mcms-skeleton class="h-8 w-16 mt-2" />
254
+ </mcms-card-header>
255
+ </mcms-card>
256
+ }
257
+ </div>
258
+ } @else {
259
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
260
+ <!-- Total Analyzed -->
261
+ <mcms-card>
262
+ <mcms-card-header>
263
+ <div class="flex items-center justify-between">
264
+ <mcms-card-description>Documents Analyzed</mcms-card-description>
265
+ <ng-icon
266
+ name="heroDocumentText"
267
+ class="text-muted-foreground"
268
+ size="20"
269
+ aria-hidden="true"
270
+ />
271
+ </div>
272
+ <mcms-card-title>
273
+ <span class="text-3xl font-bold">{{ seo.analyses().length }}</span>
274
+ </mcms-card-title>
275
+ </mcms-card-header>
276
+ </mcms-card>
277
+
278
+ <!-- Average Score -->
279
+ <mcms-card>
280
+ <mcms-card-header>
281
+ <div class="flex items-center justify-between">
282
+ <mcms-card-description>Average Score</mcms-card-description>
283
+ <ng-icon
284
+ name="heroChartBarSquare"
285
+ class="text-muted-foreground"
286
+ size="20"
287
+ aria-hidden="true"
288
+ />
289
+ </div>
290
+ <mcms-card-title>
291
+ <span class="text-3xl font-bold">{{ overallAvgScore() }}</span>
292
+ <span class="text-lg text-muted-foreground">/100</span>
293
+ </mcms-card-title>
294
+ </mcms-card-header>
295
+ </mcms-card>
296
+
297
+ <!-- Good Grade Count -->
298
+ <mcms-card>
299
+ <mcms-card-header>
300
+ <div class="flex items-center justify-between">
301
+ <mcms-card-description>Good</mcms-card-description>
302
+ <ng-icon
303
+ name="heroCheckCircle"
304
+ class="text-green-500"
305
+ size="20"
306
+ aria-hidden="true"
307
+ />
308
+ </div>
309
+ <mcms-card-title>
310
+ <span class="text-3xl font-bold">{{ gradeCount('good') }}</span>
311
+ </mcms-card-title>
312
+ </mcms-card-header>
313
+ </mcms-card>
314
+
315
+ <!-- Needs Attention -->
316
+ <mcms-card>
317
+ <mcms-card-header>
318
+ <div class="flex items-center justify-between">
319
+ <mcms-card-description>Needs Attention</mcms-card-description>
320
+ <ng-icon
321
+ name="heroExclamationTriangle"
322
+ class="text-yellow-500"
323
+ size="20"
324
+ aria-hidden="true"
325
+ />
326
+ </div>
327
+ <mcms-card-title>
328
+ <span class="text-3xl font-bold">{{
329
+ gradeCount('warning') + gradeCount('poor')
330
+ }}</span>
331
+ </mcms-card-title>
332
+ </mcms-card-header>
333
+ </mcms-card>
334
+ </div>
335
+ }
336
+ </section>
337
+
338
+ <!-- Collection Breakdown -->
339
+ @if (seo.summaries().length > 0) {
340
+ <section class="mb-10">
341
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
342
+ By Collection
343
+ </h2>
344
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
345
+ @for (summary of seo.summaries(); track summary.collection) {
346
+ <mcms-card>
347
+ <mcms-card-header>
348
+ <div class="flex items-center justify-between">
349
+ <mcms-card-title>{{ summary.collection }}</mcms-card-title>
350
+ <mcms-badge [variant]="getGradeVariant(computeGrade(summary.avgScore))">
351
+ {{ summary.avgScore }}/100
352
+ </mcms-badge>
353
+ </div>
354
+ <mcms-card-description>
355
+ {{ summary.totalDocuments }} documents analyzed
356
+ </mcms-card-description>
357
+ </mcms-card-header>
358
+ <mcms-card-content>
359
+ <div class="flex gap-4 text-sm">
360
+ <span class="text-green-600">{{ summary.gradeDistribution.good }} good</span>
361
+ <span class="text-yellow-600"
362
+ >{{ summary.gradeDistribution.warning }} warning</span
363
+ >
364
+ <span class="text-red-600">{{ summary.gradeDistribution.poor }} poor</span>
365
+ </div>
366
+ </mcms-card-content>
367
+ </mcms-card>
368
+ }
369
+ </div>
370
+ </section>
371
+ }
372
+
373
+ <!-- Recent Analyses -->
374
+ @if (recentAnalyses().length > 0) {
375
+ <section class="mb-10">
376
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
377
+ Recent Analyses
378
+ </h2>
379
+ <div class="border border-border rounded-lg overflow-hidden">
380
+ <div class="overflow-x-auto">
381
+ <table class="w-full text-sm">
382
+ <caption class="sr-only">
383
+ Recent SEO analysis results
384
+ </caption>
385
+ <thead>
386
+ <tr class="border-b border-border bg-muted/50">
387
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
388
+ Collection
389
+ </th>
390
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
391
+ Document
392
+ </th>
393
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
394
+ Score
395
+ </th>
396
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
397
+ Grade
398
+ </th>
399
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
400
+ Keyword
401
+ </th>
402
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
403
+ Analyzed
404
+ </th>
405
+ </tr>
406
+ </thead>
407
+ <tbody>
408
+ @for (entry of recentAnalyses(); track entry.id) {
409
+ <tr
410
+ class="border-b border-border last:border-0 hover:bg-muted/30 transition-colors"
411
+ >
412
+ <td class="px-4 py-3">
413
+ <mcms-badge variant="outline">{{ entry.collection }}</mcms-badge>
414
+ </td>
415
+ <td class="px-4 py-3 font-mono text-xs text-muted-foreground">
416
+ {{ entry.documentId }}
417
+ </td>
418
+ <td class="px-4 py-3 font-bold">{{ entry.score }}</td>
419
+ <td class="px-4 py-3">
420
+ <mcms-badge [variant]="getGradeVariant(entry.grade)">
421
+ {{ entry.grade }}
422
+ </mcms-badge>
423
+ </td>
424
+ <td class="px-4 py-3 text-muted-foreground">
425
+ {{ entry.focusKeyword ?? '\u2014' }}
426
+ </td>
427
+ <td class="px-4 py-3 text-muted-foreground whitespace-nowrap">
428
+ {{ formatTime(entry.analyzedAt) }}
429
+ </td>
430
+ </tr>
431
+ }
432
+ </tbody>
433
+ </table>
434
+ </div>
435
+ </div>
436
+ </section>
437
+ }
438
+
439
+ <!-- Error state -->
440
+ @if (seo.error(); as err) {
441
+ <mcms-card role="alert">
442
+ <mcms-card-header>
443
+ <mcms-card-title>Error loading SEO data</mcms-card-title>
444
+ <mcms-card-description>{{ err }}</mcms-card-description>
445
+ </mcms-card-header>
446
+ <mcms-card-content>
447
+ <button
448
+ class="text-sm text-primary hover:underline focus-visible:outline-none
449
+ focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer"
450
+ (click)="refresh()"
451
+ >
452
+ Try again
453
+ </button>
454
+ </mcms-card-content>
455
+ </mcms-card>
456
+ }
457
+
458
+ <!-- Empty state -->
459
+ @if (!seo.loading() && seo.analyses().length === 0 && !seo.error()) {
460
+ <mcms-card>
461
+ <mcms-card-content>
462
+ <div class="flex flex-col items-center justify-center py-12 text-center">
463
+ <ng-icon
464
+ name="heroMagnifyingGlass"
465
+ class="text-muted-foreground mb-4"
466
+ size="40"
467
+ aria-hidden="true"
468
+ />
469
+ <p class="text-foreground font-medium">No SEO analyses yet</p>
470
+ <p class="text-sm text-muted-foreground mt-1">
471
+ Analyses will appear here as documents with SEO fields are saved
472
+ </p>
473
+ </div>
474
+ </mcms-card-content>
475
+ </mcms-card>
476
+ }
477
+ `
478
+ })
479
+ ], SeoDashboardPage);
480
+ }
481
+ });
482
+
483
+ // libs/plugins/seo/src/lib/sitemap/sitemap-settings-form.dialog.ts
484
+ var import_core4, import_common2, import_ui2, CHANGE_FREQ_OPTIONS, SitemapSettingsFormDialog;
485
+ var init_sitemap_settings_form_dialog = __esm({
486
+ "libs/plugins/seo/src/lib/sitemap/sitemap-settings-form.dialog.ts"() {
487
+ "use strict";
488
+ import_core4 = require("@angular/core");
489
+ import_common2 = require("@angular/common");
490
+ import_ui2 = require("@momentumcms/ui");
491
+ CHANGE_FREQ_OPTIONS = [
492
+ { label: "(default)", value: "" },
493
+ { label: "Always", value: "always" },
494
+ { label: "Hourly", value: "hourly" },
495
+ { label: "Daily", value: "daily" },
496
+ { label: "Weekly", value: "weekly" },
497
+ { label: "Monthly", value: "monthly" },
498
+ { label: "Yearly", value: "yearly" },
499
+ { label: "Never", value: "never" }
500
+ ];
501
+ SitemapSettingsFormDialog = class {
502
+ constructor() {
503
+ this.data = (0, import_core4.inject)(import_ui2.DIALOG_DATA);
504
+ this.dialogRef = (0, import_core4.inject)(import_ui2.DialogRef);
505
+ this.platformId = (0, import_core4.inject)(import_core4.PLATFORM_ID);
506
+ this.includeInSitemap = (0, import_core4.signal)(this.data.entry.includeInSitemap);
507
+ this.priorityStr = (0, import_core4.signal)(
508
+ this.data.entry.priority != null ? String(this.data.entry.priority) : ""
509
+ );
510
+ this.changeFreq = (0, import_core4.signal)(this.data.entry.changeFreq ?? "");
511
+ this.saving = (0, import_core4.signal)(false);
512
+ this.changeFreqOptions = CHANGE_FREQ_OPTIONS;
513
+ }
514
+ async save() {
515
+ if (this.saving())
516
+ return;
517
+ if (!(0, import_common2.isPlatformBrowser)(this.platformId))
518
+ return;
519
+ this.saving.set(true);
520
+ const body = {
521
+ includeInSitemap: this.includeInSitemap()
522
+ };
523
+ const priorityVal = this.priorityStr().trim();
524
+ if (priorityVal && !Number.isNaN(Number(priorityVal))) {
525
+ body["priority"] = Number(priorityVal);
526
+ }
527
+ const freq = this.changeFreq();
528
+ if (freq) {
529
+ body["changeFreq"] = freq;
530
+ }
531
+ try {
532
+ await fetch(`/api/seo/sitemap-settings/${this.data.entry.collection}`, {
533
+ method: "PUT",
534
+ headers: { "Content-Type": "application/json" },
535
+ body: JSON.stringify(body)
536
+ });
537
+ this.dialogRef.close("saved");
538
+ } finally {
539
+ this.saving.set(false);
540
+ }
541
+ }
542
+ };
543
+ SitemapSettingsFormDialog = __decorateClass([
544
+ (0, import_core4.Component)({
545
+ selector: "mcms-sitemap-settings-form-dialog",
546
+ imports: [
547
+ import_ui2.Dialog,
548
+ import_ui2.DialogHeader,
549
+ import_ui2.DialogTitle,
550
+ import_ui2.DialogContent,
551
+ import_ui2.DialogFooter,
552
+ import_ui2.DialogClose,
553
+ import_ui2.Input,
554
+ import_ui2.Select,
555
+ import_ui2.Switch,
556
+ import_ui2.Button,
557
+ import_ui2.McmsFormField
558
+ ],
559
+ changeDetection: import_core4.ChangeDetectionStrategy.OnPush,
560
+ template: `
561
+ <mcms-dialog>
562
+ <mcms-dialog-header>
563
+ <mcms-dialog-title> Edit Sitemap Settings </mcms-dialog-title>
564
+ </mcms-dialog-header>
565
+
566
+ <mcms-dialog-content>
567
+ <div class="space-y-4">
568
+ <mcms-form-field id="setting-collection">
569
+ <span mcmsLabel>Collection</span>
570
+ <mcms-input [value]="data.entry.collection" [disabled]="true" id="setting-collection" />
571
+ </mcms-form-field>
572
+
573
+ <div class="pt-2">
574
+ <mcms-switch [(value)]="includeInSitemap">Include in Sitemap</mcms-switch>
575
+ </div>
576
+
577
+ <mcms-form-field
578
+ id="setting-priority"
579
+ [hint]="'Value between 0.0 and 1.0. Leave empty for default.'"
580
+ >
581
+ <span mcmsLabel>Priority</span>
582
+ <mcms-input
583
+ [(value)]="priorityStr"
584
+ type="number"
585
+ placeholder="0.5"
586
+ id="setting-priority"
587
+ />
588
+ </mcms-form-field>
589
+
590
+ <mcms-form-field id="setting-change-freq">
591
+ <span mcmsLabel>Change Frequency</span>
592
+ <mcms-select
593
+ [(value)]="changeFreq"
594
+ [options]="changeFreqOptions"
595
+ id="setting-change-freq"
596
+ />
597
+ </mcms-form-field>
598
+ </div>
599
+ </mcms-dialog-content>
600
+
601
+ <mcms-dialog-footer>
602
+ <button mcms-button variant="outline" mcmsDialogClose>Cancel</button>
603
+ <button mcms-button [loading]="saving()" (click)="save()">Save</button>
604
+ </mcms-dialog-footer>
605
+ </mcms-dialog>
606
+ `
607
+ })
608
+ ], SitemapSettingsFormDialog);
609
+ }
610
+ });
611
+
612
+ // libs/plugins/seo/src/lib/sitemap/sitemap-settings.page.ts
613
+ var sitemap_settings_page_exports = {};
614
+ __export(sitemap_settings_page_exports, {
615
+ SitemapSettingsPage: () => SitemapSettingsPage
616
+ });
617
+ function parseSettingsEntry(raw) {
618
+ if (raw == null || typeof raw !== "object")
619
+ return null;
620
+ const doc = raw;
621
+ if (typeof doc["collection"] !== "string")
622
+ return null;
623
+ return {
624
+ collection: doc["collection"],
625
+ includeInSitemap: doc["includeInSitemap"] !== false,
626
+ priority: doc["priority"] != null ? Number(doc["priority"]) : null,
627
+ changeFreq: typeof doc["changeFreq"] === "string" ? doc["changeFreq"] : null,
628
+ id: typeof doc["id"] === "string" ? doc["id"] : null
629
+ };
630
+ }
631
+ var import_core5, import_common3, import_ui3, import_core6, import_outline2, SitemapSettingsPage;
632
+ var init_sitemap_settings_page = __esm({
633
+ "libs/plugins/seo/src/lib/sitemap/sitemap-settings.page.ts"() {
634
+ "use strict";
635
+ import_core5 = require("@angular/core");
636
+ import_common3 = require("@angular/common");
637
+ import_ui3 = require("@momentumcms/ui");
638
+ import_core6 = require("@ng-icons/core");
639
+ import_outline2 = require("@ng-icons/heroicons/outline");
640
+ init_sitemap_settings_form_dialog();
641
+ SitemapSettingsPage = class {
642
+ constructor() {
643
+ this.platformId = (0, import_core5.inject)(import_core5.PLATFORM_ID);
644
+ this.dialog = (0, import_core5.inject)(import_ui3.DialogService);
645
+ this.loading = (0, import_core5.signal)(false);
646
+ this.error = (0, import_core5.signal)(null);
647
+ this.settings = (0, import_core5.signal)([]);
648
+ this.totalCollections = (0, import_core5.computed)(() => this.settings().length);
649
+ this.includedCount = (0, import_core5.computed)(
650
+ () => this.settings().filter((s) => s.includeInSitemap).length
651
+ );
652
+ this.excludedCount = (0, import_core5.computed)(
653
+ () => this.settings().filter((s) => !s.includeInSitemap).length
654
+ );
655
+ }
656
+ ngOnInit() {
657
+ if (!(0, import_common3.isPlatformBrowser)(this.platformId))
658
+ return;
659
+ void this.fetchSettings();
660
+ }
661
+ refresh() {
662
+ void this.fetchSettings();
663
+ }
664
+ openEditDialog(entry) {
665
+ const ref = this.dialog.open(SitemapSettingsFormDialog, {
666
+ data: { entry },
667
+ width: "28rem"
668
+ });
669
+ ref.afterClosed.subscribe((result) => {
670
+ if (result === "saved")
671
+ void this.fetchSettings();
672
+ });
673
+ }
674
+ async toggleInclude(entry) {
675
+ if (!(0, import_common3.isPlatformBrowser)(this.platformId))
676
+ return;
677
+ const newValue = !entry.includeInSitemap;
678
+ this.settings.update(
679
+ (list) => list.map(
680
+ (s) => s.collection === entry.collection ? { ...s, includeInSitemap: newValue } : s
681
+ )
682
+ );
683
+ try {
684
+ const res = await fetch(`/api/seo/sitemap-settings/${entry.collection}`, {
685
+ method: "PUT",
686
+ headers: { "Content-Type": "application/json" },
687
+ body: JSON.stringify({ includeInSitemap: newValue })
688
+ });
689
+ if (!res.ok)
690
+ throw new Error("Failed");
691
+ } catch {
692
+ this.settings.update(
693
+ (list) => list.map(
694
+ (s) => s.collection === entry.collection ? { ...s, includeInSitemap: entry.includeInSitemap } : s
695
+ )
696
+ );
697
+ }
698
+ }
699
+ async fetchSettings() {
700
+ this.loading.set(true);
701
+ this.error.set(null);
702
+ try {
703
+ const res = await fetch("/api/seo/sitemap-settings");
704
+ if (!res.ok) {
705
+ this.error.set(`HTTP ${res.status}`);
706
+ return;
707
+ }
708
+ const data = await res.json();
709
+ if (data == null || typeof data !== "object")
710
+ return;
711
+ const body = data;
712
+ if (!Array.isArray(body["settings"]))
713
+ return;
714
+ const entries = body["settings"].map(parseSettingsEntry).filter((e) => e != null);
715
+ this.settings.set(entries);
716
+ } catch {
717
+ this.error.set("Failed to load sitemap settings");
718
+ } finally {
719
+ this.loading.set(false);
720
+ }
721
+ }
722
+ };
723
+ SitemapSettingsPage = __decorateClass([
724
+ (0, import_core5.Component)({
725
+ selector: "mcms-sitemap-settings-page",
726
+ imports: [
727
+ import_ui3.Card,
728
+ import_ui3.CardHeader,
729
+ import_ui3.CardTitle,
730
+ import_ui3.CardDescription,
731
+ import_ui3.CardContent,
732
+ import_ui3.Badge,
733
+ import_ui3.Skeleton,
734
+ import_ui3.Button,
735
+ import_ui3.Switch,
736
+ import_core6.NgIcon
737
+ ],
738
+ providers: [(0, import_core6.provideIcons)({ heroMap: import_outline2.heroMap, heroArrowPath: import_outline2.heroArrowPath })],
739
+ changeDetection: import_core5.ChangeDetectionStrategy.OnPush,
740
+ host: { class: "block max-w-6xl" },
741
+ template: `
742
+ <header class="mb-10">
743
+ <div class="flex items-center justify-between">
744
+ <div>
745
+ <h1 class="text-4xl font-bold tracking-tight text-foreground">Sitemap</h1>
746
+ <p class="text-muted-foreground mt-3 text-lg">
747
+ Control which collections appear in the XML sitemap
748
+ </p>
749
+ </div>
750
+ <button
751
+ mcms-button
752
+ variant="outline"
753
+ size="sm"
754
+ (click)="refresh()"
755
+ ariaLabel="Refresh sitemap settings"
756
+ >
757
+ <ng-icon name="heroArrowPath" size="16" aria-hidden="true" />
758
+ Refresh
759
+ </button>
760
+ </div>
761
+ </header>
762
+
763
+ <!-- Summary Cards -->
764
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8">
765
+ <mcms-card>
766
+ <mcms-card-header>
767
+ <div class="flex items-center justify-between">
768
+ <mcms-card-description>Total Collections</mcms-card-description>
769
+ <ng-icon name="heroMap" class="text-muted-foreground" size="20" aria-hidden="true" />
770
+ </div>
771
+ <mcms-card-title>
772
+ <span class="text-3xl font-bold">{{ totalCollections() }}</span>
773
+ </mcms-card-title>
774
+ </mcms-card-header>
775
+ </mcms-card>
776
+ <mcms-card>
777
+ <mcms-card-header>
778
+ <mcms-card-description>In Sitemap</mcms-card-description>
779
+ <mcms-card-title>
780
+ <span class="text-3xl font-bold text-emerald-600">{{ includedCount() }}</span>
781
+ </mcms-card-title>
782
+ </mcms-card-header>
783
+ </mcms-card>
784
+ <mcms-card>
785
+ <mcms-card-header>
786
+ <mcms-card-description>Excluded</mcms-card-description>
787
+ <mcms-card-title>
788
+ <span class="text-3xl font-bold text-muted-foreground">{{ excludedCount() }}</span>
789
+ </mcms-card-title>
790
+ </mcms-card-header>
791
+ </mcms-card>
792
+ </div>
793
+
794
+ <!-- Settings Table -->
795
+ @if (loading() && settings().length === 0) {
796
+ <div class="space-y-3" aria-busy="true">
797
+ @for (i of [1, 2, 3]; track i) {
798
+ <mcms-skeleton class="h-14 w-full" />
799
+ }
800
+ </div>
801
+ } @else if (settings().length > 0) {
802
+ <div class="border border-border rounded-lg overflow-hidden">
803
+ <div class="overflow-x-auto">
804
+ <table class="w-full text-sm" role="table">
805
+ <caption class="sr-only">
806
+ Sitemap settings per collection
807
+ </caption>
808
+ <thead>
809
+ <tr class="border-b border-border bg-muted/50">
810
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
811
+ Collection
812
+ </th>
813
+ <th scope="col" class="px-4 py-3 text-center font-medium text-muted-foreground">
814
+ In Sitemap
815
+ </th>
816
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
817
+ Priority
818
+ </th>
819
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
820
+ Frequency
821
+ </th>
822
+ <th scope="col" class="px-4 py-3 text-right font-medium text-muted-foreground">
823
+ Actions
824
+ </th>
825
+ </tr>
826
+ </thead>
827
+ <tbody>
828
+ @for (entry of settings(); track entry.collection) {
829
+ <tr
830
+ class="border-b border-border last:border-0 hover:bg-muted/30 transition-colors"
831
+ >
832
+ <td class="px-4 py-3 font-medium text-foreground">
833
+ {{ entry.collection }}
834
+ </td>
835
+ <td class="px-4 py-3 text-center">
836
+ <mcms-switch
837
+ [value]="entry.includeInSitemap"
838
+ (valueChange)="toggleInclude(entry)"
839
+ ariaLabel="Toggle sitemap inclusion"
840
+ />
841
+ </td>
842
+ <td class="px-4 py-3 text-muted-foreground">
843
+ {{ entry.priority != null ? entry.priority : '\u2014' }}
844
+ </td>
845
+ <td class="px-4 py-3">
846
+ @if (entry.changeFreq) {
847
+ <mcms-badge variant="outline">{{ entry.changeFreq }}</mcms-badge>
848
+ } @else {
849
+ <span class="text-muted-foreground">\u2014</span>
850
+ }
851
+ </td>
852
+ <td class="px-4 py-3 text-right">
853
+ <button
854
+ mcms-button
855
+ variant="ghost"
856
+ size="sm"
857
+ (click)="openEditDialog(entry)"
858
+ ariaLabel="Edit settings"
859
+ >
860
+ Edit
861
+ </button>
862
+ </td>
863
+ </tr>
864
+ }
865
+ </tbody>
866
+ </table>
867
+ </div>
868
+ </div>
869
+ } @else {
870
+ <mcms-card>
871
+ <mcms-card-content>
872
+ <div class="flex flex-col items-center justify-center py-12 text-center">
873
+ <ng-icon
874
+ name="heroMap"
875
+ class="text-muted-foreground mb-4"
876
+ size="40"
877
+ aria-hidden="true"
878
+ />
879
+ <p class="text-foreground font-medium">No SEO-enabled collections</p>
880
+ <p class="text-sm text-muted-foreground mt-1">
881
+ Add SEO fields to your collections to configure sitemap settings
882
+ </p>
883
+ </div>
884
+ </mcms-card-content>
885
+ </mcms-card>
886
+ }
887
+
888
+ <!-- Error state -->
889
+ @if (error(); as err) {
890
+ <mcms-card role="alert" class="mt-6">
891
+ <mcms-card-header>
892
+ <mcms-card-title>Error loading sitemap settings</mcms-card-title>
893
+ <mcms-card-description>{{ err }}</mcms-card-description>
894
+ </mcms-card-header>
895
+ <mcms-card-content>
896
+ <button
897
+ class="text-sm text-primary hover:underline focus-visible:outline-none
898
+ focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer"
899
+ (click)="refresh()"
900
+ >
901
+ Try again
902
+ </button>
903
+ </mcms-card-content>
904
+ </mcms-card>
905
+ }
906
+ `
907
+ })
908
+ ], SitemapSettingsPage);
909
+ }
910
+ });
911
+
912
+ // libs/plugins/seo/src/lib/robots/robots-txt-generator.ts
913
+ function sanitizeLine(str) {
914
+ return str.replace(/[\r\n]/g, "");
915
+ }
916
+ function generateRobotsTxt(siteUrl, config) {
917
+ const lines = [];
918
+ if (config.rules && config.rules.length > 0) {
919
+ for (const rule of config.rules) {
920
+ lines.push(`User-agent: ${sanitizeLine(rule.userAgent)}`);
921
+ if (rule.allow) {
922
+ for (const path of rule.allow) {
923
+ lines.push(`Allow: ${sanitizeLine(path)}`);
924
+ }
925
+ }
926
+ if (rule.disallow) {
927
+ for (const path of rule.disallow) {
928
+ lines.push(`Disallow: ${sanitizeLine(path)}`);
929
+ }
930
+ }
931
+ if (config.crawlDelay != null) {
932
+ lines.push(`Crawl-delay: ${config.crawlDelay}`);
933
+ }
934
+ lines.push("");
935
+ }
936
+ } else {
937
+ lines.push("User-agent: *");
938
+ lines.push("Allow: /");
939
+ if (config.crawlDelay != null) {
940
+ lines.push(`Crawl-delay: ${config.crawlDelay}`);
941
+ }
942
+ lines.push("");
943
+ }
944
+ if (siteUrl) {
945
+ lines.push(`Sitemap: ${siteUrl}/sitemap.xml`);
946
+ }
947
+ if (config.additionalSitemaps) {
948
+ for (const sitemap of config.additionalSitemaps) {
949
+ lines.push(`Sitemap: ${sanitizeLine(sitemap)}`);
950
+ }
951
+ }
952
+ return lines.join("\n");
953
+ }
954
+ var init_robots_txt_generator = __esm({
955
+ "libs/plugins/seo/src/lib/robots/robots-txt-generator.ts"() {
956
+ "use strict";
957
+ }
958
+ });
959
+
960
+ // libs/plugins/seo/src/lib/robots/robots-settings.page.ts
961
+ var robots_settings_page_exports = {};
962
+ __export(robots_settings_page_exports, {
963
+ RobotsSettingsPage: () => RobotsSettingsPage
964
+ });
965
+ var import_core7, import_common4, import_ui4, import_core8, import_outline3, RobotsSettingsPage;
966
+ var init_robots_settings_page = __esm({
967
+ "libs/plugins/seo/src/lib/robots/robots-settings.page.ts"() {
968
+ "use strict";
969
+ import_core7 = require("@angular/core");
970
+ import_common4 = require("@angular/common");
971
+ import_ui4 = require("@momentumcms/ui");
972
+ import_core8 = require("@ng-icons/core");
973
+ import_outline3 = require("@ng-icons/heroicons/outline");
974
+ init_robots_txt_generator();
975
+ RobotsSettingsPage = class {
976
+ constructor() {
977
+ this.platformId = (0, import_core7.inject)(import_core7.PLATFORM_ID);
978
+ this.loading = (0, import_core7.signal)(false);
979
+ this.saving = (0, import_core7.signal)(false);
980
+ this.error = (0, import_core7.signal)(null);
981
+ this.saveSuccess = (0, import_core7.signal)(false);
982
+ this.rules = (0, import_core7.signal)([{ userAgent: "*", allow: ["/"], disallow: [] }]);
983
+ this.crawlDelay = (0, import_core7.signal)("");
984
+ this.additionalSitemaps = (0, import_core7.signal)([]);
985
+ this.preview = (0, import_core7.computed)(() => {
986
+ const config = {
987
+ rules: this.rules(),
988
+ crawlDelay: this.crawlDelay() ? Number(this.crawlDelay()) : void 0,
989
+ additionalSitemaps: this.additionalSitemaps().filter((s) => s.length > 0)
990
+ };
991
+ return generateRobotsTxt("", config);
992
+ });
993
+ }
994
+ ngOnInit() {
995
+ if (!(0, import_common4.isPlatformBrowser)(this.platformId))
996
+ return;
997
+ void this.fetchSettings();
998
+ }
999
+ refresh() {
1000
+ void this.fetchSettings();
1001
+ }
1002
+ addRule() {
1003
+ this.rules.update((list) => [...list, { userAgent: "*", allow: [], disallow: [] }]);
1004
+ }
1005
+ removeRule(index) {
1006
+ this.rules.update((list) => list.filter((_, i) => i !== index));
1007
+ }
1008
+ updateRuleField(index, field, event) {
1009
+ const value = event.target.value;
1010
+ this.rules.update((list) => list.map((r, i) => i === index ? { ...r, [field]: value } : r));
1011
+ }
1012
+ updateRulePathField(index, field, event) {
1013
+ const value = event.target.value;
1014
+ const paths = value.split("\n").filter((p) => p.length > 0);
1015
+ this.rules.update((list) => list.map((r, i) => i === index ? { ...r, [field]: paths } : r));
1016
+ }
1017
+ updateCrawlDelay(event) {
1018
+ this.crawlDelay.set(event.target.value);
1019
+ }
1020
+ updateAdditionalSitemaps(event) {
1021
+ const value = event.target.value;
1022
+ this.additionalSitemaps.set(value.split("\n"));
1023
+ }
1024
+ async save() {
1025
+ if (!(0, import_common4.isPlatformBrowser)(this.platformId))
1026
+ return;
1027
+ this.saving.set(true);
1028
+ this.error.set(null);
1029
+ this.saveSuccess.set(false);
1030
+ try {
1031
+ const body = {
1032
+ robotsRules: this.rules(),
1033
+ robotsCrawlDelay: this.crawlDelay() ? Number(this.crawlDelay()) : null,
1034
+ robotsAdditionalSitemaps: this.additionalSitemaps().filter((s) => s.length > 0)
1035
+ };
1036
+ const res = await fetch("/api/seo/seo-settings", {
1037
+ method: "PUT",
1038
+ headers: { "Content-Type": "application/json" },
1039
+ body: JSON.stringify(body)
1040
+ });
1041
+ if (!res.ok) {
1042
+ this.error.set(`HTTP ${res.status}`);
1043
+ return;
1044
+ }
1045
+ this.saveSuccess.set(true);
1046
+ setTimeout(() => this.saveSuccess.set(false), 3e3);
1047
+ } catch {
1048
+ this.error.set("Failed to save settings");
1049
+ } finally {
1050
+ this.saving.set(false);
1051
+ }
1052
+ }
1053
+ async fetchSettings() {
1054
+ this.loading.set(true);
1055
+ this.error.set(null);
1056
+ try {
1057
+ const res = await fetch("/api/seo/seo-settings");
1058
+ if (!res.ok) {
1059
+ this.error.set(`HTTP ${res.status}`);
1060
+ return;
1061
+ }
1062
+ const data = await res.json();
1063
+ if (data.robotsRules && Array.isArray(data.robotsRules) && data.robotsRules.length > 0) {
1064
+ this.rules.set(
1065
+ data.robotsRules.map((r) => ({
1066
+ userAgent: r.userAgent ?? "*",
1067
+ allow: Array.isArray(r.allow) ? r.allow : [],
1068
+ disallow: Array.isArray(r.disallow) ? r.disallow : []
1069
+ }))
1070
+ );
1071
+ }
1072
+ this.crawlDelay.set(data.robotsCrawlDelay != null ? String(data.robotsCrawlDelay) : "");
1073
+ this.additionalSitemaps.set(
1074
+ Array.isArray(data.robotsAdditionalSitemaps) ? data.robotsAdditionalSitemaps : []
1075
+ );
1076
+ } catch {
1077
+ this.error.set("Failed to load robots settings");
1078
+ } finally {
1079
+ this.loading.set(false);
1080
+ }
1081
+ }
1082
+ };
1083
+ RobotsSettingsPage = __decorateClass([
1084
+ (0, import_core7.Component)({
1085
+ selector: "mcms-robots-settings-page",
1086
+ imports: [
1087
+ import_ui4.Card,
1088
+ import_ui4.CardHeader,
1089
+ import_ui4.CardTitle,
1090
+ import_ui4.CardDescription,
1091
+ import_ui4.CardContent,
1092
+ import_ui4.Badge,
1093
+ import_ui4.Skeleton,
1094
+ import_ui4.Button,
1095
+ import_core8.NgIcon
1096
+ ],
1097
+ providers: [(0, import_core8.provideIcons)({ heroDocumentText: import_outline3.heroDocumentText, heroArrowPath: import_outline3.heroArrowPath, heroPlusCircle: import_outline3.heroPlusCircle, heroTrash: import_outline3.heroTrash })],
1098
+ changeDetection: import_core7.ChangeDetectionStrategy.OnPush,
1099
+ host: { class: "block max-w-6xl" },
1100
+ template: `
1101
+ <header class="mb-10">
1102
+ <div class="flex items-center justify-between">
1103
+ <div>
1104
+ <h1 class="text-4xl font-bold tracking-tight text-foreground">Robots</h1>
1105
+ <p class="text-muted-foreground mt-3 text-lg">
1106
+ Configure robots.txt rules for search engine crawlers
1107
+ </p>
1108
+ </div>
1109
+ <div class="flex gap-2">
1110
+ <button
1111
+ mcms-button
1112
+ variant="outline"
1113
+ size="sm"
1114
+ (click)="refresh()"
1115
+ ariaLabel="Refresh robots settings"
1116
+ >
1117
+ <ng-icon name="heroArrowPath" size="16" aria-hidden="true" />
1118
+ Refresh
1119
+ </button>
1120
+ <button
1121
+ mcms-button
1122
+ size="sm"
1123
+ (click)="save()"
1124
+ [disabled]="saving()"
1125
+ ariaLabel="Save robots settings"
1126
+ >
1127
+ {{ saving() ? 'Saving...' : 'Save' }}
1128
+ </button>
1129
+ </div>
1130
+ </div>
1131
+ </header>
1132
+
1133
+ @if (loading() && rules().length === 0) {
1134
+ <div class="space-y-4" aria-busy="true">
1135
+ @for (i of [1, 2, 3]; track i) {
1136
+ <mcms-skeleton class="h-20 w-full" />
1137
+ }
1138
+ </div>
1139
+ } @else {
1140
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
1141
+ <!-- Rules Section -->
1142
+ <div class="space-y-6">
1143
+ <div class="flex items-center justify-between">
1144
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
1145
+ Rules
1146
+ </h2>
1147
+ <button
1148
+ mcms-button
1149
+ variant="outline"
1150
+ size="sm"
1151
+ (click)="addRule()"
1152
+ ariaLabel="Add new rule"
1153
+ >
1154
+ <ng-icon name="heroPlusCircle" size="16" aria-hidden="true" />
1155
+ Add Rule
1156
+ </button>
1157
+ </div>
1158
+
1159
+ @for (rule of rules(); track $index; let i = $index) {
1160
+ <mcms-card>
1161
+ <mcms-card-header>
1162
+ <div class="flex items-center justify-between">
1163
+ <mcms-card-title class="text-base">Rule {{ i + 1 }}</mcms-card-title>
1164
+ @if (rules().length > 1) {
1165
+ <button
1166
+ mcms-button
1167
+ variant="ghost"
1168
+ size="sm"
1169
+ (click)="removeRule(i)"
1170
+ ariaLabel="Remove rule"
1171
+ >
1172
+ <ng-icon
1173
+ name="heroTrash"
1174
+ size="16"
1175
+ class="text-destructive"
1176
+ aria-hidden="true"
1177
+ />
1178
+ </button>
1179
+ }
1180
+ </div>
1181
+ </mcms-card-header>
1182
+ <mcms-card-content>
1183
+ <div class="space-y-3">
1184
+ <div>
1185
+ <label
1186
+ [attr.for]="'user-agent-' + i"
1187
+ class="text-sm font-medium text-foreground block mb-1"
1188
+ >User-Agent</label
1189
+ >
1190
+ <input
1191
+ type="text"
1192
+ [id]="'user-agent-' + i"
1193
+ class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm
1194
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1195
+ [value]="rule.userAgent"
1196
+ (input)="updateRuleField(i, 'userAgent', $event)"
1197
+ placeholder="*"
1198
+ />
1199
+ </div>
1200
+ <div>
1201
+ <label
1202
+ [attr.for]="'allow-' + i"
1203
+ class="text-sm font-medium text-foreground block mb-1"
1204
+ >Allow (one per line)</label
1205
+ >
1206
+ <textarea
1207
+ [id]="'allow-' + i"
1208
+ class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono
1209
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1210
+ rows="2"
1211
+ [value]="rule.allow.join('\\n')"
1212
+ (input)="updateRulePathField(i, 'allow', $event)"
1213
+ placeholder="/
1214
+ /public"
1215
+ ></textarea>
1216
+ </div>
1217
+ <div>
1218
+ <label
1219
+ [attr.for]="'disallow-' + i"
1220
+ class="text-sm font-medium text-foreground block mb-1"
1221
+ >Disallow (one per line)</label
1222
+ >
1223
+ <textarea
1224
+ [id]="'disallow-' + i"
1225
+ class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono
1226
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1227
+ rows="2"
1228
+ [value]="rule.disallow.join('\\n')"
1229
+ (input)="updateRulePathField(i, 'disallow', $event)"
1230
+ placeholder="/admin
1231
+ /api"
1232
+ ></textarea>
1233
+ </div>
1234
+ </div>
1235
+ </mcms-card-content>
1236
+ </mcms-card>
1237
+ }
1238
+
1239
+ <!-- Crawl Delay -->
1240
+ <mcms-card>
1241
+ <mcms-card-header>
1242
+ <mcms-card-title class="text-base">Crawl Delay</mcms-card-title>
1243
+ <mcms-card-description
1244
+ >Seconds between successive requests (optional)</mcms-card-description
1245
+ >
1246
+ </mcms-card-header>
1247
+ <mcms-card-content>
1248
+ <label for="crawl-delay" class="sr-only">Crawl Delay</label>
1249
+ <input
1250
+ type="number"
1251
+ id="crawl-delay"
1252
+ class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm
1253
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1254
+ [value]="crawlDelay()"
1255
+ (input)="updateCrawlDelay($event)"
1256
+ min="0"
1257
+ placeholder="Not set"
1258
+ />
1259
+ </mcms-card-content>
1260
+ </mcms-card>
1261
+
1262
+ <!-- Additional Sitemaps -->
1263
+ <mcms-card>
1264
+ <mcms-card-header>
1265
+ <mcms-card-title class="text-base">Additional Sitemaps</mcms-card-title>
1266
+ <mcms-card-description
1267
+ >Extra sitemap URLs to include (one per line)</mcms-card-description
1268
+ >
1269
+ </mcms-card-header>
1270
+ <mcms-card-content>
1271
+ <label for="additional-sitemaps" class="sr-only">Additional Sitemaps</label>
1272
+ <textarea
1273
+ id="additional-sitemaps"
1274
+ class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono
1275
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1276
+ rows="3"
1277
+ [value]="additionalSitemaps().join('\\n')"
1278
+ (input)="updateAdditionalSitemaps($event)"
1279
+ placeholder="https://example.com/extra-sitemap.xml"
1280
+ ></textarea>
1281
+ </mcms-card-content>
1282
+ </mcms-card>
1283
+ </div>
1284
+
1285
+ <!-- Preview Section -->
1286
+ <div>
1287
+ <h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
1288
+ Preview
1289
+ </h2>
1290
+ <mcms-card>
1291
+ <mcms-card-header>
1292
+ <div class="flex items-center justify-between">
1293
+ <mcms-card-title class="text-base">robots.txt</mcms-card-title>
1294
+ <mcms-badge variant="outline">Preview</mcms-badge>
1295
+ </div>
1296
+ </mcms-card-header>
1297
+ <mcms-card-content>
1298
+ <pre
1299
+ class="text-sm font-mono bg-muted/50 rounded-md p-4 overflow-x-auto whitespace-pre-wrap"
1300
+ >{{ preview() }}</pre
1301
+ >
1302
+ </mcms-card-content>
1303
+ </mcms-card>
1304
+ </div>
1305
+ </div>
1306
+ }
1307
+
1308
+ <!-- Success/Error messages -->
1309
+ @if (saveSuccess()) {
1310
+ <div
1311
+ class="mt-6 p-3 rounded-md bg-emerald-50 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 text-sm"
1312
+ >
1313
+ Settings saved successfully
1314
+ </div>
1315
+ }
1316
+ @if (error(); as err) {
1317
+ <mcms-card role="alert" class="mt-6">
1318
+ <mcms-card-header>
1319
+ <mcms-card-title>Error</mcms-card-title>
1320
+ <mcms-card-description>{{ err }}</mcms-card-description>
1321
+ </mcms-card-header>
1322
+ <mcms-card-content>
1323
+ <button
1324
+ class="text-sm text-primary hover:underline focus-visible:outline-none
1325
+ focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer"
1326
+ (click)="refresh()"
1327
+ >
1328
+ Try again
1329
+ </button>
1330
+ </mcms-card-content>
1331
+ </mcms-card>
1332
+ }
1333
+ `
1334
+ })
1335
+ ], RobotsSettingsPage);
1336
+ }
1337
+ });
1338
+
1339
+ // libs/plugins/seo/src/lib/seo-admin-routes.ts
1340
+ var seo_admin_routes_exports = {};
1341
+ __export(seo_admin_routes_exports, {
1342
+ seoAdminRoutes: () => seoAdminRoutes
1343
+ });
1344
+ module.exports = __toCommonJS(seo_admin_routes_exports);
1345
+ var seoAdminRoutes = [
1346
+ {
1347
+ path: "seo",
1348
+ label: "SEO",
1349
+ icon: "heroMagnifyingGlass",
1350
+ group: "SEO",
1351
+ loadComponent: () => Promise.resolve().then(() => (init_seo_dashboard_page(), seo_dashboard_page_exports)).then(
1352
+ (m) => m["SeoDashboardPage"]
1353
+ )
1354
+ },
1355
+ {
1356
+ path: "seo/sitemap",
1357
+ label: "Sitemap",
1358
+ icon: "heroMap",
1359
+ group: "SEO",
1360
+ loadComponent: () => Promise.resolve().then(() => (init_sitemap_settings_page(), sitemap_settings_page_exports)).then(
1361
+ (m) => m["SitemapSettingsPage"]
1362
+ )
1363
+ },
1364
+ {
1365
+ path: "seo/robots",
1366
+ label: "Robots",
1367
+ icon: "heroDocumentText",
1368
+ group: "SEO",
1369
+ loadComponent: () => Promise.resolve().then(() => (init_robots_settings_page(), robots_settings_page_exports)).then(
1370
+ (m) => m["RobotsSettingsPage"]
1371
+ )
1372
+ }
1373
+ ];
1374
+ // Annotate the CommonJS export names for ESM import in node:
1375
+ 0 && (module.exports = {
1376
+ seoAdminRoutes
1377
+ });