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