@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/index.js CHANGED
@@ -37,439 +37,33 @@ __export(index_exports, {
37
37
  CardFooter: () => CardFooter,
38
38
  CardHeader: () => CardHeader,
39
39
  CardTitle: () => CardTitle,
40
+ FileStorage: () => FileStorage,
40
41
  Input: () => Input,
41
42
  OGImagePreview: () => OGImagePreview,
42
43
  SEORecordForm: () => SEORecordForm,
43
44
  SEORecordList: () => SEORecordList,
44
45
  Spinner: () => Spinner,
45
46
  ValidationDashboard: () => ValidationDashboard,
46
- createSEORecord: () => createSEORecord,
47
+ crawlSiteForSEO: () => crawlSiteForSEO,
47
48
  createSEORecordSchema: () => createSEORecordSchema,
48
- deleteSEORecord: () => deleteSEORecord,
49
- getAllSEORecords: () => getSEORecords,
50
- getRoutePathFromParams: () => getRoutePathFromParams,
51
- getSEORecordById: () => getSEORecordById,
52
- getSEORecordByRoute: () => getSEORecordByRoute,
53
- updateSEORecord: () => updateSEORecord,
49
+ createStorageAdapter: () => createStorageAdapter,
50
+ detectStorageConfig: () => detectStorageConfig,
51
+ discoverNextJSRoutes: () => discoverNextJSRoutes,
52
+ extractMetadataFromHTML: () => extractMetadataFromHTML,
53
+ extractMetadataFromURL: () => extractMetadataFromURL,
54
+ extractSitemapFromRobotsTxt: () => extractSitemapFromRobotsTxt,
55
+ generateExamplePaths: () => generateExamplePaths,
56
+ generateRobotsTxt: () => generateRobotsTxt,
57
+ generateSitemapFromRecords: () => generateSitemapFromRecords,
58
+ generateSitemapXML: () => generateSitemapXML,
59
+ metadataToSEORecord: () => metadataToSEORecord,
60
+ seoRecordsToSitemapEntries: () => seoRecordsToSitemapEntries,
61
+ updateRobotsTxtWithSitemap: () => updateRobotsTxtWithSitemap,
54
62
  updateSEORecordSchema: () => updateSEORecordSchema,
55
- useGenerateMetadata: () => useGenerateMetadata,
56
- validateHTML: () => validateHTML,
57
- validateOGImage: () => validateOGImage
63
+ validateSitemapEntry: () => validateSitemapEntry
58
64
  });
59
65
  module.exports = __toCommonJS(index_exports);
60
66
 
