@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.
- package/index.cjs +1665 -0
- package/index.js +1637 -0
- package/lib/seo-admin-routes.cjs +1377 -0
- package/lib/seo-admin-routes.js +1435 -0
- package/lib/seo-field-injector.cjs +312 -0
- package/lib/seo-field-injector.js +285 -0
- package/package.json +54 -0
- package/src/index.d.ts +11 -0
- package/src/lib/analysis/seo-analysis-collection.d.ts +7 -0
- package/src/lib/analysis/seo-analysis-hooks.d.ts +13 -0
- package/src/lib/analysis/seo-analysis.types.d.ts +56 -0
- package/src/lib/analysis/seo-analyzer.d.ts +36 -0
- package/src/lib/dashboard/seo-analysis-handler.d.ts +18 -0
- package/src/lib/meta/meta-builder.d.ts +14 -0
- package/src/lib/meta/meta-handler.d.ts +16 -0
- package/src/lib/robots/robots-handler.d.ts +27 -0
- package/src/lib/robots/robots-txt-generator.d.ts +12 -0
- package/src/lib/seo-admin-routes.d.ts +3 -0
- package/src/lib/seo-config.types.d.ts +174 -0
- package/src/lib/seo-field-injector.d.ts +27 -0
- package/src/lib/seo-fields.d.ts +21 -0
- package/src/lib/seo-plugin.d.ts +29 -0
- package/src/lib/seo-utils.d.ts +21 -0
- package/src/lib/settings/seo-settings-collection.d.ts +9 -0
- package/src/lib/settings/seo-settings-handler.d.ts +21 -0
- package/src/lib/sitemap/sitemap-cache.d.ts +12 -0
- package/src/lib/sitemap/sitemap-generator.d.ts +20 -0
- package/src/lib/sitemap/sitemap-handler.d.ts +28 -0
- package/src/lib/sitemap/sitemap-settings-collection.d.ts +7 -0
- package/src/lib/sitemap/sitemap-settings-handler.d.ts +18 -0
|
@@ -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
|
+
});
|