@seo-console/package 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,1547 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ crawlSiteForSEO: () => crawlSiteForSEO,
34
+ createSEORecord: () => createSEORecord,
35
+ createSEORecordSchema: () => createSEORecordSchema,
36
+ deleteSEORecord: () => deleteSEORecord,
37
+ discoverNextJSRoutes: () => discoverNextJSRoutes,
38
+ extractMetadataFromURL: () => extractMetadataFromURL,
39
+ generateRobotsTxt: () => generateRobotsTxt,
40
+ generateSitemapFromRecords: () => generateSitemapFromRecords,
41
+ generateSitemapXML: () => generateSitemapXML,
42
+ getAllSEORecords: () => getSEORecords,
43
+ getRoutePathFromParams: () => getRoutePathFromParams,
44
+ getSEORecordById: () => getSEORecordById,
45
+ getSEORecordByRoute: () => getSEORecordByRoute,
46
+ getSEORecords: () => getSEORecords,
47
+ metadataToSEORecord: () => metadataToSEORecord,
48
+ seoRecordsToSitemapEntries: () => seoRecordsToSitemapEntries,
49
+ updateRobotsTxtWithSitemap: () => updateRobotsTxtWithSitemap,
50
+ updateSEORecord: () => updateSEORecord,
51
+ updateSEORecordSchema: () => updateSEORecordSchema,
52
+ useGenerateMetadata: () => useGenerateMetadata,
53
+ validateCrawlability: () => validateCrawlability,
54
+ validateHTML: () => validateHTML,
55
+ validateOGImage: () => validateOGImage,
56
+ validatePublicAccess: () => validatePublicAccess,
57
+ validateRobotsTxt: () => validateRobotsTxt,
58
+ validateURL: () => validateURL
59
+ });
60
+ module.exports = __toCommonJS(server_exports);
61
+
62
+ // src/lib/supabase/server.ts
63
+ var import_ssr = require("@supabase/ssr");
64
+ var import_headers = require("next/headers");
65
+ async function createClient() {
66
+ const cookieStore = await (0, import_headers.cookies)();
67
+ return (0, import_ssr.createServerClient)(
68
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
69
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
70
+ {
71
+ cookies: {
72
+ getAll() {
73
+ return cookieStore.getAll();
74
+ },
75
+ setAll(cookiesToSet) {
76
+ try {
77
+ cookiesToSet.forEach(
78
+ ({ name, value, options }) => cookieStore.set(name, value, options)
79
+ );
80
+ } catch {
81
+ }
82
+ }
83
+ }
84
+ }
85
+ );
86
+ }
87
+
88
+ // src/lib/database/seo-records.ts
89
+ function transformRowToSEORecord(row) {
90
+ return {
91
+ id: row.id,
92
+ userId: row.user_id,
93
+ routePath: row.route_path,
94
+ title: row.title ?? void 0,
95
+ description: row.description ?? void 0,
96
+ keywords: row.keywords ?? void 0,
97
+ ogTitle: row.og_title ?? void 0,
98
+ ogDescription: row.og_description ?? void 0,
99
+ ogImageUrl: row.og_image_url ?? void 0,
100
+ ogImageWidth: row.og_image_width ?? void 0,
101
+ ogImageHeight: row.og_image_height ?? void 0,
102
+ ogType: row.og_type ?? void 0,
103
+ ogUrl: row.og_url ?? void 0,
104
+ ogSiteName: row.og_site_name ?? void 0,
105
+ twitterCard: row.twitter_card ?? void 0,
106
+ twitterTitle: row.twitter_title ?? void 0,
107
+ twitterDescription: row.twitter_description ?? void 0,
108
+ twitterImageUrl: row.twitter_image_url ?? void 0,
109
+ twitterSite: row.twitter_site ?? void 0,
110
+ twitterCreator: row.twitter_creator ?? void 0,
111
+ canonicalUrl: row.canonical_url ?? void 0,
112
+ robots: row.robots ?? void 0,
113
+ author: row.author ?? void 0,
114
+ publishedTime: row.published_time ? new Date(row.published_time) : void 0,
115
+ modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
116
+ structuredData: row.structured_data ? row.structured_data : void 0,
117
+ validationStatus: row.validation_status ?? void 0,
118
+ lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
119
+ validationErrors: row.validation_errors ? row.validation_errors : void 0,
120
+ createdAt: new Date(row.created_at),
121
+ updatedAt: new Date(row.updated_at)
122
+ };
123
+ }
124
+ function transformToInsert(record) {
125
+ return {
126
+ user_id: record.userId,
127
+ route_path: record.routePath,
128
+ title: record.title ?? null,
129
+ description: record.description ?? null,
130
+ keywords: record.keywords ?? null,
131
+ og_title: record.ogTitle ?? null,
132
+ og_description: record.ogDescription ?? null,
133
+ og_image_url: record.ogImageUrl ?? null,
134
+ og_image_width: record.ogImageWidth ?? null,
135
+ og_image_height: record.ogImageHeight ?? null,
136
+ og_type: record.ogType ?? null,
137
+ og_url: record.ogUrl ?? null,
138
+ og_site_name: record.ogSiteName ?? null,
139
+ twitter_card: record.twitterCard ?? null,
140
+ twitter_title: record.twitterTitle ?? null,
141
+ twitter_description: record.twitterDescription ?? null,
142
+ twitter_image_url: record.twitterImageUrl ?? null,
143
+ twitter_site: record.twitterSite ?? null,
144
+ twitter_creator: record.twitterCreator ?? null,
145
+ canonical_url: record.canonicalUrl ?? null,
146
+ robots: record.robots ?? null,
147
+ author: record.author ?? null,
148
+ published_time: record.publishedTime?.toISOString() ?? null,
149
+ modified_time: record.modifiedTime?.toISOString() ?? null,
150
+ structured_data: record.structuredData ?? null
151
+ };
152
+ }
153
+ function transformToUpdate(record) {
154
+ const update = {};
155
+ if (record.routePath !== void 0) update.route_path = record.routePath;
156
+ if (record.title !== void 0) update.title = record.title ?? null;
157
+ if (record.description !== void 0)
158
+ update.description = record.description ?? null;
159
+ if (record.keywords !== void 0) update.keywords = record.keywords ?? null;
160
+ if (record.ogTitle !== void 0) update.og_title = record.ogTitle ?? null;
161
+ if (record.ogDescription !== void 0)
162
+ update.og_description = record.ogDescription ?? null;
163
+ if (record.ogImageUrl !== void 0)
164
+ update.og_image_url = record.ogImageUrl ?? null;
165
+ if (record.ogImageWidth !== void 0)
166
+ update.og_image_width = record.ogImageWidth ?? null;
167
+ if (record.ogImageHeight !== void 0)
168
+ update.og_image_height = record.ogImageHeight ?? null;
169
+ if (record.ogType !== void 0) update.og_type = record.ogType ?? null;
170
+ if (record.ogUrl !== void 0) update.og_url = record.ogUrl ?? null;
171
+ if (record.ogSiteName !== void 0)
172
+ update.og_site_name = record.ogSiteName ?? null;
173
+ if (record.twitterCard !== void 0)
174
+ update.twitter_card = record.twitterCard ?? null;
175
+ if (record.twitterTitle !== void 0)
176
+ update.twitter_title = record.twitterTitle ?? null;
177
+ if (record.twitterDescription !== void 0)
178
+ update.twitter_description = record.twitterDescription ?? null;
179
+ if (record.twitterImageUrl !== void 0)
180
+ update.twitter_image_url = record.twitterImageUrl ?? null;
181
+ if (record.twitterSite !== void 0)
182
+ update.twitter_site = record.twitterSite ?? null;
183
+ if (record.twitterCreator !== void 0)
184
+ update.twitter_creator = record.twitterCreator ?? null;
185
+ if (record.canonicalUrl !== void 0)
186
+ update.canonical_url = record.canonicalUrl ?? null;
187
+ if (record.robots !== void 0) update.robots = record.robots ?? null;
188
+ if (record.author !== void 0) update.author = record.author ?? null;
189
+ if (record.publishedTime !== void 0)
190
+ update.published_time = record.publishedTime?.toISOString() ?? null;
191
+ if (record.modifiedTime !== void 0)
192
+ update.modified_time = record.modifiedTime?.toISOString() ?? null;
193
+ if (record.structuredData !== void 0)
194
+ update.structured_data = record.structuredData ?? null;
195
+ if (record.validationStatus !== void 0)
196
+ update.validation_status = record.validationStatus ?? null;
197
+ if (record.lastValidatedAt !== void 0)
198
+ update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;
199
+ if (record.validationErrors !== void 0)
200
+ update.validation_errors = record.validationErrors ?? null;
201
+ return update;
202
+ }
203
+ async function getSEORecords() {
204
+ try {
205
+ const supabase = await createClient();
206
+ const {
207
+ data: { user }
208
+ } = await supabase.auth.getUser();
209
+ if (!user) {
210
+ return {
211
+ success: false,
212
+ error: new Error("User not authenticated")
213
+ };
214
+ }
215
+ const { data, error } = await supabase.from("seo_records").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
216
+ if (error) {
217
+ return { success: false, error };
218
+ }
219
+ const records = (data || []).map(transformRowToSEORecord);
220
+ return { success: true, data: records };
221
+ } catch (error) {
222
+ return {
223
+ success: false,
224
+ error: error instanceof Error ? error : new Error("Unknown error")
225
+ };
226
+ }
227
+ }
228
+ async function getSEORecordById(id) {
229
+ try {
230
+ const supabase = await createClient();
231
+ const {
232
+ data: { user }
233
+ } = await supabase.auth.getUser();
234
+ if (!user) {
235
+ return {
236
+ success: false,
237
+ error: new Error("User not authenticated")
238
+ };
239
+ }
240
+ const { data, error } = await supabase.from("seo_records").select("*").eq("id", id).eq("user_id", user.id).single();
241
+ if (error) {
242
+ return { success: false, error };
243
+ }
244
+ if (!data) {
245
+ return {
246
+ success: false,
247
+ error: new Error("SEO record not found")
248
+ };
249
+ }
250
+ return { success: true, data: transformRowToSEORecord(data) };
251
+ } catch (error) {
252
+ return {
253
+ success: false,
254
+ error: error instanceof Error ? error : new Error("Unknown error")
255
+ };
256
+ }
257
+ }
258
+ async function getSEORecordByRoute(routePath) {
259
+ try {
260
+ const supabase = await createClient();
261
+ const {
262
+ data: { user }
263
+ } = await supabase.auth.getUser();
264
+ if (!user) {
265
+ return {
266
+ success: false,
267
+ error: new Error("User not authenticated")
268
+ };
269
+ }
270
+ const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
271
+ if (error) {
272
+ return { success: false, error };
273
+ }
274
+ if (!data) {
275
+ return { success: true, data: null };
276
+ }
277
+ return { success: true, data: transformRowToSEORecord(data) };
278
+ } catch (error) {
279
+ return {
280
+ success: false,
281
+ error: error instanceof Error ? error : new Error("Unknown error")
282
+ };
283
+ }
284
+ }
285
+ async function createSEORecord(record) {
286
+ try {
287
+ const supabase = await createClient();
288
+ const {
289
+ data: { user }
290
+ } = await supabase.auth.getUser();
291
+ if (!user) {
292
+ return {
293
+ success: false,
294
+ error: new Error("User not authenticated")
295
+ };
296
+ }
297
+ const insertData = transformToInsert({ ...record, userId: user.id });
298
+ const { data, error } = await supabase.from("seo_records").insert(insertData).select().single();
299
+ if (error) {
300
+ return { success: false, error };
301
+ }
302
+ return { success: true, data: transformRowToSEORecord(data) };
303
+ } catch (error) {
304
+ return {
305
+ success: false,
306
+ error: error instanceof Error ? error : new Error("Unknown error")
307
+ };
308
+ }
309
+ }
310
+ async function updateSEORecord(record) {
311
+ try {
312
+ const supabase = await createClient();
313
+ const {
314
+ data: { user }
315
+ } = await supabase.auth.getUser();
316
+ if (!user) {
317
+ return {
318
+ success: false,
319
+ error: new Error("User not authenticated")
320
+ };
321
+ }
322
+ const { id, ...updateData } = record;
323
+ const transformedUpdate = transformToUpdate(updateData);
324
+ const { data, error } = await supabase.from("seo_records").update(transformedUpdate).eq("id", id).eq("user_id", user.id).select().single();
325
+ if (error) {
326
+ return { success: false, error };
327
+ }
328
+ if (!data) {
329
+ return {
330
+ success: false,
331
+ error: new Error("SEO record not found")
332
+ };
333
+ }
334
+ return { success: true, data: transformRowToSEORecord(data) };
335
+ } catch (error) {
336
+ return {
337
+ success: false,
338
+ error: error instanceof Error ? error : new Error("Unknown error")
339
+ };
340
+ }
341
+ }
342
+ async function deleteSEORecord(id) {
343
+ try {
344
+ const supabase = await createClient();
345
+ const {
346
+ data: { user }
347
+ } = await supabase.auth.getUser();
348
+ if (!user) {
349
+ return {
350
+ success: false,
351
+ error: new Error("User not authenticated")
352
+ };
353
+ }
354
+ const { error } = await supabase.from("seo_records").delete().eq("id", id).eq("user_id", user.id);
355
+ if (error) {
356
+ return { success: false, error };
357
+ }
358
+ return { success: true, data: void 0 };
359
+ } catch (error) {
360
+ return {
361
+ success: false,
362
+ error: error instanceof Error ? error : new Error("Unknown error")
363
+ };
364
+ }
365
+ }
366
+
367
+ // src/lib/validation/seo-schema.ts
368
+ var import_zod = require("zod");
369
+ var ogTypeSchema = import_zod.z.enum([
370
+ "website",
371
+ "article",
372
+ "product",
373
+ "book",
374
+ "profile",
375
+ "music",
376
+ "video"
377
+ ]);
378
+ var twitterCardSchema = import_zod.z.enum([
379
+ "summary",
380
+ "summary_large_image",
381
+ "app",
382
+ "player"
383
+ ]);
384
+ var validationStatusSchema = import_zod.z.enum([
385
+ "pending",
386
+ "valid",
387
+ "invalid",
388
+ "warning"
389
+ ]);
390
+ var seoMetadataSchema = import_zod.z.object({
391
+ // Basic metadata
392
+ title: import_zod.z.string().max(60, "Title must be 60 characters or less").optional(),
393
+ description: import_zod.z.string().max(160, "Description must be 160 characters or less").optional(),
394
+ keywords: import_zod.z.array(import_zod.z.string()).optional(),
395
+ // Open Graph
396
+ ogTitle: import_zod.z.string().max(60).optional(),
397
+ ogDescription: import_zod.z.string().max(200).optional(),
398
+ ogImageUrl: import_zod.z.string().url("Must be a valid URL").optional(),
399
+ ogImageWidth: import_zod.z.number().int().positive().max(1200).optional(),
400
+ ogImageHeight: import_zod.z.number().int().positive().max(1200).optional(),
401
+ ogType: ogTypeSchema.optional(),
402
+ ogUrl: import_zod.z.string().url("Must be a valid URL").optional(),
403
+ ogSiteName: import_zod.z.string().optional(),
404
+ // Twitter Card
405
+ twitterCard: twitterCardSchema.optional(),
406
+ twitterTitle: import_zod.z.string().max(70).optional(),
407
+ twitterDescription: import_zod.z.string().max(200).optional(),
408
+ twitterImageUrl: import_zod.z.string().url("Must be a valid URL").optional(),
409
+ twitterSite: import_zod.z.string().optional(),
410
+ twitterCreator: import_zod.z.string().optional(),
411
+ // Additional metadata
412
+ canonicalUrl: import_zod.z.string().url("Must be a valid URL").optional(),
413
+ robots: import_zod.z.string().optional(),
414
+ author: import_zod.z.string().optional(),
415
+ publishedTime: import_zod.z.coerce.date().optional(),
416
+ modifiedTime: import_zod.z.coerce.date().optional(),
417
+ // Structured data
418
+ structuredData: import_zod.z.record(import_zod.z.unknown()).optional()
419
+ });
420
+ var seoRecordSchema = seoMetadataSchema.extend({
421
+ id: import_zod.z.string().uuid().optional(),
422
+ userId: import_zod.z.string().uuid(),
423
+ routePath: import_zod.z.string().min(1, "Route path is required").regex(/^\/.*/, "Route path must start with /"),
424
+ validationStatus: validationStatusSchema.optional(),
425
+ lastValidatedAt: import_zod.z.coerce.date().optional(),
426
+ validationErrors: import_zod.z.record(import_zod.z.unknown()).optional(),
427
+ createdAt: import_zod.z.coerce.date().optional(),
428
+ updatedAt: import_zod.z.coerce.date().optional()
429
+ });
430
+ var createSEORecordSchema = seoRecordSchema.omit({
431
+ id: true,
432
+ validationStatus: true,
433
+ lastValidatedAt: true,
434
+ validationErrors: true,
435
+ createdAt: true,
436
+ updatedAt: true
437
+ });
438
+ var updateSEORecordSchema = seoRecordSchema.partial().required({ id: true }).omit({
439
+ userId: true,
440
+ createdAt: true
441
+ });
442
+
443
+ // src/lib/validation/image-validator.ts
444
+ var import_sharp = __toESM(require("sharp"));
445
+ async function validateOGImage(imageUrl, expectedWidth, expectedHeight) {
446
+ const issues = [];
447
+ try {
448
+ const response = await fetch(imageUrl, {
449
+ headers: {
450
+ "User-Agent": "Mozilla/5.0 (compatible; SEO-Console/1.0; +https://example.com/bot)"
451
+ },
452
+ signal: AbortSignal.timeout(15e3)
453
+ // 15 second timeout for images
454
+ });
455
+ if (!response.ok) {
456
+ return {
457
+ isValid: false,
458
+ issues: [
459
+ {
460
+ field: "image",
461
+ severity: "critical",
462
+ message: `Failed to fetch image: ${response.status} ${response.statusText}`,
463
+ actual: imageUrl
464
+ }
465
+ ]
466
+ };
467
+ }
468
+ const imageBuffer = await response.arrayBuffer();
469
+ const buffer = Buffer.from(imageBuffer);
470
+ const sizeInMB = buffer.length / (1024 * 1024);
471
+ if (sizeInMB > 1) {
472
+ issues.push({
473
+ field: "image",
474
+ severity: "warning",
475
+ message: "Image file size exceeds 1MB recommendation",
476
+ actual: `${sizeInMB.toFixed(2)}MB`
477
+ });
478
+ }
479
+ const metadata = await (0, import_sharp.default)(buffer).metadata();
480
+ const { width, height, format } = metadata;
481
+ if (!width || !height) {
482
+ return {
483
+ isValid: false,
484
+ issues: [
485
+ {
486
+ field: "image",
487
+ severity: "critical",
488
+ message: "Could not determine image dimensions",
489
+ actual: imageUrl
490
+ }
491
+ ]
492
+ };
493
+ }
494
+ const recommendedWidth = 1200;
495
+ const recommendedHeight = 630;
496
+ const aspectRatio = width / height;
497
+ const recommendedAspectRatio = recommendedWidth / recommendedHeight;
498
+ if (width < recommendedWidth || height < recommendedHeight) {
499
+ issues.push({
500
+ field: "image",
501
+ severity: "warning",
502
+ message: `Image dimensions below recommended size (${recommendedWidth}x${recommendedHeight})`,
503
+ expected: `${recommendedWidth}x${recommendedHeight}`,
504
+ actual: `${width}x${height}`
505
+ });
506
+ }
507
+ if (Math.abs(aspectRatio - recommendedAspectRatio) > 0.1) {
508
+ issues.push({
509
+ field: "image",
510
+ severity: "info",
511
+ message: "Image aspect ratio differs from recommended 1.91:1",
512
+ expected: "1.91:1",
513
+ actual: `${aspectRatio.toFixed(2)}:1`
514
+ });
515
+ }
516
+ const supportedFormats = ["jpeg", "jpg", "png", "webp", "avif", "gif"];
517
+ if (!format || !supportedFormats.includes(format.toLowerCase())) {
518
+ issues.push({
519
+ field: "image",
520
+ severity: "warning",
521
+ message: "Image format may not be optimal for social sharing",
522
+ expected: "JPEG, PNG, WebP, or AVIF",
523
+ actual: format || "unknown"
524
+ });
525
+ }
526
+ if (expectedWidth && width !== expectedWidth) {
527
+ issues.push({
528
+ field: "image",
529
+ severity: "warning",
530
+ message: "Image width does not match expected value",
531
+ expected: `${expectedWidth}px`,
532
+ actual: `${width}px`
533
+ });
534
+ }
535
+ if (expectedHeight && height !== expectedHeight) {
536
+ issues.push({
537
+ field: "image",
538
+ severity: "warning",
539
+ message: "Image height does not match expected value",
540
+ expected: `${expectedHeight}px`,
541
+ actual: `${height}px`
542
+ });
543
+ }
544
+ return {
545
+ isValid: issues.filter((i) => i.severity === "critical").length === 0,
546
+ issues,
547
+ metadata: {
548
+ width,
549
+ height,
550
+ format: format || "unknown",
551
+ size: buffer.length
552
+ }
553
+ };
554
+ } catch (error) {
555
+ return {
556
+ isValid: false,
557
+ issues: [
558
+ {
559
+ field: "image",
560
+ severity: "critical",
561
+ message: error instanceof Error ? error.message : "Failed to validate image",
562
+ actual: imageUrl
563
+ }
564
+ ]
565
+ };
566
+ }
567
+ }
568
+
569
+ // src/lib/validation/html-validator.ts
570
+ var cheerio = __toESM(require("cheerio"));
571
+ async function validateHTML(html, record, _baseUrl) {
572
+ const issues = [];
573
+ const $ = cheerio.load(html);
574
+ const title = $("title").text().trim();
575
+ if (record.title) {
576
+ if (!title) {
577
+ issues.push({
578
+ field: "title",
579
+ severity: "critical",
580
+ message: "Title tag is missing",
581
+ expected: record.title
582
+ });
583
+ } else if (title !== record.title) {
584
+ issues.push({
585
+ field: "title",
586
+ severity: "warning",
587
+ message: "Title tag does not match SEO record",
588
+ expected: record.title,
589
+ actual: title
590
+ });
591
+ }
592
+ if (title.length > 60) {
593
+ issues.push({
594
+ field: "title",
595
+ severity: "warning",
596
+ message: "Title exceeds recommended 60 characters",
597
+ actual: `${title.length} characters`
598
+ });
599
+ }
600
+ }
601
+ const metaDescription = $('meta[name="description"]').attr("content")?.trim();
602
+ if (record.description) {
603
+ if (!metaDescription) {
604
+ issues.push({
605
+ field: "description",
606
+ severity: "critical",
607
+ message: "Meta description is missing",
608
+ expected: record.description
609
+ });
610
+ } else if (metaDescription !== record.description) {
611
+ issues.push({
612
+ field: "description",
613
+ severity: "warning",
614
+ message: "Meta description does not match SEO record",
615
+ expected: record.description,
616
+ actual: metaDescription
617
+ });
618
+ }
619
+ if (metaDescription && metaDescription.length > 160) {
620
+ issues.push({
621
+ field: "description",
622
+ severity: "warning",
623
+ message: "Description exceeds recommended 160 characters",
624
+ actual: `${metaDescription.length} characters`
625
+ });
626
+ }
627
+ }
628
+ if (record.ogTitle || record.ogDescription || record.ogImageUrl) {
629
+ const ogTitle = $('meta[property="og:title"]').attr("content");
630
+ const ogDescription = $('meta[property="og:description"]').attr("content");
631
+ const ogImage = $('meta[property="og:image"]').attr("content");
632
+ const ogType = $('meta[property="og:type"]').attr("content");
633
+ const ogUrl = $('meta[property="og:url"]').attr("content");
634
+ if (record.ogTitle && !ogTitle) {
635
+ issues.push({
636
+ field: "og:title",
637
+ severity: "critical",
638
+ message: "Open Graph title is missing",
639
+ expected: record.ogTitle
640
+ });
641
+ }
642
+ if (record.ogDescription && !ogDescription) {
643
+ issues.push({
644
+ field: "og:description",
645
+ severity: "warning",
646
+ message: "Open Graph description is missing",
647
+ expected: record.ogDescription
648
+ });
649
+ }
650
+ if (record.ogImageUrl && !ogImage) {
651
+ issues.push({
652
+ field: "og:image",
653
+ severity: "critical",
654
+ message: "Open Graph image is missing",
655
+ expected: record.ogImageUrl
656
+ });
657
+ }
658
+ if (record.ogType && ogType !== record.ogType) {
659
+ issues.push({
660
+ field: "og:type",
661
+ severity: "warning",
662
+ message: "Open Graph type does not match",
663
+ expected: record.ogType,
664
+ actual: ogType
665
+ });
666
+ }
667
+ if (record.ogUrl && ogUrl !== record.ogUrl) {
668
+ issues.push({
669
+ field: "og:url",
670
+ severity: "warning",
671
+ message: "Open Graph URL does not match",
672
+ expected: record.ogUrl,
673
+ actual: ogUrl
674
+ });
675
+ }
676
+ }
677
+ if (record.twitterCard || record.twitterTitle || record.twitterImageUrl) {
678
+ const twitterCard = $('meta[name="twitter:card"]').attr("content");
679
+ const twitterTitle = $('meta[name="twitter:title"]').attr("content");
680
+ const _twitterDescription = $('meta[name="twitter:description"]').attr("content");
681
+ const twitterImage = $('meta[name="twitter:image"]').attr("content");
682
+ if (record.twitterCard && twitterCard !== record.twitterCard) {
683
+ issues.push({
684
+ field: "twitter:card",
685
+ severity: "warning",
686
+ message: "Twitter card type does not match",
687
+ expected: record.twitterCard,
688
+ actual: twitterCard
689
+ });
690
+ }
691
+ if (record.twitterTitle && !twitterTitle) {
692
+ issues.push({
693
+ field: "twitter:title",
694
+ severity: "warning",
695
+ message: "Twitter title is missing",
696
+ expected: record.twitterTitle
697
+ });
698
+ }
699
+ if (record.twitterImageUrl && !twitterImage) {
700
+ issues.push({
701
+ field: "twitter:image",
702
+ severity: "warning",
703
+ message: "Twitter image is missing",
704
+ expected: record.twitterImageUrl
705
+ });
706
+ }
707
+ }
708
+ if (record.canonicalUrl) {
709
+ const canonical = $('link[rel="canonical"]').attr("href");
710
+ if (!canonical) {
711
+ issues.push({
712
+ field: "canonical",
713
+ severity: "critical",
714
+ message: "Canonical URL is missing",
715
+ expected: record.canonicalUrl
716
+ });
717
+ } else if (canonical !== record.canonicalUrl) {
718
+ issues.push({
719
+ field: "canonical",
720
+ severity: "warning",
721
+ message: "Canonical URL does not match",
722
+ expected: record.canonicalUrl,
723
+ actual: canonical
724
+ });
725
+ }
726
+ if (canonical && !canonical.startsWith("http://") && !canonical.startsWith("https://")) {
727
+ issues.push({
728
+ field: "canonical",
729
+ severity: "warning",
730
+ message: "Canonical URL should be absolute",
731
+ actual: canonical
732
+ });
733
+ }
734
+ }
735
+ const robotsMeta = $('meta[name="robots"]').attr("content");
736
+ if (robotsMeta) {
737
+ const robots = robotsMeta.toLowerCase();
738
+ if (robots.includes("noindex")) {
739
+ issues.push({
740
+ field: "robots",
741
+ severity: "critical",
742
+ message: "Page has noindex meta tag - will NOT be indexed by search engines",
743
+ actual: robotsMeta
744
+ });
745
+ }
746
+ if (robots.includes("nofollow")) {
747
+ issues.push({
748
+ field: "robots",
749
+ severity: "warning",
750
+ message: "Page has nofollow meta tag - links won't be followed",
751
+ actual: robotsMeta
752
+ });
753
+ }
754
+ }
755
+ return {
756
+ isValid: issues.filter((i) => i.severity === "critical").length === 0,
757
+ issues,
758
+ validatedAt: /* @__PURE__ */ new Date()
759
+ };
760
+ }
761
+ async function validateURL(url, record) {
762
+ try {
763
+ const response = await fetch(url, {
764
+ headers: {
765
+ "User-Agent": "Mozilla/5.0 (compatible; SEO-Console/1.0; +https://example.com/bot)"
766
+ },
767
+ // Timeout after 10 seconds
768
+ signal: AbortSignal.timeout(1e4)
769
+ });
770
+ if (!response.ok) {
771
+ return {
772
+ isValid: false,
773
+ issues: [
774
+ {
775
+ field: "fetch",
776
+ severity: "critical",
777
+ message: `Failed to fetch URL: ${response.status} ${response.statusText}`,
778
+ actual: url
779
+ }
780
+ ],
781
+ validatedAt: /* @__PURE__ */ new Date()
782
+ };
783
+ }
784
+ const html = await response.text();
785
+ return validateHTML(html, record, url);
786
+ } catch (error) {
787
+ return {
788
+ isValid: false,
789
+ issues: [
790
+ {
791
+ field: "fetch",
792
+ severity: "critical",
793
+ message: error instanceof Error ? error.message : "Failed to fetch URL",
794
+ actual: url
795
+ }
796
+ ],
797
+ validatedAt: /* @__PURE__ */ new Date()
798
+ };
799
+ }
800
+ }
801
+
802
+ // src/lib/storage/file-storage.ts
803
+ var import_fs = require("fs");
804
+ var FileStorage = class {
805
+ constructor(filePath = "seo-records.json") {
806
+ this.records = [];
807
+ this.initialized = false;
808
+ this.filePath = filePath;
809
+ }
810
+ async ensureInitialized() {
811
+ if (this.initialized) return;
812
+ try {
813
+ const data = await import_fs.promises.readFile(this.filePath, "utf-8");
814
+ this.records = JSON.parse(data);
815
+ } catch (error) {
816
+ if (error.code === "ENOENT") {
817
+ this.records = [];
818
+ await this.save();
819
+ } else {
820
+ throw error;
821
+ }
822
+ }
823
+ this.initialized = true;
824
+ }
825
+ async save() {
826
+ await import_fs.promises.writeFile(this.filePath, JSON.stringify(this.records, null, 2), "utf-8");
827
+ }
828
+ async isAvailable() {
829
+ try {
830
+ const dir = this.filePath.includes("/") ? this.filePath.substring(0, this.filePath.lastIndexOf("/")) : ".";
831
+ await import_fs.promises.access(dir);
832
+ return true;
833
+ } catch {
834
+ return false;
835
+ }
836
+ }
837
+ async getRecords() {
838
+ await this.ensureInitialized();
839
+ return [...this.records];
840
+ }
841
+ async getRecordById(id) {
842
+ await this.ensureInitialized();
843
+ return this.records.find((r) => r.id === id) || null;
844
+ }
845
+ async getRecordByRoute(routePath) {
846
+ await this.ensureInitialized();
847
+ return this.records.find((r) => r.routePath === routePath) || null;
848
+ }
849
+ async createRecord(record) {
850
+ await this.ensureInitialized();
851
+ const newRecord = {
852
+ id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
853
+ userId: "file-user",
854
+ // File storage doesn't need user IDs
855
+ routePath: record.routePath,
856
+ title: record.title,
857
+ description: record.description,
858
+ keywords: record.keywords,
859
+ ogTitle: record.ogTitle,
860
+ ogDescription: record.ogDescription,
861
+ ogImageUrl: record.ogImageUrl,
862
+ ogImageWidth: record.ogImageWidth,
863
+ ogImageHeight: record.ogImageHeight,
864
+ ogType: record.ogType,
865
+ ogUrl: record.ogUrl,
866
+ ogSiteName: record.ogSiteName,
867
+ twitterCard: record.twitterCard,
868
+ twitterTitle: record.twitterTitle,
869
+ twitterDescription: record.twitterDescription,
870
+ twitterImageUrl: record.twitterImageUrl,
871
+ twitterSite: record.twitterSite,
872
+ twitterCreator: record.twitterCreator,
873
+ canonicalUrl: record.canonicalUrl,
874
+ robots: record.robots,
875
+ author: record.author,
876
+ publishedTime: record.publishedTime,
877
+ modifiedTime: record.modifiedTime,
878
+ structuredData: record.structuredData,
879
+ validationStatus: "pending",
880
+ lastValidatedAt: void 0,
881
+ validationErrors: void 0
882
+ };
883
+ this.records.push(newRecord);
884
+ await this.save();
885
+ return newRecord;
886
+ }
887
+ async updateRecord(record) {
888
+ await this.ensureInitialized();
889
+ const index = this.records.findIndex((r) => r.id === record.id);
890
+ if (index === -1) {
891
+ throw new Error(`SEO record with id ${record.id} not found`);
892
+ }
893
+ const updated = {
894
+ ...this.records[index],
895
+ ...record
896
+ };
897
+ this.records[index] = updated;
898
+ await this.save();
899
+ return updated;
900
+ }
901
+ async deleteRecord(id) {
902
+ await this.ensureInitialized();
903
+ const index = this.records.findIndex((r) => r.id === id);
904
+ if (index === -1) {
905
+ throw new Error(`SEO record with id ${id} not found`);
906
+ }
907
+ this.records.splice(index, 1);
908
+ await this.save();
909
+ }
910
+ };
911
+
912
+ // src/lib/storage/supabase-storage.ts
913
+ var SupabaseStorage = class {
914
+ constructor(supabaseUrl, supabaseKey) {
915
+ this.supabaseUrl = supabaseUrl;
916
+ this.supabaseKey = supabaseKey;
917
+ if (typeof process !== "undefined") {
918
+ process.env.NEXT_PUBLIC_SUPABASE_URL = supabaseUrl;
919
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = supabaseKey;
920
+ }
921
+ }
922
+ async isAvailable() {
923
+ try {
924
+ const result = await getSEORecords();
925
+ return result.success;
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+ async getRecords() {
931
+ const result = await getSEORecords();
932
+ if (!result.success) {
933
+ throw new Error(result.error?.message || "Failed to get records");
934
+ }
935
+ return result.data;
936
+ }
937
+ async getRecordById(id) {
938
+ const result = await getSEORecordById(id);
939
+ if (!result.success) {
940
+ if (result.error?.message?.includes("not found")) {
941
+ return null;
942
+ }
943
+ throw new Error(result.error?.message || "Failed to get record");
944
+ }
945
+ return result.data || null;
946
+ }
947
+ async getRecordByRoute(routePath) {
948
+ const result = await getSEORecordByRoute(routePath);
949
+ if (!result.success) {
950
+ if (result.error?.message?.includes("not found")) {
951
+ return null;
952
+ }
953
+ throw new Error(result.error?.message || "Failed to get record");
954
+ }
955
+ return result.data || null;
956
+ }
957
+ async createRecord(record) {
958
+ const result = await createSEORecord(record);
959
+ if (!result.success) {
960
+ throw new Error(result.error?.message || "Failed to create record");
961
+ }
962
+ return result.data;
963
+ }
964
+ async updateRecord(record) {
965
+ const result = await updateSEORecord(record);
966
+ if (!result.success) {
967
+ throw new Error(result.error?.message || "Failed to update record");
968
+ }
969
+ return result.data;
970
+ }
971
+ async deleteRecord(id) {
972
+ const result = await deleteSEORecord(id);
973
+ if (!result.success) {
974
+ throw new Error(result.error?.message || "Failed to delete record");
975
+ }
976
+ }
977
+ };
978
+
979
+ // src/lib/storage/storage-factory.ts
980
+ function createStorageAdapter(config) {
981
+ switch (config.type) {
982
+ case "file":
983
+ return new FileStorage(config.filePath || "seo-records.json");
984
+ case "supabase":
985
+ if (!config.supabaseUrl || !config.supabaseKey) {
986
+ throw new Error("Supabase URL and key are required for Supabase storage");
987
+ }
988
+ return new SupabaseStorage(config.supabaseUrl, config.supabaseKey);
989
+ case "memory":
990
+ return new FileStorage(":memory:");
991
+ default:
992
+ throw new Error(`Unsupported storage type: ${config.type}`);
993
+ }
994
+ }
995
+ function detectStorageConfig() {
996
+ if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
997
+ return {
998
+ type: "supabase",
999
+ supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
1000
+ supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
1001
+ };
1002
+ }
1003
+ if (process.env.SEO_CONSOLE_STORAGE_PATH) {
1004
+ return {
1005
+ type: "file",
1006
+ filePath: process.env.SEO_CONSOLE_STORAGE_PATH
1007
+ };
1008
+ }
1009
+ return {
1010
+ type: "file",
1011
+ filePath: "seo-records.json"
1012
+ };
1013
+ }
1014
+
1015
+ // src/hooks/useGenerateMetadata.ts
1016
+ async function useGenerateMetadata(options = {}) {
1017
+ const { routePath, fallback = {} } = options;
1018
+ if (!routePath) {
1019
+ return {
1020
+ title: fallback.title,
1021
+ description: fallback.description,
1022
+ ...fallback
1023
+ };
1024
+ }
1025
+ let record = null;
1026
+ try {
1027
+ const storageConfig = detectStorageConfig();
1028
+ if (storageConfig.type === "file" || storageConfig.type === "memory") {
1029
+ const storage = createStorageAdapter(storageConfig);
1030
+ record = await storage.getRecordByRoute(routePath);
1031
+ } else {
1032
+ const result = await getSEORecordByRoute(routePath);
1033
+ record = result.success ? result.data || null : null;
1034
+ }
1035
+ } catch (error) {
1036
+ const result = await getSEORecordByRoute(routePath);
1037
+ record = result.success ? result.data || null : null;
1038
+ }
1039
+ if (!record) {
1040
+ return {
1041
+ title: fallback.title,
1042
+ description: fallback.description,
1043
+ ...fallback
1044
+ };
1045
+ }
1046
+ const metadata = {};
1047
+ if (record.title) {
1048
+ metadata.title = record.title;
1049
+ }
1050
+ if (record.description) {
1051
+ metadata.description = record.description;
1052
+ }
1053
+ if (record.keywords && record.keywords.length > 0) {
1054
+ metadata.keywords = record.keywords;
1055
+ }
1056
+ if (record.author) {
1057
+ metadata.authors = [{ name: record.author }];
1058
+ }
1059
+ if (record.ogTitle || record.ogDescription || record.ogImageUrl || record.ogType) {
1060
+ const supportedOGTypes = ["website", "article", "book", "profile"];
1061
+ const ogType = record.ogType && supportedOGTypes.includes(record.ogType) ? record.ogType : "website";
1062
+ const openGraph = {
1063
+ type: ogType,
1064
+ title: record.ogTitle || record.title || void 0,
1065
+ description: record.ogDescription || record.description || void 0,
1066
+ url: record.ogUrl || void 0,
1067
+ siteName: record.ogSiteName || void 0
1068
+ };
1069
+ if (record.ogImageUrl) {
1070
+ openGraph.images = [
1071
+ {
1072
+ url: record.ogImageUrl,
1073
+ width: record.ogImageWidth || void 0,
1074
+ height: record.ogImageHeight || void 0,
1075
+ alt: record.ogTitle || record.title || void 0
1076
+ }
1077
+ ];
1078
+ }
1079
+ if (ogType === "article") {
1080
+ const articleOpenGraph = {
1081
+ ...openGraph,
1082
+ ...record.publishedTime && {
1083
+ publishedTime: record.publishedTime.toISOString()
1084
+ },
1085
+ ...record.modifiedTime && {
1086
+ modifiedTime: record.modifiedTime.toISOString()
1087
+ }
1088
+ };
1089
+ metadata.openGraph = articleOpenGraph;
1090
+ } else {
1091
+ metadata.openGraph = openGraph;
1092
+ }
1093
+ }
1094
+ if (record.twitterCard || record.twitterTitle || record.twitterDescription || record.twitterImageUrl) {
1095
+ metadata.twitter = {
1096
+ card: record.twitterCard || "summary",
1097
+ title: record.twitterTitle || record.ogTitle || record.title || void 0,
1098
+ description: record.twitterDescription || record.ogDescription || record.description || void 0,
1099
+ images: record.twitterImageUrl ? [record.twitterImageUrl] : void 0,
1100
+ site: record.twitterSite || void 0,
1101
+ creator: record.twitterCreator || void 0
1102
+ };
1103
+ }
1104
+ if (record.canonicalUrl) {
1105
+ metadata.alternates = {
1106
+ canonical: record.canonicalUrl
1107
+ };
1108
+ }
1109
+ if (record.robots) {
1110
+ metadata.robots = record.robots;
1111
+ }
1112
+ return {
1113
+ ...fallback,
1114
+ ...metadata,
1115
+ // Ensure title and description from record override fallback if present
1116
+ title: record.title || fallback.title,
1117
+ description: record.description || fallback.description,
1118
+ // Merge openGraph if both exist
1119
+ openGraph: fallback.openGraph ? { ...metadata.openGraph, ...fallback.openGraph } : metadata.openGraph,
1120
+ // Merge twitter if both exist
1121
+ twitter: fallback.twitter ? { ...metadata.twitter, ...fallback.twitter } : metadata.twitter
1122
+ };
1123
+ }
1124
+ function getRoutePathFromParams(params, pattern) {
1125
+ let routePath = pattern;
1126
+ for (const [key, value] of Object.entries(params)) {
1127
+ const paramValue = Array.isArray(value) ? value.join("/") : value;
1128
+ routePath = routePath.replace(`[${key}]`, paramValue);
1129
+ routePath = routePath.replace(`[...${key}]`, paramValue);
1130
+ }
1131
+ return routePath;
1132
+ }
1133
+
1134
+ // src/lib/sitemap-generator.ts
1135
+ function generateSitemapXML(options) {
1136
+ const { baseUrl, entries } = options;
1137
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
1138
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1139
+ ${entries.map((entry) => {
1140
+ const loc = entry.loc.startsWith("http") ? entry.loc : new URL(entry.loc, baseUrl).toString();
1141
+ return ` <url>
1142
+ <loc>${escapeXML(loc)}</loc>${entry.lastmod ? `
1143
+ <lastmod>${entry.lastmod}</lastmod>` : ""}${entry.changefreq ? `
1144
+ <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority !== void 0 ? `
1145
+ <priority>${entry.priority}</priority>` : ""}
1146
+ </url>`;
1147
+ }).join("\n")}
1148
+ </urlset>`;
1149
+ return xml;
1150
+ }
1151
+ function seoRecordsToSitemapEntries(records, baseUrl) {
1152
+ return records.filter((record) => {
1153
+ return record.canonicalUrl && record.canonicalUrl.trim() !== "";
1154
+ }).map((record) => {
1155
+ const entry = {
1156
+ loc: record.canonicalUrl
1157
+ };
1158
+ if (record.modifiedTime) {
1159
+ entry.lastmod = record.modifiedTime.toISOString().split("T")[0];
1160
+ } else if (record.lastValidatedAt) {
1161
+ entry.lastmod = record.lastValidatedAt.toISOString().split("T")[0];
1162
+ }
1163
+ if (record.routePath === "/") {
1164
+ entry.changefreq = "daily";
1165
+ entry.priority = 1;
1166
+ } else if (record.routePath.includes("/blog/") || record.routePath.includes("/posts/")) {
1167
+ entry.changefreq = "weekly";
1168
+ entry.priority = 0.8;
1169
+ } else {
1170
+ entry.changefreq = "monthly";
1171
+ entry.priority = 0.6;
1172
+ }
1173
+ return entry;
1174
+ }).sort((a, b) => {
1175
+ if (a.priority !== b.priority) {
1176
+ return (b.priority || 0) - (a.priority || 0);
1177
+ }
1178
+ return a.loc.localeCompare(b.loc);
1179
+ });
1180
+ }
1181
+ function generateSitemapFromRecords(records, baseUrl) {
1182
+ const entries = seoRecordsToSitemapEntries(records, baseUrl);
1183
+ return generateSitemapXML({ baseUrl, entries });
1184
+ }
1185
+ function escapeXML(str) {
1186
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1187
+ }
1188
+
1189
+ // src/lib/robots-generator.ts
1190
+ function generateRobotsTxt(options = {}) {
1191
+ const { userAgents = [], sitemapUrl, crawlDelay } = options;
1192
+ let content = "";
1193
+ if (userAgents.length === 0) {
1194
+ content += "User-agent: *\n";
1195
+ if (crawlDelay) {
1196
+ content += `Crawl-delay: ${crawlDelay}
1197
+ `;
1198
+ }
1199
+ content += "Allow: /\n";
1200
+ content += "\n";
1201
+ } else {
1202
+ for (const ua of userAgents) {
1203
+ content += `User-agent: ${ua.agent}
1204
+ `;
1205
+ if (crawlDelay) {
1206
+ content += `Crawl-delay: ${crawlDelay}
1207
+ `;
1208
+ }
1209
+ if (ua.allow) {
1210
+ for (const path of ua.allow) {
1211
+ content += `Allow: ${path}
1212
+ `;
1213
+ }
1214
+ }
1215
+ if (ua.disallow) {
1216
+ for (const path of ua.disallow) {
1217
+ content += `Disallow: ${path}
1218
+ `;
1219
+ }
1220
+ }
1221
+ content += "\n";
1222
+ }
1223
+ }
1224
+ if (sitemapUrl) {
1225
+ content += `Sitemap: ${sitemapUrl}
1226
+ `;
1227
+ }
1228
+ return content.trim();
1229
+ }
1230
+ function updateRobotsTxtWithSitemap(existingContent, sitemapUrl) {
1231
+ const sitemapRegex = /^Sitemap:\s*.+$/m;
1232
+ if (sitemapRegex.test(existingContent)) {
1233
+ return existingContent.replace(sitemapRegex, `Sitemap: ${sitemapUrl}`);
1234
+ }
1235
+ const trimmed = existingContent.trim();
1236
+ return trimmed ? `${trimmed}
1237
+
1238
+ Sitemap: ${sitemapUrl}` : `Sitemap: ${sitemapUrl}`;
1239
+ }
1240
+
1241
+ // src/lib/metadata-extractor.ts
1242
+ var cheerio2 = __toESM(require("cheerio"));
1243
+ function extractMetadataFromHTML(html, baseUrl) {
1244
+ const $ = cheerio2.load(html);
1245
+ const metadata = {};
1246
+ metadata.title = $("title").text() || void 0;
1247
+ metadata.description = $('meta[name="description"]').attr("content") || void 0;
1248
+ metadata.robots = $('meta[name="robots"]').attr("content") || void 0;
1249
+ const keywords = $('meta[name="keywords"]').attr("content");
1250
+ if (keywords) {
1251
+ metadata.keywords = keywords.split(",").map((k) => k.trim());
1252
+ }
1253
+ metadata.ogTitle = $('meta[property="og:title"]').attr("content") || void 0;
1254
+ metadata.ogDescription = $('meta[property="og:description"]').attr("content") || void 0;
1255
+ metadata.ogImageUrl = $('meta[property="og:image"]').attr("content") || void 0;
1256
+ metadata.ogType = $('meta[property="og:type"]').attr("content") || void 0;
1257
+ metadata.ogUrl = $('meta[property="og:url"]').attr("content") || void 0;
1258
+ metadata.canonicalUrl = $('link[rel="canonical"]').attr("href") || void 0;
1259
+ if (baseUrl) {
1260
+ if (metadata.ogImageUrl && !metadata.ogImageUrl.startsWith("http")) {
1261
+ metadata.ogImageUrl = new URL(metadata.ogImageUrl, baseUrl).toString();
1262
+ }
1263
+ if (metadata.canonicalUrl && !metadata.canonicalUrl.startsWith("http")) {
1264
+ metadata.canonicalUrl = new URL(metadata.canonicalUrl, baseUrl).toString();
1265
+ }
1266
+ }
1267
+ return metadata;
1268
+ }
1269
+ async function extractMetadataFromURL(url) {
1270
+ try {
1271
+ const response = await fetch(url, {
1272
+ headers: {
1273
+ "User-Agent": "SEO-Console/1.0"
1274
+ }
1275
+ });
1276
+ if (!response.ok) {
1277
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
1278
+ }
1279
+ const html = await response.text();
1280
+ return extractMetadataFromHTML(html, url);
1281
+ } catch (error) {
1282
+ console.error(`Error extracting metadata from ${url}:`, error);
1283
+ return {};
1284
+ }
1285
+ }
1286
+ function metadataToSEORecord(metadata, routePath, userId = "extracted") {
1287
+ return {
1288
+ userId,
1289
+ routePath,
1290
+ title: metadata.title,
1291
+ description: metadata.description,
1292
+ keywords: metadata.keywords,
1293
+ ogTitle: metadata.ogTitle,
1294
+ ogDescription: metadata.ogDescription,
1295
+ ogImageUrl: metadata.ogImageUrl,
1296
+ ogType: metadata.ogType,
1297
+ ogUrl: metadata.ogUrl,
1298
+ canonicalUrl: metadata.canonicalUrl,
1299
+ robots: metadata.robots,
1300
+ validationStatus: "pending"
1301
+ };
1302
+ }
1303
+ async function crawlSiteForSEO(baseUrl, routes) {
1304
+ const results = /* @__PURE__ */ new Map();
1305
+ for (const route of routes) {
1306
+ const url = new URL(route, baseUrl).toString();
1307
+ try {
1308
+ const metadata = await extractMetadataFromURL(url);
1309
+ results.set(route, metadata);
1310
+ await new Promise((resolve) => setTimeout(resolve, 100));
1311
+ } catch (error) {
1312
+ console.error(`Failed to crawl ${url}:`, error);
1313
+ }
1314
+ }
1315
+ return results;
1316
+ }
1317
+
1318
+ // src/lib/route-discovery.ts
1319
+ var import_path = require("path");
1320
+ var import_glob = require("glob");
1321
+ async function discoverNextJSRoutes(appDir = "app", rootDir = process.cwd()) {
1322
+ const routes = [];
1323
+ const appPath = (0, import_path.join)(rootDir, appDir);
1324
+ try {
1325
+ const pageFiles = await (0, import_glob.glob)("**/page.tsx", {
1326
+ cwd: appPath,
1327
+ absolute: false,
1328
+ ignore: ["**/node_modules/**", "**/.next/**"]
1329
+ });
1330
+ for (const file of pageFiles) {
1331
+ const route = fileToRoute(file, appDir);
1332
+ if (route) {
1333
+ routes.push(route);
1334
+ }
1335
+ }
1336
+ } catch (error) {
1337
+ console.error("Error discovering routes:", error);
1338
+ }
1339
+ return routes;
1340
+ }
1341
+ function fileToRoute(filePath, appDir) {
1342
+ let routePath = filePath.replace(/^app\//, "").replace(/\/page\.tsx$/, "").replace(/\/page$/, "");
1343
+ if (routePath === "page" || routePath === "") {
1344
+ routePath = "/";
1345
+ } else {
1346
+ routePath = "/" + routePath;
1347
+ }
1348
+ const segments = routePath.split("/").filter(Boolean);
1349
+ const params = [];
1350
+ let isDynamic = false;
1351
+ let isCatchAll = false;
1352
+ for (const segment of segments) {
1353
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
1354
+ const param = segment.slice(4, -1);
1355
+ params.push(param);
1356
+ isDynamic = true;
1357
+ isCatchAll = true;
1358
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
1359
+ const param = segment.slice(1, -1);
1360
+ params.push(param);
1361
+ isDynamic = true;
1362
+ }
1363
+ }
1364
+ return {
1365
+ routePath,
1366
+ filePath: (0, import_path.join)(appDir, filePath),
1367
+ isDynamic,
1368
+ isCatchAll,
1369
+ params
1370
+ };
1371
+ }
1372
+
1373
+ // src/lib/validation/crawlability-validator.ts
1374
+ async function validateCrawlability(url, html) {
1375
+ const issues = [];
1376
+ const warnings = [];
1377
+ try {
1378
+ if (!html) {
1379
+ const response = await fetch(url, {
1380
+ headers: {
1381
+ "User-Agent": "SEO-Console-Bot/1.0"
1382
+ },
1383
+ redirect: "follow"
1384
+ });
1385
+ if (response.status === 404) {
1386
+ issues.push({
1387
+ type: "404",
1388
+ severity: "error",
1389
+ message: "Page returns 404 Not Found",
1390
+ page: url
1391
+ });
1392
+ return {
1393
+ crawlable: false,
1394
+ indexable: false,
1395
+ issues,
1396
+ warnings
1397
+ };
1398
+ }
1399
+ if (response.status !== 200) {
1400
+ issues.push({
1401
+ type: "404",
1402
+ severity: "error",
1403
+ message: `Page returns HTTP ${response.status}`,
1404
+ page: url
1405
+ });
1406
+ }
1407
+ if (response.status === 401 || response.status === 403) {
1408
+ issues.push({
1409
+ type: "auth_wall",
1410
+ severity: "error",
1411
+ message: "Page requires authentication (401/403)",
1412
+ page: url
1413
+ });
1414
+ }
1415
+ html = await response.text();
1416
+ }
1417
+ const metadata = extractMetadataFromHTML(html, url);
1418
+ if (metadata.robots) {
1419
+ const robots = metadata.robots.toLowerCase();
1420
+ if (robots.includes("noindex")) {
1421
+ issues.push({
1422
+ type: "noindex",
1423
+ severity: "error",
1424
+ message: "Page has noindex meta tag - will not be indexed",
1425
+ page: url
1426
+ });
1427
+ }
1428
+ if (robots.includes("nofollow")) {
1429
+ warnings.push({
1430
+ type: "nofollow",
1431
+ severity: "warning",
1432
+ message: "Page has nofollow meta tag - links won't be followed",
1433
+ page: url
1434
+ });
1435
+ }
1436
+ }
1437
+ if (!metadata.canonicalUrl) {
1438
+ warnings.push({
1439
+ type: "canonical_missing",
1440
+ severity: "warning",
1441
+ message: "Page missing canonical URL",
1442
+ page: url
1443
+ });
1444
+ }
1445
+ return {
1446
+ crawlable: issues.filter((i) => i.type !== "noindex").length === 0,
1447
+ indexable: !issues.some((i) => i.type === "noindex"),
1448
+ issues,
1449
+ warnings
1450
+ };
1451
+ } catch (error) {
1452
+ issues.push({
1453
+ type: "404",
1454
+ severity: "error",
1455
+ message: error instanceof Error ? error.message : "Failed to fetch page",
1456
+ page: url
1457
+ });
1458
+ return {
1459
+ crawlable: false,
1460
+ indexable: false,
1461
+ issues,
1462
+ warnings
1463
+ };
1464
+ }
1465
+ }
1466
+ async function validateRobotsTxt(baseUrl, routePath) {
1467
+ try {
1468
+ const robotsUrl = new URL("/robots.txt", baseUrl).toString();
1469
+ const response = await fetch(robotsUrl);
1470
+ if (!response.ok) {
1471
+ return { allowed: true, reason: "robots.txt not found (default: allow all)" };
1472
+ }
1473
+ const robotsTxt = await response.text();
1474
+ const lines = robotsTxt.split("\n").map((l) => l.trim());
1475
+ let currentUserAgent = "*";
1476
+ let isAllowed = true;
1477
+ for (const line of lines) {
1478
+ if (line.startsWith("#") || !line) continue;
1479
+ const [directive, ...valueParts] = line.split(":").map((s) => s.trim());
1480
+ const value = valueParts.join(":").trim();
1481
+ if (directive.toLowerCase() === "user-agent") {
1482
+ currentUserAgent = value;
1483
+ } else if (directive.toLowerCase() === "disallow") {
1484
+ if (currentUserAgent === "*" || currentUserAgent.toLowerCase() === "googlebot") {
1485
+ if (value === "/") {
1486
+ isAllowed = false;
1487
+ return { allowed: false, reason: "robots.txt disallows all pages" };
1488
+ } else if (routePath.startsWith(value)) {
1489
+ isAllowed = false;
1490
+ return { allowed: false, reason: `robots.txt disallows ${routePath}` };
1491
+ }
1492
+ }
1493
+ } else if (directive.toLowerCase() === "allow") {
1494
+ if (value === "/" || routePath.startsWith(value)) {
1495
+ isAllowed = true;
1496
+ }
1497
+ }
1498
+ }
1499
+ return { allowed: isAllowed };
1500
+ } catch (error) {
1501
+ return { allowed: true, reason: "Could not fetch robots.txt" };
1502
+ }
1503
+ }
1504
+ async function validatePublicAccess(url) {
1505
+ try {
1506
+ const response = await fetch(url, {
1507
+ headers: {
1508
+ "User-Agent": "SEO-Console-Bot/1.0"
1509
+ }
1510
+ });
1511
+ const requiresAuth = response.status === 401 || response.status === 403;
1512
+ const accessible = response.ok && !requiresAuth;
1513
+ return { accessible, requiresAuth };
1514
+ } catch {
1515
+ return { accessible: false, requiresAuth: false };
1516
+ }
1517
+ }
1518
+ // Annotate the CommonJS export names for ESM import in node:
1519
+ 0 && (module.exports = {
1520
+ crawlSiteForSEO,
1521
+ createSEORecord,
1522
+ createSEORecordSchema,
1523
+ deleteSEORecord,
1524
+ discoverNextJSRoutes,
1525
+ extractMetadataFromURL,
1526
+ generateRobotsTxt,
1527
+ generateSitemapFromRecords,
1528
+ generateSitemapXML,
1529
+ getAllSEORecords,
1530
+ getRoutePathFromParams,
1531
+ getSEORecordById,
1532
+ getSEORecordByRoute,
1533
+ getSEORecords,
1534
+ metadataToSEORecord,
1535
+ seoRecordsToSitemapEntries,
1536
+ updateRobotsTxtWithSitemap,
1537
+ updateSEORecord,
1538
+ updateSEORecordSchema,
1539
+ useGenerateMetadata,
1540
+ validateCrawlability,
1541
+ validateHTML,
1542
+ validateOGImage,
1543
+ validatePublicAccess,
1544
+ validateRobotsTxt,
1545
+ validateURL
1546
+ });
1547
+ //# sourceMappingURL=server.js.map