61
- // src/lib/supabase/server.ts
62
- var import_ssr = require("@supabase/ssr");
63
- var import_headers = require("next/headers");
64
- async function createClient() {
65
- const cookieStore = await (0, import_headers.cookies)();
66
- return (0, import_ssr.createServerClient)(
67
- process.env.NEXT_PUBLIC_SUPABASE_URL,
68
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
69
- {
70
- cookies: {
71
- getAll() {
72
- return cookieStore.getAll();
73
- },
74
- setAll(cookiesToSet) {
75
- try {
76
- cookiesToSet.forEach(
77
- ({ name, value, options }) => cookieStore.set(name, value, options)
78
- );
79
- } catch {
80
- }
81
- }
82
- }
83
- }
84
- );
85
- }
86
-
87
- // src/lib/database/seo-records.ts
88
- function transformRowToSEORecord(row) {
89
- return {
90
- id: row.id,
91
- userId: row.user_id,
92
- routePath: row.route_path,
93
- title: row.title ?? void 0,
94
- description: row.description ?? void 0,
95
- keywords: row.keywords ?? void 0,
96
- ogTitle: row.og_title ?? void 0,
97
- ogDescription: row.og_description ?? void 0,
98
- ogImageUrl: row.og_image_url ?? void 0,
99
- ogImageWidth: row.og_image_width ?? void 0,
100
- ogImageHeight: row.og_image_height ?? void 0,
101
- ogType: row.og_type ?? void 0,
102
- ogUrl: row.og_url ?? void 0,
103
- ogSiteName: row.og_site_name ?? void 0,
104
- twitterCard: row.twitter_card ?? void 0,
105
- twitterTitle: row.twitter_title ?? void 0,
106
- twitterDescription: row.twitter_description ?? void 0,
107
- twitterImageUrl: row.twitter_image_url ?? void 0,
108
- twitterSite: row.twitter_site ?? void 0,
109
- twitterCreator: row.twitter_creator ?? void 0,
110
- canonicalUrl: row.canonical_url ?? void 0,
111
- robots: row.robots ?? void 0,
112
- author: row.author ?? void 0,
113
- publishedTime: row.published_time ? new Date(row.published_time) : void 0,
114
- modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
115
- structuredData: row.structured_data ? row.structured_data : void 0,
116
- validationStatus: row.validation_status ?? void 0,
117
- lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
118
- validationErrors: row.validation_errors ? row.validation_errors : void 0,
119
- createdAt: new Date(row.created_at),
120
- updatedAt: new Date(row.updated_at)
121
- };
122
- }
123
- function transformToInsert(record) {
124
- return {
125
- user_id: record.userId,
126
- route_path: record.routePath,
127
- title: record.title ?? null,
128
- description: record.description ?? null,
129
- keywords: record.keywords ?? null,
130
- og_title: record.ogTitle ?? null,
131
- og_description: record.ogDescription ?? null,
132
- og_image_url: record.ogImageUrl ?? null,
133
- og_image_width: record.ogImageWidth ?? null,
134
- og_image_height: record.ogImageHeight ?? null,
135
- og_type: record.ogType ?? null,
136
- og_url: record.ogUrl ?? null,
137
- og_site_name: record.ogSiteName ?? null,
138
- twitter_card: record.twitterCard ?? null,
139
- twitter_title: record.twitterTitle ?? null,
140
- twitter_description: record.twitterDescription ?? null,
141
- twitter_image_url: record.twitterImageUrl ?? null,
142
- twitter_site: record.twitterSite ?? null,
143
- twitter_creator: record.twitterCreator ?? null,
144
- canonical_url: record.canonicalUrl ?? null,
145
- robots: record.robots ?? null,
146
- author: record.author ?? null,
147
- published_time: record.publishedTime?.toISOString() ?? null,
148
- modified_time: record.modifiedTime?.toISOString() ?? null,
149
- structured_data: record.structuredData ?? null
150
- };
151
- }
152
- function transformToUpdate(record) {
153
- const update = {};
154
- if (record.routePath !== void 0) update.route_path = record.routePath;
155
- if (record.title !== void 0) update.title = record.title ?? null;
156
- if (record.description !== void 0)
157
- update.description = record.description ?? null;
158
- if (record.keywords !== void 0) update.keywords = record.keywords ?? null;
159
- if (record.ogTitle !== void 0) update.og_title = record.ogTitle ?? null;
160
- if (record.ogDescription !== void 0)
161
- update.og_description = record.ogDescription ?? null;
162
- if (record.ogImageUrl !== void 0)
163
- update.og_image_url = record.ogImageUrl ?? null;
164
- if (record.ogImageWidth !== void 0)
165
- update.og_image_width = record.ogImageWidth ?? null;
166
- if (record.ogImageHeight !== void 0)
167
- update.og_image_height = record.ogImageHeight ?? null;
168
- if (record.ogType !== void 0) update.og_type = record.ogType ?? null;
169
- if (record.ogUrl !== void 0) update.og_url = record.ogUrl ?? null;
170
- if (record.ogSiteName !== void 0)
171
- update.og_site_name = record.ogSiteName ?? null;
172
- if (record.twitterCard !== void 0)
173
- update.twitter_card = record.twitterCard ?? null;
174
- if (record.twitterTitle !== void 0)
175
- update.twitter_title = record.twitterTitle ?? null;
176
- if (record.twitterDescription !== void 0)
177
- update.twitter_description = record.twitterDescription ?? null;
178
- if (record.twitterImageUrl !== void 0)
179
- update.twitter_image_url = record.twitterImageUrl ?? null;
180
- if (record.twitterSite !== void 0)
181
- update.twitter_site = record.twitterSite ?? null;
182
- if (record.twitterCreator !== void 0)
183
- update.twitter_creator = record.twitterCreator ?? null;
184
- if (record.canonicalUrl !== void 0)
185
- update.canonical_url = record.canonicalUrl ?? null;
186
- if (record.robots !== void 0) update.robots = record.robots ?? null;
187
- if (record.author !== void 0) update.author = record.author ?? null;
188
- if (record.publishedTime !== void 0)
189
- update.published_time = record.publishedTime?.toISOString() ?? null;
190
- if (record.modifiedTime !== void 0)
191
- update.modified_time = record.modifiedTime?.toISOString() ?? null;
192
- if (record.structuredData !== void 0)
193
- update.structured_data = record.structuredData ?? null;
194
- if (record.validationStatus !== void 0)
195
- update.validation_status = record.validationStatus ?? null;
196
- if (record.lastValidatedAt !== void 0)
197
- update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;
198
- if (record.validationErrors !== void 0)
199
- update.validation_errors = record.validationErrors ?? null;
200
- return update;
201
- }
202
- async function getSEORecords() {
203
- try {
204
- const supabase = await createClient();
205
- const {
206
- data: { user }
207
- } = await supabase.auth.getUser();
208
- if (!user) {
209
- return {
210
- success: false,
211
- error: new Error("User not authenticated")
212
- };
213
- }
214
- const { data, error } = await supabase.from("seo_records").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
215
- if (error) {
216
- return { success: false, error };
217
- }
218
- const records = (data || []).map(transformRowToSEORecord);
219
- return { success: true, data: records };
220
- } catch (error) {
221
- return {
222
- success: false,
223
- error: error instanceof Error ? error : new Error("Unknown error")
224
- };
225
- }
226
- }
227
- async function getSEORecordById(id) {
228
- try {
229
- const supabase = await createClient();
230
- const {
231
- data: { user }
232
- } = await supabase.auth.getUser();
233
- if (!user) {
234
- return {
235
- success: false,
236
- error: new Error("User not authenticated")
237
- };
238
- }
239
- const { data, error } = await supabase.from("seo_records").select("*").eq("id", id).eq("user_id", user.id).single();
240
- if (error) {
241
- return { success: false, error };
242
- }
243
- if (!data) {
244
- return {
245
- success: false,
246
- error: new Error("SEO record not found")
247
- };
248
- }
249
- return { success: true, data: transformRowToSEORecord(data) };
250
- } catch (error) {
251
- return {
252
- success: false,
253
- error: error instanceof Error ? error : new Error("Unknown error")
254
- };
255
- }
256
- }
257
- async function getSEORecordByRoute(routePath) {
258
- try {
259
- const supabase = await createClient();
260
- const {
261
- data: { user }
262
- } = await supabase.auth.getUser();
263
- if (!user) {
264
- return {
265
- success: false,
266
- error: new Error("User not authenticated")
267
- };
268
- }
269
- const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
270
- if (error) {
271
- return { success: false, error };
272
- }
273
- if (!data) {
274
- return { success: true, data: null };
275
- }
276
- return { success: true, data: transformRowToSEORecord(data) };
277
- } catch (error) {
278
- return {
279
- success: false,
280
- error: error instanceof Error ? error : new Error("Unknown error")
281
- };
282
- }
283
- }
284
- async function createSEORecord(record) {
285
- try {
286
- const supabase = await createClient();
287
- const {
288
- data: { user }
289
- } = await supabase.auth.getUser();
290
- if (!user) {
291
- return {
292
- success: false,
293
- error: new Error("User not authenticated")
294
- };
295
- }
296
- const insertData = transformToInsert({ ...record, userId: user.id });
297
- const { data, error } = await supabase.from("seo_records").insert(insertData).select().single();
298
- if (error) {
299
- return { success: false, error };
300
- }
301
- return { success: true, data: transformRowToSEORecord(data) };
302
- } catch (error) {
303
- return {
304
- success: false,
305
- error: error instanceof Error ? error : new Error("Unknown error")
306
- };
307
- }
308
- }
309
- async function updateSEORecord(record) {
310
- try {
311
- const supabase = await createClient();
312
- const {
313
- data: { user }
314
- } = await supabase.auth.getUser();
315
- if (!user) {
316
- return {
317
- success: false,
318
- error: new Error("User not authenticated")
319
- };
320
- }
321
- const { id, ...updateData } = record;
322
- const transformedUpdate = transformToUpdate(updateData);
323
- const { data, error } = await supabase.from("seo_records").update(transformedUpdate).eq("id", id).eq("user_id", user.id).select().single();
324
- if (error) {
325
- return { success: false, error };
326
- }
327
- if (!data) {
328
- return {
329
- success: false,
330
- error: new Error("SEO record not found")
331
- };
332
- }
333
- return { success: true, data: transformRowToSEORecord(data) };
334
- } catch (error) {
335
- return {
336
- success: false,
337
- error: error instanceof Error ? error : new Error("Unknown error")
338
- };
339
- }
340
- }
341
- async function deleteSEORecord(id) {
342
- try {
343
- const supabase = await createClient();
344
- const {
345
- data: { user }
346
- } = await supabase.auth.getUser();
347
- if (!user) {
348
- return {
349
- success: false,
350
- error: new Error("User not authenticated")
351
- };
352
- }
353
- const { error } = await supabase.from("seo_records").delete().eq("id", id).eq("user_id", user.id);
354
- if (error) {
355
- return { success: false, error };
356
- }
357
- return { success: true, data: void 0 };
358
- } catch (error) {
359
- return {
360
- success: false,
361
- error: error instanceof Error ? error : new Error("Unknown error")
362
- };
363
- }
364
- }
365
-
366
- // src/hooks/useGenerateMetadata.ts
367
- async function useGenerateMetadata(options = {}) {
368
- const { routePath, fallback = {} } = options;
369
- if (!routePath) {
370
- return {
371
- title: fallback.title,
372
- description: fallback.description,
373
- ...fallback
374
- };
375
- }
376
- const result = await getSEORecordByRoute(routePath);
377
- if (!result.success || !result.data) {
378
- return {
379
- title: fallback.title,
380
- description: fallback.description,
381
- ...fallback
382
- };
383
- }
384
- const record = result.data;
385
- const metadata = {};
386
- if (record.title) {
387
- metadata.title = record.title;
388
- }
389
- if (record.description) {
390
- metadata.description = record.description;
391
- }
392
- if (record.keywords && record.keywords.length > 0) {
393
- metadata.keywords = record.keywords;
394
- }
395
- if (record.author) {
396
- metadata.authors = [{ name: record.author }];
397
- }
398
- if (record.ogTitle || record.ogDescription || record.ogImageUrl || record.ogType) {
399
- const supportedOGTypes = ["website", "article", "book", "profile"];
400
- const ogType = record.ogType && supportedOGTypes.includes(record.ogType) ? record.ogType : "website";
401
- const openGraph = {
402
- type: ogType,
403
- title: record.ogTitle || record.title || void 0,
404
- description: record.ogDescription || record.description || void 0,
405
- url: record.ogUrl || void 0,
406
- siteName: record.ogSiteName || void 0
407
- };
408
- if (record.ogImageUrl) {
409
- openGraph.images = [
410
- {
411
- url: record.ogImageUrl,
412
- width: record.ogImageWidth || void 0,
413
- height: record.ogImageHeight || void 0,
414
- alt: record.ogTitle || record.title || void 0
415
- }
416
- ];
417
- }
418
- if (ogType === "article") {
419
- const articleOpenGraph = {
420
- ...openGraph,
421
- ...record.publishedTime && {
422
- publishedTime: record.publishedTime.toISOString()
423
- },
424
- ...record.modifiedTime && {
425
- modifiedTime: record.modifiedTime.toISOString()
426
- }
427
- };
428
- metadata.openGraph = articleOpenGraph;
429
- } else {
430
- metadata.openGraph = openGraph;
431
- }
432
- }
433
- if (record.twitterCard || record.twitterTitle || record.twitterDescription || record.twitterImageUrl) {
434
- metadata.twitter = {
435
- card: record.twitterCard || "summary",
436
- title: record.twitterTitle || record.ogTitle || record.title || void 0,
437
- description: record.twitterDescription || record.ogDescription || record.description || void 0,
438
- images: record.twitterImageUrl ? [record.twitterImageUrl] : void 0,
439
- site: record.twitterSite || void 0,
440
- creator: record.twitterCreator || void 0
441
- };
442
- }
443
- if (record.canonicalUrl) {
444
- metadata.alternates = {
445
- canonical: record.canonicalUrl
446
- };
447
- }
448
- if (record.robots) {
449
- metadata.robots = record.robots;
450
- }
451
- return {
452
- ...fallback,
453
- ...metadata,
454
- // Ensure title and description from record override fallback if present
455
- title: record.title || fallback.title,
456
- description: record.description || fallback.description,
457
- // Merge openGraph if both exist
458
- openGraph: fallback.openGraph ? { ...metadata.openGraph, ...fallback.openGraph } : metadata.openGraph,
459
- // Merge twitter if both exist
460
- twitter: fallback.twitter ? { ...metadata.twitter, ...fallback.twitter } : metadata.twitter
461
- };
462
- }
463
- function getRoutePathFromParams(params, pattern) {
464
- let routePath = pattern;
465
- for (const [key, value] of Object.entries(params)) {
466
- const paramValue = Array.isArray(value) ? value.join("/") : value;
467
- routePath = routePath.replace(`[${key}]`, paramValue);
468
- routePath = routePath.replace(`[...${key}]`, paramValue);
469
- }
470
- return routePath;
471
- }
472
-
473
67
  // src/components/seo/SEORecordList.tsx
