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