474
68
  var import_react3 = require("react");
475
69
 
@@ -3836,7 +3430,9 @@ function SEORecordList() {
3836
3430
  // src/components/seo/ValidationDashboard.tsx
3837
3431
  var import_react4 = require("react");
3838
3432
  var import_jsx_runtime8 = require("react/jsx-runtime");
3839
- var Link = ({ href, children, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href, ...props, children });
3433
+ var Link = ({ href, children, ...props }) => {
3434
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href, ...props, children });
3435
+ };
3840
3436
  function ValidationDashboard() {
3841
3437
  const [records, setRecords] = (0, import_react4.useState)([]);
3842
3438
  const [loading, setLoading] = (0, import_react4.useState)(true);
@@ -4047,303 +3643,813 @@ function ValidationDashboard() {
4047
3643
  ] })
4048
3644
  ] });
4049
3645
  }
4050
-
4051
- // src/lib/validation/image-validator.ts
4052
- var import_sharp = __toESM(require("sharp"));
4053
- async function validateOGImage(imageUrl, expectedWidth, expectedHeight) {
4054
- const issues = [];
3646
+
3647
+ // src/lib/route-discovery.ts
3648
+ var import_path = require("path");
3649
+ var import_glob = require("glob");
3650
+ async function discoverNextJSRoutes(appDir = "app", rootDir = process.cwd()) {
3651
+ const routes = [];
3652
+ const appPath = (0, import_path.join)(rootDir, appDir);
3653
+ try {
3654
+ const pageFiles = await (0, import_glob.glob)("**/page.tsx", {
3655
+ cwd: appPath,
3656
+ absolute: false,
3657
+ ignore: ["**/node_modules/**", "**/.next/**"]
3658
+ });
3659
+ for (const file of pageFiles) {
3660
+ const route = fileToRoute(file, appDir);
3661
+ if (route) {
3662
+ routes.push(route);
3663
+ }
3664
+ }
3665
+ } catch (error) {
3666
+ console.error("Error discovering routes:", error);
3667
+ }
3668
+ return routes;
3669
+ }
3670
+ function fileToRoute(filePath, appDir) {
3671
+ let routePath = filePath.replace(/^app\//, "").replace(/\/page\.tsx$/, "").replace(/\/page$/, "");
3672
+ if (routePath === "page" || routePath === "") {
3673
+ routePath = "/";
3674
+ } else {
3675
+ routePath = "/" + routePath;
3676
+ }
3677
+ const segments = routePath.split("/").filter(Boolean);
3678
+ const params = [];
3679
+ let isDynamic = false;
3680
+ let isCatchAll = false;
3681
+ for (const segment of segments) {
3682
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
3683
+ const param = segment.slice(4, -1);
3684
+ params.push(param);
3685
+ isDynamic = true;
3686
+ isCatchAll = true;
3687
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
3688
+ const param = segment.slice(1, -1);
3689
+ params.push(param);
3690
+ isDynamic = true;
3691
+ }
3692
+ }
3693
+ return {
3694
+ routePath,
3695
+ filePath: (0, import_path.join)(appDir, filePath),
3696
+ isDynamic,
3697
+ isCatchAll,
3698
+ params
3699
+ };
3700
+ }
3701
+ function generateExamplePaths(route, count = 3) {
3702
+ if (!route.isDynamic) {
3703
+ return [route.routePath];
3704
+ }
3705
+ const examples = [];
3706
+ const segments = route.routePath.split("/").filter(Boolean);
3707
+ for (let i = 0; i < count; i++) {
3708
+ let examplePath = "";
3709
+ for (const segment of segments) {
3710
+ if (segment.startsWith("[...")) {
3711
+ examplePath += `/example-${i}-part-1/example-${i}-part-2`;
3712
+ } else if (segment.startsWith("[")) {
3713
+ examplePath += `/example-${i}`;
3714
+ } else {
3715
+ examplePath += `/${segment}`;
3716
+ }
3717
+ }
3718
+ examples.push(examplePath || "/");
3719
+ }
3720
+ return examples;
3721
+ }
3722
+
3723
+ // src/lib/metadata-extractor.ts
3724
+ var cheerio = __toESM(require("cheerio"));
3725
+ function extractMetadataFromHTML(html, baseUrl) {
3726
+ const $ = cheerio.load(html);
3727
+ const metadata = {};
3728
+ metadata.title = $("title").text() || void 0;
3729
+ metadata.description = $('meta[name="description"]').attr("content") || void 0;
3730
+ metadata.robots = $('meta[name="robots"]').attr("content") || void 0;
3731
+ const keywords = $('meta[name="keywords"]').attr("content");
3732
+ if (keywords) {
3733
+ metadata.keywords = keywords.split(",").map((k) => k.trim());
3734
+ }
3735
+ metadata.ogTitle = $('meta[property="og:title"]').attr("content") || void 0;
3736
+ metadata.ogDescription = $('meta[property="og:description"]').attr("content") || void 0;
3737
+ metadata.ogImageUrl = $('meta[property="og:image"]').attr("content") || void 0;
3738
+ metadata.ogType = $('meta[property="og:type"]').attr("content") || void 0;
3739
+ metadata.ogUrl = $('meta[property="og:url"]').attr("content") || void 0;
3740
+ metadata.canonicalUrl = $('link[rel="canonical"]').attr("href") || void 0;
3741
+ if (baseUrl) {
3742
+ if (metadata.ogImageUrl && !metadata.ogImageUrl.startsWith("http")) {
3743
+ metadata.ogImageUrl = new URL(metadata.ogImageUrl, baseUrl).toString();
3744
+ }
3745
+ if (metadata.canonicalUrl && !metadata.canonicalUrl.startsWith("http")) {
3746
+ metadata.canonicalUrl = new URL(metadata.canonicalUrl, baseUrl).toString();
3747
+ }
3748
+ }
3749
+ return metadata;
3750
+ }
3751
+ async function extractMetadataFromURL(url) {
3752
+ try {
3753
+ const response = await fetch(url, {
3754
+ headers: {
3755
+ "User-Agent": "SEO-Console/1.0"
3756
+ }
3757
+ });
3758
+ if (!response.ok) {
3759
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
3760
+ }
3761
+ const html = await response.text();
3762
+ return extractMetadataFromHTML(html, url);
3763
+ } catch (error) {
3764
+ console.error(`Error extracting metadata from ${url}:`, error);
3765
+ return {};
3766
+ }
3767
+ }
3768
+ function metadataToSEORecord(metadata, routePath, userId = "extracted") {
3769
+ return {
3770
+ userId,
3771
+ routePath,
3772
+ title: metadata.title,
3773
+ description: metadata.description,
3774
+ keywords: metadata.keywords,
3775
+ ogTitle: metadata.ogTitle,
3776
+ ogDescription: metadata.ogDescription,
3777
+ ogImageUrl: metadata.ogImageUrl,
3778
+ ogType: metadata.ogType,
3779
+ ogUrl: metadata.ogUrl,
3780
+ canonicalUrl: metadata.canonicalUrl,
3781
+ robots: metadata.robots,
3782
+ validationStatus: "pending"
3783
+ };
3784
+ }
3785
+ async function crawlSiteForSEO(baseUrl, routes) {
3786
+ const results = /* @__PURE__ */ new Map();
3787
+ for (const route of routes) {
3788
+ const url = new URL(route, baseUrl).toString();
3789
+ try {
3790
+ const metadata = await extractMetadataFromURL(url);
3791
+ results.set(route, metadata);
3792
+ await new Promise((resolve) => setTimeout(resolve, 100));
3793
+ } catch (error) {
3794
+ console.error(`Failed to crawl ${url}:`, error);
3795
+ }
3796
+ }
3797
+ return results;
3798
+ }
3799
+
3800
+ // src/lib/sitemap-generator.ts
3801
+ function generateSitemapXML(options) {
3802
+ const { baseUrl, entries } = options;
3803
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
3804
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3805
+ ${entries.map((entry) => {
3806
+ const loc = entry.loc.startsWith("http") ? entry.loc : new URL(entry.loc, baseUrl).toString();
3807
+ return ` <url>
3808
+ <loc>${escapeXML(loc)}</loc>${entry.lastmod ? `
3809
+ <lastmod>${entry.lastmod}</lastmod>` : ""}${entry.changefreq ? `
3810
+ <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority !== void 0 ? `
3811
+ <priority>${entry.priority}</priority>` : ""}
3812
+ </url>`;
3813
+ }).join("\n")}
3814
+ </urlset>`;
3815
+ return xml;
3816
+ }
3817
+ function seoRecordsToSitemapEntries(records, baseUrl) {
3818
+ return records.filter((record) => {
3819
+ return record.canonicalUrl && record.canonicalUrl.trim() !== "";
3820
+ }).map((record) => {
3821
+ const entry = {
3822
+ loc: record.canonicalUrl
3823
+ };
3824
+ if (record.modifiedTime) {
3825
+ entry.lastmod = record.modifiedTime.toISOString().split("T")[0];
3826
+ } else if (record.lastValidatedAt) {
3827
+ entry.lastmod = record.lastValidatedAt.toISOString().split("T")[0];
3828
+ }
3829
+ if (record.routePath === "/") {
3830
+ entry.changefreq = "daily";
3831
+ entry.priority = 1;
3832
+ } else if (record.routePath.includes("/blog/") || record.routePath.includes("/posts/")) {
3833
+ entry.changefreq = "weekly";
3834
+ entry.priority = 0.8;
3835
+ } else {
3836
+ entry.changefreq = "monthly";
3837
+ entry.priority = 0.6;
3838
+ }
3839
+ return entry;
3840
+ }).sort((a, b) => {
3841
+ if (a.priority !== b.priority) {
3842
+ return (b.priority || 0) - (a.priority || 0);
3843
+ }
3844
+ return a.loc.localeCompare(b.loc);
3845
+ });
3846
+ }
3847
+ function generateSitemapFromRecords(records, baseUrl) {
3848
+ const entries = seoRecordsToSitemapEntries(records, baseUrl);
3849
+ return generateSitemapXML({ baseUrl, entries });
3850
+ }
3851
+ function escapeXML(str) {
3852
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3853
+ }
3854
+ function validateSitemapEntry(entry) {
3855
+ const errors = [];
3856
+ if (!entry.loc) {
3857
+ errors.push("Location (loc) is required");
3858
+ } else {
3859
+ try {
3860
+ new URL(entry.loc);
3861
+ } catch {
3862
+ errors.push("Location must be a valid URL");
3863
+ }
3864
+ }
3865
+ if (entry.priority !== void 0) {
3866
+ if (entry.priority < 0 || entry.priority > 1) {
3867
+ errors.push("Priority must be between 0.0 and 1.0");
3868
+ }
3869
+ }
3870
+ if (entry.lastmod) {
3871
+ const date = new Date(entry.lastmod);
3872
+ if (isNaN(date.getTime())) {
3873
+ errors.push("Lastmod must be a valid date (YYYY-MM-DD)");
3874
+ }
3875
+ }
3876
+ return {
3877
+ valid: errors.length === 0,
3878
+ errors
3879
+ };
3880
+ }
3881
+
3882
+ // src/lib/robots-generator.ts
3883
+ function generateRobotsTxt(options = {}) {
3884
+ const { userAgents = [], sitemapUrl, crawlDelay } = options;
3885
+ let content = "";
3886
+ if (userAgents.length === 0) {
3887
+ content += "User-agent: *\n";
3888
+ if (crawlDelay) {
3889
+ content += `Crawl-delay: ${crawlDelay}
3890
+ `;
3891
+ }
3892
+ content += "Allow: /\n";
3893
+ content += "\n";
3894
+ } else {
3895
+ for (const ua of userAgents) {
3896
+ content += `User-agent: ${ua.agent}
3897
+ `;
3898
+ if (crawlDelay) {
3899
+ content += `Crawl-delay: ${crawlDelay}
3900
+ `;
3901
+ }
3902
+ if (ua.allow) {
3903
+ for (const path of ua.allow) {
3904
+ content += `Allow: ${path}
3905
+ `;
3906
+ }
3907
+ }
3908
+ if (ua.disallow) {
3909
+ for (const path of ua.disallow) {
3910
+ content += `Disallow: ${path}
3911
+ `;
3912
+ }
3913
+ }
3914
+ content += "\n";
3915
+ }
3916
+ }
3917
+ if (sitemapUrl) {
3918
+ content += `Sitemap: ${sitemapUrl}
3919
+ `;
3920
+ }
3921
+ return content.trim();
3922
+ }
3923
+ function updateRobotsTxtWithSitemap(existingContent, sitemapUrl) {
3924
+ const sitemapRegex = /^Sitemap:\s*.+$/m;
3925
+ if (sitemapRegex.test(existingContent)) {
3926
+ return existingContent.replace(sitemapRegex, `Sitemap: ${sitemapUrl}`);
3927
+ }
3928
+ const trimmed = existingContent.trim();
3929
+ return trimmed ? `${trimmed}
3930
+
3931
+ Sitemap: ${sitemapUrl}` : `Sitemap: ${sitemapUrl}`;
3932
+ }
3933
+ function extractSitemapFromRobotsTxt(content) {
3934
+ const match = content.match(/^Sitemap:\s*(.+)$/m);
3935
+ return match ? match[1].trim() : null;
3936
+ }
3937
+
3938
+ // src/lib/storage/file-storage.ts
3939
+ var import_fs = require("fs");
3940
+ var FileStorage = class {
3941
+ constructor(filePath = "seo-records.json") {
3942
+ this.records = [];
3943
+ this.initialized = false;
3944
+ this.filePath = filePath;
3945
+ }
3946
+ async ensureInitialized() {
3947
+ if (this.initialized) return;
3948
+ try {
3949
+ const data = await import_fs.promises.readFile(this.filePath, "utf-8");
3950
+ this.records = JSON.parse(data);
3951
+ } catch (error) {
3952
+ if (error.code === "ENOENT") {
3953
+ this.records = [];
3954
+ await this.save();
3955
+ } else {
3956
+ throw error;
3957
+ }
3958
+ }
3959
+ this.initialized = true;
3960
+ }
3961
+ async save() {
3962
+ await import_fs.promises.writeFile(this.filePath, JSON.stringify(this.records, null, 2), "utf-8");
3963
+ }
3964
+ async isAvailable() {
3965
+ try {
3966
+ const dir = this.filePath.includes("/") ? this.filePath.substring(0, this.filePath.lastIndexOf("/")) : ".";
3967
+ await import_fs.promises.access(dir);
3968
+ return true;
3969
+ } catch {
3970
+ return false;
3971
+ }
3972
+ }
3973
+ async getRecords() {
3974
+ await this.ensureInitialized();
3975
+ return [...this.records];
3976
+ }
3977
+ async getRecordById(id) {
3978
+ await this.ensureInitialized();
3979
+ return this.records.find((r) => r.id === id) || null;
3980
+ }
3981
+ async getRecordByRoute(routePath) {
3982
+ await this.ensureInitialized();
3983
+ return this.records.find((r) => r.routePath === routePath) || null;
3984
+ }
3985
+ async createRecord(record) {
3986
+ await this.ensureInitialized();
3987
+ const newRecord = {
3988
+ id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
3989
+ userId: "file-user",
3990
+ // File storage doesn't need user IDs
3991
+ routePath: record.routePath,
3992
+ title: record.title,
3993
+ description: record.description,
3994
+ keywords: record.keywords,
3995
+ ogTitle: record.ogTitle,
3996
+ ogDescription: record.ogDescription,
3997
+ ogImageUrl: record.ogImageUrl,
3998
+ ogImageWidth: record.ogImageWidth,
3999
+ ogImageHeight: record.ogImageHeight,
4000
+ ogType: record.ogType,
4001
+ ogUrl: record.ogUrl,
4002
+ ogSiteName: record.ogSiteName,
4003
+ twitterCard: record.twitterCard,
4004
+ twitterTitle: record.twitterTitle,
4005
+ twitterDescription: record.twitterDescription,
4006
+ twitterImageUrl: record.twitterImageUrl,
4007
+ twitterSite: record.twitterSite,
4008
+ twitterCreator: record.twitterCreator,
4009
+ canonicalUrl: record.canonicalUrl,
4010
+ robots: record.robots,
4011
+ author: record.author,
4012
+ publishedTime: record.publishedTime,
4013
+ modifiedTime: record.modifiedTime,
4014
+ structuredData: record.structuredData,
4015
+ validationStatus: "pending",
4016
+ lastValidatedAt: void 0,
4017
+ validationErrors: void 0
4018
+ };
4019
+ this.records.push(newRecord);
4020
+ await this.save();
4021
+ return newRecord;
4022
+ }
4023
+ async updateRecord(record) {
4024
+ await this.ensureInitialized();
4025
+ const index = this.records.findIndex((r) => r.id === record.id);
4026
+ if (index === -1) {
4027
+ throw new Error(`SEO record with id ${record.id} not found`);
4028
+ }
4029
+ const updated = {
4030
+ ...this.records[index],
4031
+ ...record
4032
+ };
4033
+ this.records[index] = updated;
4034
+ await this.save();
4035
+ return updated;
4036
+ }
4037
+ async deleteRecord(id) {
4038
+ await this.ensureInitialized();
4039
+ const index = this.records.findIndex((r) => r.id === id);
4040
+ if (index === -1) {
4041
+ throw new Error(`SEO record with id ${id} not found`);
4042
+ }
4043
+ this.records.splice(index, 1);
4044
+ await this.save();
4045
+ }
4046
+ };
4047
+
4048
+ // src/lib/supabase/server.ts
4049
+ var import_ssr = require("@supabase/ssr");
4050
+ var import_headers = require("next/headers");
4051
+ async function createClient() {
4052
+ const cookieStore = await (0, import_headers.cookies)();
4053
+ return (0, import_ssr.createServerClient)(
4054
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
4055
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
4056
+ {
4057
+ cookies: {
4058
+ getAll() {
4059
+ return cookieStore.getAll();
4060
+ },
4061
+ setAll(cookiesToSet) {
4062
+ try {
4063
+ cookiesToSet.forEach(
4064
+ ({ name, value, options }) => cookieStore.set(name, value, options)
4065
+ );
4066
+ } catch {
4067
+ }
4068
+ }
4069
+ }
4070
+ }
4071
+ );
4072
+ }
4073
+
4074
+ // src/lib/database/seo-records.ts
4075
+ function transformRowToSEORecord(row) {
4076
+ return {
4077
+ id: row.id,
4078
+ userId: row.user_id,
4079
+ routePath: row.route_path,
4080
+ title: row.title ?? void 0,
4081
+ description: row.description ?? void 0,
4082
+ keywords: row.keywords ?? void 0,
4083
+ ogTitle: row.og_title ?? void 0,
4084
+ ogDescription: row.og_description ?? void 0,
4085
+ ogImageUrl: row.og_image_url ?? void 0,
4086
+ ogImageWidth: row.og_image_width ?? void 0,
4087
+ ogImageHeight: row.og_image_height ?? void 0,
4088
+ ogType: row.og_type ?? void 0,
4089
+ ogUrl: row.og_url ?? void 0,
4090
+ ogSiteName: row.og_site_name ?? void 0,
4091
+ twitterCard: row.twitter_card ?? void 0,
4092
+ twitterTitle: row.twitter_title ?? void 0,
4093
+ twitterDescription: row.twitter_description ?? void 0,
4094
+ twitterImageUrl: row.twitter_image_url ?? void 0,
4095
+ twitterSite: row.twitter_site ?? void 0,
4096
+ twitterCreator: row.twitter_creator ?? void 0,
4097
+ canonicalUrl: row.canonical_url ?? void 0,
4098
+ robots: row.robots ?? void 0,
4099
+ author: row.author ?? void 0,
4100
+ publishedTime: row.published_time ? new Date(row.published_time) : void 0,
4101
+ modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
4102
+ structuredData: row.structured_data ? row.structured_data : void 0,
4103
+ validationStatus: row.validation_status ?? void 0,
4104
+ lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
4105
+ validationErrors: row.validation_errors ? row.validation_errors : void 0,
4106
+ createdAt: new Date(row.created_at),
4107
+ updatedAt: new Date(row.updated_at)
4108
+ };
4109
+ }
4110
+ function transformToInsert(record) {
4111
+ return {
4112
+ user_id: record.userId,
4113
+ route_path: record.routePath,
4114
+ title: record.title ?? null,
4115
+ description: record.description ?? null,
4116
+ keywords: record.keywords ?? null,
4117
+ og_title: record.ogTitle ?? null,
4118
+ og_description: record.ogDescription ?? null,
4119
+ og_image_url: record.ogImageUrl ?? null,
4120
+ og_image_width: record.ogImageWidth ?? null,
4121
+ og_image_height: record.ogImageHeight ?? null,
4122
+ og_type: record.ogType ?? null,
4123
+ og_url: record.ogUrl ?? null,
4124
+ og_site_name: record.ogSiteName ?? null,
4125
+ twitter_card: record.twitterCard ?? null,
4126
+ twitter_title: record.twitterTitle ?? null,
4127
+ twitter_description: record.twitterDescription ?? null,
4128
+ twitter_image_url: record.twitterImageUrl ?? null,
4129
+ twitter_site: record.twitterSite ?? null,
4130
+ twitter_creator: record.twitterCreator ?? null,
4131
+ canonical_url: record.canonicalUrl ?? null,
4132
+ robots: record.robots ?? null,
4133
+ author: record.author ?? null,
4134
+ published_time: record.publishedTime?.toISOString() ?? null,
4135
+ modified_time: record.modifiedTime?.toISOString() ?? null,
4136
+ structured_data: record.structuredData ?? null
4137
+ };
4138
+ }
4139
+ function transformToUpdate(record) {
4140
+ const update = {};
4141
+ if (record.routePath !== void 0) update.route_path = record.routePath;
4142
+ if (record.title !== void 0) update.title = record.title ?? null;
4143
+ if (record.description !== void 0)
4144
+ update.description = record.description ?? null;
4145
+ if (record.keywords !== void 0) update.keywords = record.keywords ?? null;
4146
+ if (record.ogTitle !== void 0) update.og_title = record.ogTitle ?? null;
4147
+ if (record.ogDescription !== void 0)
4148
+ update.og_description = record.ogDescription ?? null;
4149
+ if (record.ogImageUrl !== void 0)
4150
+ update.og_image_url = record.ogImageUrl ?? null;
4151
+ if (record.ogImageWidth !== void 0)
4152
+ update.og_image_width = record.ogImageWidth ?? null;
4153
+ if (record.ogImageHeight !== void 0)
4154
+ update.og_image_height = record.ogImageHeight ?? null;
4155
+ if (record.ogType !== void 0) update.og_type = record.ogType ?? null;
4156
+ if (record.ogUrl !== void 0) update.og_url = record.ogUrl ?? null;
4157
+ if (record.ogSiteName !== void 0)
4158
+ update.og_site_name = record.ogSiteName ?? null;
4159
+ if (record.twitterCard !== void 0)
4160
+ update.twitter_card = record.twitterCard ?? null;
4161
+ if (record.twitterTitle !== void 0)
4162
+ update.twitter_title = record.twitterTitle ?? null;
4163
+ if (record.twitterDescription !== void 0)
4164
+ update.twitter_description = record.twitterDescription ?? null;
4165
+ if (record.twitterImageUrl !== void 0)
4166
+ update.twitter_image_url = record.twitterImageUrl ?? null;
4167
+ if (record.twitterSite !== void 0)
4168
+ update.twitter_site = record.twitterSite ?? null;
4169
+ if (record.twitterCreator !== void 0)
4170
+ update.twitter_creator = record.twitterCreator ?? null;
4171
+ if (record.canonicalUrl !== void 0)
4172
+ update.canonical_url = record.canonicalUrl ?? null;
4173
+ if (record.robots !== void 0) update.robots = record.robots ?? null;
4174
+ if (record.author !== void 0) update.author = record.author ?? null;
4175
+ if (record.publishedTime !== void 0)
4176
+ update.published_time = record.publishedTime?.toISOString() ?? null;
4177
+ if (record.modifiedTime !== void 0)
4178
+ update.modified_time = record.modifiedTime?.toISOString() ?? null;
4179
+ if (record.structuredData !== void 0)
4180
+ update.structured_data = record.structuredData ?? null;
4181
+ if (record.validationStatus !== void 0)
4182
+ update.validation_status = record.validationStatus ?? null;
4183
+ if (record.lastValidatedAt !== void 0)
4184
+ update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;
4185
+ if (record.validationErrors !== void 0)
4186
+ update.validation_errors = record.validationErrors ?? null;
4187
+ return update;
4188
+ }
4189
+ async function getSEORecords() {
4055
4190
  try {
4056
- const response = await fetch(imageUrl, {
4057
- headers: {
4058
- "User-Agent": "Mozilla/5.0 (compatible; SEO-Console/1.0; +https://example.com/bot)"
4059
- },
4060
- signal: AbortSignal.timeout(15e3)
4061
- // 15 second timeout for images
4062
- });
4063
- if (!response.ok) {
4191
+ const supabase = await createClient();
4192
+ const {
4193
+ data: { user }
4194
+ } = await supabase.auth.getUser();
4195
+ if (!user) {
4064
4196
  return {
4065
- isValid: false,
4066
- issues: [
4067
- {
4068
- field: "image",
4069
- severity: "critical",
4070
- message: `Failed to fetch image: ${response.status} ${response.statusText}`,
4071
- actual: imageUrl
4072
- }
4073
- ]
4197
+ success: false,
4198
+ error: new Error("User not authenticated")
4074
4199
  };
4075
4200
  }
4076
- const imageBuffer = await response.arrayBuffer();
4077
- const buffer = Buffer.from(imageBuffer);
4078
- const sizeInMB = buffer.length / (1024 * 1024);
4079
- if (sizeInMB > 1) {
4080
- issues.push({
4081
- field: "image",
4082
- severity: "warning",
4083
- message: "Image file size exceeds 1MB recommendation",
4084
- actual: `${sizeInMB.toFixed(2)}MB`
4085
- });
4201
+ const { data, error } = await supabase.from("seo_records").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
4202
+ if (error) {
4203
+ return { success: false, error };
4086
4204
  }
4087
- const metadata = await (0, import_sharp.default)(buffer).metadata();
4088
- const { width, height, format } = metadata;
4089
- if (!width || !height) {
4205
+ const records = (data || []).map(transformRowToSEORecord);
4206
+ return { success: true, data: records };
4207
+ } catch (error) {
4208
+ return {
4209
+ success: false,
4210
+ error: error instanceof Error ? error : new Error("Unknown error")
4211
+ };
4212
+ }
4213
+ }
4214
+ async function getSEORecordById(id) {
4215
+ try {
4216
+ const supabase = await createClient();
4217
+ const {
4218
+ data: { user }
4219
+ } = await supabase.auth.getUser();
4220
+ if (!user) {
4090
4221
  return {
4091
- isValid: false,
4092
- issues: [
4093
- {
4094
- field: "image",
4095
- severity: "critical",
4096
- message: "Could not determine image dimensions",
4097
- actual: imageUrl
4098
- }
4099
- ]
4222
+ success: false,
4223
+ error: new Error("User not authenticated")
4100
4224
  };
4101
4225
  }
4102
- const recommendedWidth = 1200;
4103
- const recommendedHeight = 630;
4104
- const aspectRatio = width / height;
4105
- const recommendedAspectRatio = recommendedWidth / recommendedHeight;
4106
- if (width < recommendedWidth || height < recommendedHeight) {
4107
- issues.push({
4108
- field: "image",
4109
- severity: "warning",
4110
- message: `Image dimensions below recommended size (${recommendedWidth}x${recommendedHeight})`,
4111
- expected: `${recommendedWidth}x${recommendedHeight}`,
4112
- actual: `${width}x${height}`
4113
- });
4226
+ const { data, error } = await supabase.from("seo_records").select("*").eq("id", id).eq("user_id", user.id).single();
4227
+ if (error) {
4228
+ return { success: false, error };
4114
4229
  }
4115
- if (Math.abs(aspectRatio - recommendedAspectRatio) > 0.1) {
4116
- issues.push({
4117
- field: "image",
4118
- severity: "info",
4119
- message: "Image aspect ratio differs from recommended 1.91:1",
4120
- expected: "1.91:1",
4121
- actual: `${aspectRatio.toFixed(2)}:1`
4122
- });
4230
+ if (!data) {
4231
+ return {
4232
+ success: false,
4233
+ error: new Error("SEO record not found")
4234
+ };
4123
4235
  }
4124
- const supportedFormats = ["jpeg", "jpg", "png", "webp", "avif", "gif"];
4125
- if (!format || !supportedFormats.includes(format.toLowerCase())) {
4126
- issues.push({
4127
- field: "image",
4128
- severity: "warning",
4129
- message: "Image format may not be optimal for social sharing",
4130
- expected: "JPEG, PNG, WebP, or AVIF",
4131
- actual: format || "unknown"
4132
- });
4236
+ return { success: true, data: transformRowToSEORecord(data) };
4237
+ } catch (error) {
4238
+ return {
4239
+ success: false,
4240
+ error: error instanceof Error ? error : new Error("Unknown error")
4241
+ };
4242
+ }
4243
+ }
4244
+ async function getSEORecordByRoute(routePath) {
4245
+ try {
4246
+ const supabase = await createClient();
4247
+ const {
4248
+ data: { user }
4249
+ } = await supabase.auth.getUser();
4250
+ if (!user) {
4251
+ return {
4252
+ success: false,
4253
+ error: new Error("User not authenticated")
4254
+ };
4133
4255
  }
4134
- if (expectedWidth && width !== expectedWidth) {
4135
- issues.push({
4136
- field: "image",
4137
- severity: "warning",
4138
- message: "Image width does not match expected value",
4139
- expected: `${expectedWidth}px`,
4140
- actual: `${width}px`
4141
- });
4256
+ const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
4257
+ if (error) {
4258
+ return { success: false, error };
4142
4259
  }
4143
- if (expectedHeight && height !== expectedHeight) {
4144
- issues.push({
4145
- field: "image",
4146
- severity: "warning",
4147
- message: "Image height does not match expected value",
4148
- expected: `${expectedHeight}px`,
4149
- actual: `${height}px`
4150
- });
4260
+ if (!data) {
4261
+ return { success: true, data: null };
4151
4262
  }
4152
- return {
4153
- isValid: issues.filter((i) => i.severity === "critical").length === 0,
4154
- issues,
4155
- metadata: {
4156
- width,
4157
- height,
4158
- format: format || "unknown",
4159
- size: buffer.length
4160
- }
4161
- };
4263
+ return { success: true, data: transformRowToSEORecord(data) };
4162
4264
  } catch (error) {
4163
4265
  return {
4164
- isValid: false,
4165
- issues: [
4166
- {
4167
- field: "image",
4168
- severity: "critical",
4169
- message: error instanceof Error ? error.message : "Failed to validate image",
4170
- actual: imageUrl
4171
- }
4172
- ]
4266
+ success: false,
4267
+ error: error instanceof Error ? error : new Error("Unknown error")
4173
4268
  };
4174
4269
  }
4175
4270
  }
4176
-
4177
- // src/lib/validation/html-validator.ts
4178
- var cheerio = __toESM(require("cheerio"));
4179
- async function validateHTML(html, record, _baseUrl) {
4180
- const issues = [];
4181
- const $ = cheerio.load(html);
4182
- const title = $("title").text().trim();
4183
- if (record.title) {
4184
- if (!title) {
4185
- issues.push({
4186
- field: "title",
4187
- severity: "critical",
4188
- message: "Title tag is missing",
4189
- expected: record.title
4190
- });
4191
- } else if (title !== record.title) {
4192
- issues.push({
4193
- field: "title",
4194
- severity: "warning",
4195
- message: "Title tag does not match SEO record",
4196
- expected: record.title,
4197
- actual: title
4198
- });
4271
+ async function createSEORecord(record) {
4272
+ try {
4273
+ const supabase = await createClient();
4274
+ const {
4275
+ data: { user }
4276
+ } = await supabase.auth.getUser();
4277
+ if (!user) {
4278
+ return {
4279
+ success: false,
4280
+ error: new Error("User not authenticated")
4281
+ };
4199
4282
  }
4200
- if (title.length > 60) {
4201
- issues.push({
4202
- field: "title",
4203
- severity: "warning",
4204
- message: "Title exceeds recommended 60 characters",
4205
- actual: `${title.length} characters`
4206
- });
4283
+ const insertData = transformToInsert({ ...record, userId: user.id });
4284
+ const { data, error } = await supabase.from("seo_records").insert(insertData).select().single();
4285
+ if (error) {
4286
+ return { success: false, error };
4207
4287
  }
4288
+ return { success: true, data: transformRowToSEORecord(data) };
4289
+ } catch (error) {
4290
+ return {
4291
+ success: false,
4292
+ error: error instanceof Error ? error : new Error("Unknown error")
4293
+ };
4208
4294
  }
4209
- const metaDescription = $('meta[name="description"]').attr("content")?.trim();
4210
- if (record.description) {
4211
- if (!metaDescription) {
4212
- issues.push({
4213
- field: "description",
4214
- severity: "critical",
4215
- message: "Meta description is missing",
4216
- expected: record.description
4217
- });
4218
- } else if (metaDescription !== record.description) {
4219
- issues.push({
4220
- field: "description",
4221
- severity: "warning",
4222
- message: "Meta description does not match SEO record",
4223
- expected: record.description,
4224
- actual: metaDescription
4225
- });
4295
+ }
4296
+ async function updateSEORecord(record) {
4297
+ try {
4298
+ const supabase = await createClient();
4299
+ const {
4300
+ data: { user }
4301
+ } = await supabase.auth.getUser();
4302
+ if (!user) {
4303
+ return {
4304
+ success: false,
4305
+ error: new Error("User not authenticated")
4306
+ };
4226
4307
  }
4227
- if (metaDescription && metaDescription.length > 160) {
4228
- issues.push({
4229
- field: "description",
4230
- severity: "warning",
4231
- message: "Description exceeds recommended 160 characters",
4232
- actual: `${metaDescription.length} characters`
4233
- });
4308
+ const { id, ...updateData } = record;
4309
+ const transformedUpdate = transformToUpdate(updateData);
4310
+ const { data, error } = await supabase.from("seo_records").update(transformedUpdate).eq("id", id).eq("user_id", user.id).select().single();
4311
+ if (error) {
4312
+ return { success: false, error };
4313
+ }
4314
+ if (!data) {
4315
+ return {
4316
+ success: false,
4317
+ error: new Error("SEO record not found")
4318
+ };
4234
4319
  }
4320
+ return { success: true, data: transformRowToSEORecord(data) };
4321
+ } catch (error) {
4322
+ return {
4323
+ success: false,
4324
+ error: error instanceof Error ? error : new Error("Unknown error")
4325
+ };
4235
4326
  }
4236
- if (record.ogTitle || record.ogDescription || record.ogImageUrl) {
4237
- const ogTitle = $('meta[property="og:title"]').attr("content");
4238
- const ogDescription = $('meta[property="og:description"]').attr("content");
4239
- const ogImage = $('meta[property="og:image"]').attr("content");
4240
- const ogType = $('meta[property="og:type"]').attr("content");
4241
- const ogUrl = $('meta[property="og:url"]').attr("content");
4242
- if (record.ogTitle && !ogTitle) {
4243
- issues.push({
4244
- field: "og:title",
4245
- severity: "critical",
4246
- message: "Open Graph title is missing",
4247
- expected: record.ogTitle
4248
- });
4327
+ }
4328
+ async function deleteSEORecord(id) {
4329
+ try {
4330
+ const supabase = await createClient();
4331
+ const {
4332
+ data: { user }
4333
+ } = await supabase.auth.getUser();
4334
+ if (!user) {
4335
+ return {
4336
+ success: false,
4337
+ error: new Error("User not authenticated")
4338
+ };
4249
4339
  }
4250
- if (record.ogDescription && !ogDescription) {
4251
- issues.push({
4252
- field: "og:description",
4253
- severity: "warning",
4254
- message: "Open Graph description is missing",
4255
- expected: record.ogDescription
4256
- });
4340
+ const { error } = await supabase.from("seo_records").delete().eq("id", id).eq("user_id", user.id);
4341
+ if (error) {
4342
+ return { success: false, error };
4257
4343
  }
4258
- if (record.ogImageUrl && !ogImage) {
4259
- issues.push({
4260
- field: "og:image",
4261
- severity: "critical",
4262
- message: "Open Graph image is missing",
4263
- expected: record.ogImageUrl
4264
- });
4344
+ return { success: true, data: void 0 };
4345
+ } catch (error) {
4346
+ return {
4347
+ success: false,
4348
+ error: error instanceof Error ? error : new Error("Unknown error")
4349
+ };
4350
+ }
4351
+ }
4352
+
4353
+ // src/lib/storage/supabase-storage.ts
4354
+ var SupabaseStorage = class {
4355
+ constructor(supabaseUrl, supabaseKey) {
4356
+ this.supabaseUrl = supabaseUrl;
4357
+ this.supabaseKey = supabaseKey;
4358
+ if (typeof process !== "undefined") {
4359
+ process.env.NEXT_PUBLIC_SUPABASE_URL = supabaseUrl;
4360
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = supabaseKey;
4265
4361
  }
4266
- if (record.ogType && ogType !== record.ogType) {
4267
- issues.push({
4268
- field: "og:type",
4269
- severity: "warning",
4270
- message: "Open Graph type does not match",
4271
- expected: record.ogType,
4272
- actual: ogType
4273
- });
4362
+ }
4363
+ async isAvailable() {
4364
+ try {
4365
+ const result = await getSEORecords();
4366
+ return result.success;
4367
+ } catch {
4368
+ return false;
4274
4369
  }
4275
- if (record.ogUrl && ogUrl !== record.ogUrl) {
4276
- issues.push({
4277
- field: "og:url",
4278
- severity: "warning",
4279
- message: "Open Graph URL does not match",
4280
- expected: record.ogUrl,
4281
- actual: ogUrl
4282
- });
4370
+ }
4371
+ async getRecords() {
4372
+ const result = await getSEORecords();
4373
+ if (!result.success) {
4374
+ throw new Error(result.error?.message || "Failed to get records");
4283
4375
  }
4376
+ return result.data;
4284
4377
  }
4285
- if (record.twitterCard || record.twitterTitle || record.twitterImageUrl) {
4286
- const twitterCard = $('meta[name="twitter:card"]').attr("content");
4287
- const twitterTitle = $('meta[name="twitter:title"]').attr("content");
4288
- const _twitterDescription = $('meta[name="twitter:description"]').attr("content");
4289
- const twitterImage = $('meta[name="twitter:image"]').attr("content");
4290
- if (record.twitterCard && twitterCard !== record.twitterCard) {
4291
- issues.push({
4292
- field: "twitter:card",
4293
- severity: "warning",
4294
- message: "Twitter card type does not match",
4295
- expected: record.twitterCard,
4296
- actual: twitterCard
4297
- });
4378
+ async getRecordById(id) {
4379
+ const result = await getSEORecordById(id);
4380
+ if (!result.success) {
4381
+ if (result.error?.message?.includes("not found")) {
4382
+ return null;
4383
+ }
4384
+ throw new Error(result.error?.message || "Failed to get record");
4298
4385
  }
4299
- if (record.twitterTitle && !twitterTitle) {
4300
- issues.push({
4301
- field: "twitter:title",
4302
- severity: "warning",
4303
- message: "Twitter title is missing",
4304
- expected: record.twitterTitle
4305
- });
4386
+ return result.data || null;
4387
+ }
4388
+ async getRecordByRoute(routePath) {
4389
+ const result = await getSEORecordByRoute(routePath);
4390
+ if (!result.success) {
4391
+ if (result.error?.message?.includes("not found")) {
4392
+ return null;
4393
+ }
4394
+ throw new Error(result.error?.message || "Failed to get record");
4306
4395
  }
4307
- if (record.twitterImageUrl && !twitterImage) {
4308
- issues.push({
4309
- field: "twitter:image",
4310
- severity: "warning",
4311
- message: "Twitter image is missing",
4312
- expected: record.twitterImageUrl
4313
- });
4396
+ return result.data || null;
4397
+ }
4398
+ async createRecord(record) {
4399
+ const result = await createSEORecord(record);
4400
+ if (!result.success) {
4401
+ throw new Error(result.error?.message || "Failed to create record");
4314
4402
  }
4403
+ return result.data;
4315
4404
  }
4316
- if (record.canonicalUrl) {
4317
- const canonical = $('link[rel="canonical"]').attr("href");
4318
- if (!canonical) {
4319
- issues.push({
4320
- field: "canonical",
4321
- severity: "critical",
4322
- message: "Canonical URL is missing",
4323
- expected: record.canonicalUrl
4324
- });
4325
- } else if (canonical !== record.canonicalUrl) {
4326
- issues.push({
4327
- field: "canonical",
4328
- severity: "warning",
4329
- message: "Canonical URL does not match",
4330
- expected: record.canonicalUrl,
4331
- actual: canonical
4332
- });
4405
+ async updateRecord(record) {
4406
+ const result = await updateSEORecord(record);
4407
+ if (!result.success) {
4408
+ throw new Error(result.error?.message || "Failed to update record");
4333
4409
  }
4334
- if (canonical && !canonical.startsWith("http://") && !canonical.startsWith("https://")) {
4335
- issues.push({
4336
- field: "canonical",
4337
- severity: "warning",
4338
- message: "Canonical URL should be absolute",
4339
- actual: canonical
4340
- });
4410
+ return result.data;
4411
+ }
4412
+ async deleteRecord(id) {
4413
+ const result = await deleteSEORecord(id);
4414
+ if (!result.success) {
4415
+ throw new Error(result.error?.message || "Failed to delete record");
4341
4416
  }
4342
4417
  }
4418
+ };
4419
+
4420
+ // src/lib/storage/storage-factory.ts
4421
+ function createStorageAdapter(config) {
4422
+ switch (config.type) {
4423
+ case "file":
4424
+ return new FileStorage(config.filePath || "seo-records.json");
4425
+ case "supabase":
4426
+ if (!config.supabaseUrl || !config.supabaseKey) {
4427
+ throw new Error("Supabase URL and key are required for Supabase storage");
4428
+ }
4429
+ return new SupabaseStorage(config.supabaseUrl, config.supabaseKey);
4430
+ case "memory":
4431
+ return new FileStorage(":memory:");
4432
+ default:
4433
+ throw new Error(`Unsupported storage type: ${config.type}`);
4434
+ }
4435
+ }
4436
+ function detectStorageConfig() {
4437
+ if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
4438
+ return {
4439
+ type: "supabase",
4440
+ supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
4441
+ supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
4442
+ };
4443
+ }
4444
+ if (process.env.SEO_CONSOLE_STORAGE_PATH) {
4445
+ return {
4446
+ type: "file",
4447
+ filePath: process.env.SEO_CONSOLE_STORAGE_PATH
4448
+ };
4449
+ }
4343
4450
  return {
4344
- isValid: issues.filter((i) => i.severity === "critical").length === 0,
4345
- issues,
4346
- validatedAt: /* @__PURE__ */ new Date()
4451
+ type: "file",
4452
+ filePath: "seo-records.json"
4347
4453
  };
4348
4454
  }
4349
4455
  // Annotate the CommonJS export names for ESM import in node:
@@ -4355,23 +4461,29 @@ async function validateHTML(html, record, _baseUrl) {
4355
4461
  CardFooter,
4356
4462
  CardHeader,
4357
4463
  CardTitle,
4464
+ FileStorage,
4358
4465
  Input,
4359
4466
  OGImagePreview,
4360
4467
  SEORecordForm,
4361
4468
  SEORecordList,
4362
4469
  Spinner,
4363
4470
  ValidationDashboard,
4364
- createSEORecord,
4471
+ crawlSiteForSEO,
4365
4472
  createSEORecordSchema,
4366
- deleteSEORecord,
4367
- getAllSEORecords,
4368
- getRoutePathFromParams,
4369
- getSEORecordById,
4370
- getSEORecordByRoute,
4371
- updateSEORecord,
4473
+ createStorageAdapter,
4474
+ detectStorageConfig,
4475
+ discoverNextJSRoutes,
4476
+ extractMetadataFromHTML,
4477
+ extractMetadataFromURL,
4478
+ extractSitemapFromRobotsTxt,
4479
+ generateExamplePaths,
4480
+ generateRobotsTxt,
4481
+ generateSitemapFromRecords,
4482
+ generateSitemapXML,
4483
+ metadataToSEORecord,
4484
+ seoRecordsToSitemapEntries,
4485
+ updateRobotsTxtWithSitemap,
4372
4486
  updateSEORecordSchema,
4373
- useGenerateMetadata,
4374
- validateHTML,
4375
- validateOGImage
4487
+ validateSitemapEntry
4376
4488
  });
4377
4489
  //# sourceMappingURL=index.js.map