@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.mjs CHANGED
@@ -1,415 +1,3 @@
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/hooks/useGenerateMetadata.ts
307
- async function useGenerateMetadata(options = {}) {
308
- const { routePath, fallback = {} } = options;
309
- if (!routePath) {
310
- return {
311
- title: fallback.title,
312
- description: fallback.description,
313
- ...fallback
314
- };
315
- }
316
- const result = await getSEORecordByRoute(routePath);
317
- if (!result.success || !result.data) {
318
- return {
319
- title: fallback.title,
320
- description: fallback.description,
321
- ...fallback
322
- };
323
- }
324
- const record = result.data;
325
- const metadata = {};
326
- if (record.title) {
327
- metadata.title = record.title;
328
- }
329
- if (record.description) {
330
- metadata.description = record.description;
331
- }
332
- if (record.keywords && record.keywords.length > 0) {
333
- metadata.keywords = record.keywords;
334
- }
335
- if (record.author) {
336
- metadata.authors = [{ name: record.author }];
337
- }
338
- if (record.ogTitle || record.ogDescription || record.ogImageUrl || record.ogType) {
339
- const supportedOGTypes = ["website", "article", "book", "profile"];
340
- const ogType = record.ogType && supportedOGTypes.includes(record.ogType) ? record.ogType : "website";
341
- const openGraph = {
342
- type: ogType,
343
- title: record.ogTitle || record.title || void 0,
344
- description: record.ogDescription || record.description || void 0,
345
- url: record.ogUrl || void 0,
346
- siteName: record.ogSiteName || void 0
347
- };
348
- if (record.ogImageUrl) {
349
- openGraph.images = [
350
- {
351
- url: record.ogImageUrl,
352
- width: record.ogImageWidth || void 0,
353
- height: record.ogImageHeight || void 0,
354
- alt: record.ogTitle || record.title || void 0
355
- }
356
- ];
357
- }
358
- if (ogType === "article") {
359
- const articleOpenGraph = {
360
- ...openGraph,
361
- ...record.publishedTime && {
362
- publishedTime: record.publishedTime.toISOString()
363
- },
364
- ...record.modifiedTime && {
365
- modifiedTime: record.modifiedTime.toISOString()
366
- }
367
- };
368
- metadata.openGraph = articleOpenGraph;
369
- } else {
370
- metadata.openGraph = openGraph;
371
- }
372
- }
373
- if (record.twitterCard || record.twitterTitle || record.twitterDescription || record.twitterImageUrl) {
374
- metadata.twitter = {
375
- card: record.twitterCard || "summary",
376
- title: record.twitterTitle || record.ogTitle || record.title || void 0,
377
- description: record.twitterDescription || record.ogDescription || record.description || void 0,
378
- images: record.twitterImageUrl ? [record.twitterImageUrl] : void 0,
379
- site: record.twitterSite || void 0,
380
- creator: record.twitterCreator || void 0
381
- };
382
- }
383
- if (record.canonicalUrl) {
384
- metadata.alternates = {
385
- canonical: record.canonicalUrl
386
- };
387
- }
388
- if (record.robots) {
389
- metadata.robots = record.robots;
390
- }
391
- return {
392
- ...fallback,
393
- ...metadata,
394
- // Ensure title and description from record override fallback if present
395
- title: record.title || fallback.title,
396
- description: record.description || fallback.description,
397
- // Merge openGraph if both exist
398
- openGraph: fallback.openGraph ? { ...metadata.openGraph, ...fallback.openGraph } : metadata.openGraph,
399
- // Merge twitter if both exist
400
- twitter: fallback.twitter ? { ...metadata.twitter, ...fallback.twitter } : metadata.twitter
401
- };
402
- }
403
- function getRoutePathFromParams(params, pattern) {
404
- let routePath = pattern;
405
- for (const [key, value] of Object.entries(params)) {
406
- const paramValue = Array.isArray(value) ? value.join("/") : value;
407
- routePath = routePath.replace(`[${key}]`, paramValue);
408
- routePath = routePath.replace(`[...${key}]`, paramValue);
409
- }
410
- return routePath;
411
- }
412
-
413
1
  // src/components/seo/SEORecordList.tsx
414
2
  import { useState as useState3, useEffect as useEffect2 } from "react";
415
3
 
@@ -3776,7 +3364,9 @@ function SEORecordList() {
3776
3364
  // src/components/seo/ValidationDashboard.tsx
3777
3365
  import { useState as useState4, useEffect as useEffect3 } from "react";
3778
3366
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
3779
- var Link = ({ href, children, ...props }) => /* @__PURE__ */ jsx8("a", { href, ...props, children });
3367
+ var Link = ({ href, children, ...props }) => {
3368
+ return /* @__PURE__ */ jsx8("a", { href, ...props, children });
3369
+ };
3780
3370
  function ValidationDashboard() {
3781
3371
  const [records, setRecords] = useState4([]);
3782
3372
  const [loading, setLoading] = useState4(true);
@@ -3987,303 +3577,813 @@ function ValidationDashboard() {
3987
3577
  ] })
3988
3578
  ] });
3989
3579
  }
3990
-
3991
- // src/lib/validation/image-validator.ts
3992
- import sharp from "sharp";
3993
- async function validateOGImage(imageUrl, expectedWidth, expectedHeight) {
3994
- const issues = [];
3580
+
3581
+ // src/lib/route-discovery.ts
3582
+ import { join } from "path";
3583
+ import { glob } from "glob";
3584
+ async function discoverNextJSRoutes(appDir = "app", rootDir = process.cwd()) {
3585
+ const routes = [];
3586
+ const appPath = join(rootDir, appDir);
3587
+ try {
3588
+ const pageFiles = await glob("**/page.tsx", {
3589
+ cwd: appPath,
3590
+ absolute: false,
3591
+ ignore: ["**/node_modules/**", "**/.next/**"]
3592
+ });
3593
+ for (const file of pageFiles) {
3594
+ const route = fileToRoute(file, appDir);
3595
+ if (route) {
3596
+ routes.push(route);
3597
+ }
3598
+ }
3599
+ } catch (error) {
3600
+ console.error("Error discovering routes:", error);
3601
+ }
3602
+ return routes;
3603
+ }
3604
+ function fileToRoute(filePath, appDir) {
3605
+ let routePath = filePath.replace(/^app\//, "").replace(/\/page\.tsx$/, "").replace(/\/page$/, "");
3606
+ if (routePath === "page" || routePath === "") {
3607
+ routePath = "/";
3608
+ } else {
3609
+ routePath = "/" + routePath;
3610
+ }
3611
+ const segments = routePath.split("/").filter(Boolean);
3612
+ const params = [];
3613
+ let isDynamic = false;
3614
+ let isCatchAll = false;
3615
+ for (const segment of segments) {
3616
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
3617
+ const param = segment.slice(4, -1);
3618
+ params.push(param);
3619
+ isDynamic = true;
3620
+ isCatchAll = true;
3621
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
3622
+ const param = segment.slice(1, -1);
3623
+ params.push(param);
3624
+ isDynamic = true;
3625
+ }
3626
+ }
3627
+ return {
3628
+ routePath,
3629
+ filePath: join(appDir, filePath),
3630
+ isDynamic,
3631
+ isCatchAll,
3632
+ params
3633
+ };
3634
+ }
3635
+ function generateExamplePaths(route, count = 3) {
3636
+ if (!route.isDynamic) {
3637
+ return [route.routePath];
3638
+ }
3639
+ const examples = [];
3640
+ const segments = route.routePath.split("/").filter(Boolean);
3641
+ for (let i = 0; i < count; i++) {
3642
+ let examplePath = "";
3643
+ for (const segment of segments) {
3644
+ if (segment.startsWith("[...")) {
3645
+ examplePath += `/example-${i}-part-1/example-${i}-part-2`;
3646
+ } else if (segment.startsWith("[")) {
3647
+ examplePath += `/example-${i}`;
3648
+ } else {
3649
+ examplePath += `/${segment}`;
3650
+ }
3651
+ }
3652
+ examples.push(examplePath || "/");
3653
+ }
3654
+ return examples;
3655
+ }
3656
+
3657
+ // src/lib/metadata-extractor.ts
3658
+ import * as cheerio from "cheerio";
3659
+ function extractMetadataFromHTML(html, baseUrl) {
3660
+ const $ = cheerio.load(html);
3661
+ const metadata = {};
3662
+ metadata.title = $("title").text() || void 0;
3663
+ metadata.description = $('meta[name="description"]').attr("content") || void 0;
3664
+ metadata.robots = $('meta[name="robots"]').attr("content") || void 0;
3665
+ const keywords = $('meta[name="keywords"]').attr("content");
3666
+ if (keywords) {
3667
+ metadata.keywords = keywords.split(",").map((k) => k.trim());
3668
+ }
3669
+ metadata.ogTitle = $('meta[property="og:title"]').attr("content") || void 0;
3670
+ metadata.ogDescription = $('meta[property="og:description"]').attr("content") || void 0;
3671
+ metadata.ogImageUrl = $('meta[property="og:image"]').attr("content") || void 0;
3672
+ metadata.ogType = $('meta[property="og:type"]').attr("content") || void 0;
3673
+ metadata.ogUrl = $('meta[property="og:url"]').attr("content") || void 0;
3674
+ metadata.canonicalUrl = $('link[rel="canonical"]').attr("href") || void 0;
3675
+ if (baseUrl) {
3676
+ if (metadata.ogImageUrl && !metadata.ogImageUrl.startsWith("http")) {
3677
+ metadata.ogImageUrl = new URL(metadata.ogImageUrl, baseUrl).toString();
3678
+ }
3679
+ if (metadata.canonicalUrl && !metadata.canonicalUrl.startsWith("http")) {
3680
+ metadata.canonicalUrl = new URL(metadata.canonicalUrl, baseUrl).toString();
3681
+ }
3682
+ }
3683
+ return metadata;
3684
+ }
3685
+ async function extractMetadataFromURL(url) {
3686
+ try {
3687
+ const response = await fetch(url, {
3688
+ headers: {
3689
+ "User-Agent": "SEO-Console/1.0"
3690
+ }
3691
+ });
3692
+ if (!response.ok) {
3693
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
3694
+ }
3695
+ const html = await response.text();
3696
+ return extractMetadataFromHTML(html, url);
3697
+ } catch (error) {
3698
+ console.error(`Error extracting metadata from ${url}:`, error);
3699
+ return {};
3700
+ }
3701
+ }
3702
+ function metadataToSEORecord(metadata, routePath, userId = "extracted") {
3703
+ return {
3704
+ userId,
3705
+ routePath,
3706
+ title: metadata.title,
3707
+ description: metadata.description,
3708
+ keywords: metadata.keywords,
3709
+ ogTitle: metadata.ogTitle,
3710
+ ogDescription: metadata.ogDescription,
3711
+ ogImageUrl: metadata.ogImageUrl,
3712
+ ogType: metadata.ogType,
3713
+ ogUrl: metadata.ogUrl,
3714
+ canonicalUrl: metadata.canonicalUrl,
3715
+ robots: metadata.robots,
3716
+ validationStatus: "pending"
3717
+ };
3718
+ }
3719
+ async function crawlSiteForSEO(baseUrl, routes) {
3720
+ const results = /* @__PURE__ */ new Map();
3721
+ for (const route of routes) {
3722
+ const url = new URL(route, baseUrl).toString();
3723
+ try {
3724
+ const metadata = await extractMetadataFromURL(url);
3725
+ results.set(route, metadata);
3726
+ await new Promise((resolve) => setTimeout(resolve, 100));
3727
+ } catch (error) {
3728
+ console.error(`Failed to crawl ${url}:`, error);
3729
+ }
3730
+ }
3731
+ return results;
3732
+ }
3733
+
3734
+ // src/lib/sitemap-generator.ts
3735
+ function generateSitemapXML(options) {
3736
+ const { baseUrl, entries } = options;
3737
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
3738
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3739
+ ${entries.map((entry) => {
3740
+ const loc = entry.loc.startsWith("http") ? entry.loc : new URL(entry.loc, baseUrl).toString();
3741
+ return ` <url>
3742
+ <loc>${escapeXML(loc)}</loc>${entry.lastmod ? `
3743
+ <lastmod>${entry.lastmod}</lastmod>` : ""}${entry.changefreq ? `
3744
+ <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority !== void 0 ? `
3745
+ <priority>${entry.priority}</priority>` : ""}
3746
+ </url>`;
3747
+ }).join("\n")}
3748
+ </urlset>`;
3749
+ return xml;
3750
+ }
3751
+ function seoRecordsToSitemapEntries(records, baseUrl) {
3752
+ return records.filter((record) => {
3753
+ return record.canonicalUrl && record.canonicalUrl.trim() !== "";
3754
+ }).map((record) => {
3755
+ const entry = {
3756
+ loc: record.canonicalUrl
3757
+ };
3758
+ if (record.modifiedTime) {
3759
+ entry.lastmod = record.modifiedTime.toISOString().split("T")[0];
3760
+ } else if (record.lastValidatedAt) {
3761
+ entry.lastmod = record.lastValidatedAt.toISOString().split("T")[0];
3762
+ }
3763
+ if (record.routePath === "/") {
3764
+ entry.changefreq = "daily";
3765
+ entry.priority = 1;
3766
+ } else if (record.routePath.includes("/blog/") || record.routePath.includes("/posts/")) {
3767
+ entry.changefreq = "weekly";
3768
+ entry.priority = 0.8;
3769
+ } else {
3770
+ entry.changefreq = "monthly";
3771
+ entry.priority = 0.6;
3772
+ }
3773
+ return entry;
3774
+ }).sort((a, b) => {
3775
+ if (a.priority !== b.priority) {
3776
+ return (b.priority || 0) - (a.priority || 0);
3777
+ }
3778
+ return a.loc.localeCompare(b.loc);
3779
+ });
3780
+ }
3781
+ function generateSitemapFromRecords(records, baseUrl) {
3782
+ const entries = seoRecordsToSitemapEntries(records, baseUrl);
3783
+ return generateSitemapXML({ baseUrl, entries });
3784
+ }
3785
+ function escapeXML(str) {
3786
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3787
+ }
3788
+ function validateSitemapEntry(entry) {
3789
+ const errors = [];
3790
+ if (!entry.loc) {
3791
+ errors.push("Location (loc) is required");
3792
+ } else {
3793
+ try {
3794
+ new URL(entry.loc);
3795
+ } catch {
3796
+ errors.push("Location must be a valid URL");
3797
+ }
3798
+ }
3799
+ if (entry.priority !== void 0) {
3800
+ if (entry.priority < 0 || entry.priority > 1) {
3801
+ errors.push("Priority must be between 0.0 and 1.0");
3802
+ }
3803
+ }
3804
+ if (entry.lastmod) {
3805
+ const date = new Date(entry.lastmod);
3806
+ if (isNaN(date.getTime())) {
3807
+ errors.push("Lastmod must be a valid date (YYYY-MM-DD)");
3808
+ }
3809
+ }
3810
+ return {
3811
+ valid: errors.length === 0,
3812
+ errors
3813
+ };
3814
+ }
3815
+
3816
+ // src/lib/robots-generator.ts
3817
+ function generateRobotsTxt(options = {}) {
3818
+ const { userAgents = [], sitemapUrl, crawlDelay } = options;
3819
+ let content = "";
3820
+ if (userAgents.length === 0) {
3821
+ content += "User-agent: *\n";
3822
+ if (crawlDelay) {
3823
+ content += `Crawl-delay: ${crawlDelay}
3824
+ `;
3825
+ }
3826
+ content += "Allow: /\n";
3827
+ content += "\n";
3828
+ } else {
3829
+ for (const ua of userAgents) {
3830
+ content += `User-agent: ${ua.agent}
3831
+ `;
3832
+ if (crawlDelay) {
3833
+ content += `Crawl-delay: ${crawlDelay}
3834
+ `;
3835
+ }
3836
+ if (ua.allow) {
3837
+ for (const path of ua.allow) {
3838
+ content += `Allow: ${path}
3839
+ `;
3840
+ }
3841
+ }
3842
+ if (ua.disallow) {
3843
+ for (const path of ua.disallow) {
3844
+ content += `Disallow: ${path}
3845
+ `;
3846
+ }
3847
+ }
3848
+ content += "\n";
3849
+ }
3850
+ }
3851
+ if (sitemapUrl) {
3852
+ content += `Sitemap: ${sitemapUrl}
3853
+ `;
3854
+ }
3855
+ return content.trim();
3856
+ }
3857
+ function updateRobotsTxtWithSitemap(existingContent, sitemapUrl) {
3858
+ const sitemapRegex = /^Sitemap:\s*.+$/m;
3859
+ if (sitemapRegex.test(existingContent)) {
3860
+ return existingContent.replace(sitemapRegex, `Sitemap: ${sitemapUrl}`);
3861
+ }
3862
+ const trimmed = existingContent.trim();
3863
+ return trimmed ? `${trimmed}
3864
+
3865
+ Sitemap: ${sitemapUrl}` : `Sitemap: ${sitemapUrl}`;
3866
+ }
3867
+ function extractSitemapFromRobotsTxt(content) {
3868
+ const match = content.match(/^Sitemap:\s*(.+)$/m);
3869
+ return match ? match[1].trim() : null;
3870
+ }
3871
+
3872
+ // src/lib/storage/file-storage.ts
3873
+ import { promises as fs } from "fs";
3874
+ var FileStorage = class {
3875
+ constructor(filePath = "seo-records.json") {
3876
+ this.records = [];
3877
+ this.initialized = false;
3878
+ this.filePath = filePath;
3879
+ }
3880
+ async ensureInitialized() {
3881
+ if (this.initialized) return;
3882
+ try {
3883
+ const data = await fs.readFile(this.filePath, "utf-8");
3884
+ this.records = JSON.parse(data);
3885
+ } catch (error) {
3886
+ if (error.code === "ENOENT") {
3887
+ this.records = [];
3888
+ await this.save();
3889
+ } else {
3890
+ throw error;
3891
+ }
3892
+ }
3893
+ this.initialized = true;
3894
+ }
3895
+ async save() {
3896
+ await fs.writeFile(this.filePath, JSON.stringify(this.records, null, 2), "utf-8");
3897
+ }
3898
+ async isAvailable() {
3899
+ try {
3900
+ const dir = this.filePath.includes("/") ? this.filePath.substring(0, this.filePath.lastIndexOf("/")) : ".";
3901
+ await fs.access(dir);
3902
+ return true;
3903
+ } catch {
3904
+ return false;
3905
+ }
3906
+ }
3907
+ async getRecords() {
3908
+ await this.ensureInitialized();
3909
+ return [...this.records];
3910
+ }
3911
+ async getRecordById(id) {
3912
+ await this.ensureInitialized();
3913
+ return this.records.find((r) => r.id === id) || null;
3914
+ }
3915
+ async getRecordByRoute(routePath) {
3916
+ await this.ensureInitialized();
3917
+ return this.records.find((r) => r.routePath === routePath) || null;
3918
+ }
3919
+ async createRecord(record) {
3920
+ await this.ensureInitialized();
3921
+ const newRecord = {
3922
+ id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
3923
+ userId: "file-user",
3924
+ // File storage doesn't need user IDs
3925
+ routePath: record.routePath,
3926
+ title: record.title,
3927
+ description: record.description,
3928
+ keywords: record.keywords,
3929
+ ogTitle: record.ogTitle,
3930
+ ogDescription: record.ogDescription,
3931
+ ogImageUrl: record.ogImageUrl,
3932
+ ogImageWidth: record.ogImageWidth,
3933
+ ogImageHeight: record.ogImageHeight,
3934
+ ogType: record.ogType,
3935
+ ogUrl: record.ogUrl,
3936
+ ogSiteName: record.ogSiteName,
3937
+ twitterCard: record.twitterCard,
3938
+ twitterTitle: record.twitterTitle,
3939
+ twitterDescription: record.twitterDescription,
3940
+ twitterImageUrl: record.twitterImageUrl,
3941
+ twitterSite: record.twitterSite,
3942
+ twitterCreator: record.twitterCreator,
3943
+ canonicalUrl: record.canonicalUrl,
3944
+ robots: record.robots,
3945
+ author: record.author,
3946
+ publishedTime: record.publishedTime,
3947
+ modifiedTime: record.modifiedTime,
3948
+ structuredData: record.structuredData,
3949
+ validationStatus: "pending",
3950
+ lastValidatedAt: void 0,
3951
+ validationErrors: void 0
3952
+ };
3953
+ this.records.push(newRecord);
3954
+ await this.save();
3955
+ return newRecord;
3956
+ }
3957
+ async updateRecord(record) {
3958
+ await this.ensureInitialized();
3959
+ const index = this.records.findIndex((r) => r.id === record.id);
3960
+ if (index === -1) {
3961
+ throw new Error(`SEO record with id ${record.id} not found`);
3962
+ }
3963
+ const updated = {
3964
+ ...this.records[index],
3965
+ ...record
3966
+ };
3967
+ this.records[index] = updated;
3968
+ await this.save();
3969
+ return updated;
3970
+ }
3971
+ async deleteRecord(id) {
3972
+ await this.ensureInitialized();
3973
+ const index = this.records.findIndex((r) => r.id === id);
3974
+ if (index === -1) {
3975
+ throw new Error(`SEO record with id ${id} not found`);
3976
+ }
3977
+ this.records.splice(index, 1);
3978
+ await this.save();
3979
+ }
3980
+ };
3981
+
3982
+ // src/lib/supabase/server.ts
3983
+ import { createServerClient } from "@supabase/ssr";
3984
+ import { cookies } from "next/headers";
3985
+ async function createClient() {
3986
+ const cookieStore = await cookies();
3987
+ return createServerClient(
3988
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
3989
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
3990
+ {
3991
+ cookies: {
3992
+ getAll() {
3993
+ return cookieStore.getAll();
3994
+ },
3995
+ setAll(cookiesToSet) {
3996
+ try {
3997
+ cookiesToSet.forEach(
3998
+ ({ name, value, options }) => cookieStore.set(name, value, options)
3999
+ );
4000
+ } catch {
4001
+ }
4002
+ }
4003
+ }
4004
+ }
4005
+ );
4006
+ }
4007
+
4008
+ // src/lib/database/seo-records.ts
4009
+ function transformRowToSEORecord(row) {
4010
+ return {
4011
+ id: row.id,
4012
+ userId: row.user_id,
4013
+ routePath: row.route_path,
4014
+ title: row.title ?? void 0,
4015
+ description: row.description ?? void 0,
4016
+ keywords: row.keywords ?? void 0,
4017
+ ogTitle: row.og_title ?? void 0,
4018
+ ogDescription: row.og_description ?? void 0,
4019
+ ogImageUrl: row.og_image_url ?? void 0,
4020
+ ogImageWidth: row.og_image_width ?? void 0,
4021
+ ogImageHeight: row.og_image_height ?? void 0,
4022
+ ogType: row.og_type ?? void 0,
4023
+ ogUrl: row.og_url ?? void 0,
4024
+ ogSiteName: row.og_site_name ?? void 0,
4025
+ twitterCard: row.twitter_card ?? void 0,
4026
+ twitterTitle: row.twitter_title ?? void 0,
4027
+ twitterDescription: row.twitter_description ?? void 0,
4028
+ twitterImageUrl: row.twitter_image_url ?? void 0,
4029
+ twitterSite: row.twitter_site ?? void 0,
4030
+ twitterCreator: row.twitter_creator ?? void 0,
4031
+ canonicalUrl: row.canonical_url ?? void 0,
4032
+ robots: row.robots ?? void 0,
4033
+ author: row.author ?? void 0,
4034
+ publishedTime: row.published_time ? new Date(row.published_time) : void 0,
4035
+ modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
4036
+ structuredData: row.structured_data ? row.structured_data : void 0,
4037
+ validationStatus: row.validation_status ?? void 0,
4038
+ lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
4039
+ validationErrors: row.validation_errors ? row.validation_errors : void 0,
4040
+ createdAt: new Date(row.created_at),
4041
+ updatedAt: new Date(row.updated_at)
4042
+ };
4043
+ }
4044
+ function transformToInsert(record) {
4045
+ return {
4046
+ user_id: record.userId,
4047
+ route_path: record.routePath,
4048
+ title: record.title ?? null,
4049
+ description: record.description ?? null,
4050
+ keywords: record.keywords ?? null,
4051
+ og_title: record.ogTitle ?? null,
4052
+ og_description: record.ogDescription ?? null,
4053
+ og_image_url: record.ogImageUrl ?? null,
4054
+ og_image_width: record.ogImageWidth ?? null,
4055
+ og_image_height: record.ogImageHeight ?? null,
4056
+ og_type: record.ogType ?? null,
4057
+ og_url: record.ogUrl ?? null,
4058
+ og_site_name: record.ogSiteName ?? null,
4059
+ twitter_card: record.twitterCard ?? null,
4060
+ twitter_title: record.twitterTitle ?? null,
4061
+ twitter_description: record.twitterDescription ?? null,
4062
+ twitter_image_url: record.twitterImageUrl ?? null,
4063
+ twitter_site: record.twitterSite ?? null,
4064
+ twitter_creator: record.twitterCreator ?? null,
4065
+ canonical_url: record.canonicalUrl ?? null,
4066
+ robots: record.robots ?? null,
4067
+ author: record.author ?? null,
4068
+ published_time: record.publishedTime?.toISOString() ?? null,
4069
+ modified_time: record.modifiedTime?.toISOString() ?? null,
4070
+ structured_data: record.structuredData ?? null
4071
+ };
4072
+ }
4073
+ function transformToUpdate(record) {
4074
+ const update = {};
4075
+ if (record.routePath !== void 0) update.route_path = record.routePath;
4076
+ if (record.title !== void 0) update.title = record.title ?? null;
4077
+ if (record.description !== void 0)
4078
+ update.description = record.description ?? null;
4079
+ if (record.keywords !== void 0) update.keywords = record.keywords ?? null;
4080
+ if (record.ogTitle !== void 0) update.og_title = record.ogTitle ?? null;
4081
+ if (record.ogDescription !== void 0)
4082
+ update.og_description = record.ogDescription ?? null;
4083
+ if (record.ogImageUrl !== void 0)
4084
+ update.og_image_url = record.ogImageUrl ?? null;
4085
+ if (record.ogImageWidth !== void 0)
4086
+ update.og_image_width = record.ogImageWidth ?? null;
4087
+ if (record.ogImageHeight !== void 0)
4088
+ update.og_image_height = record.ogImageHeight ?? null;
4089
+ if (record.ogType !== void 0) update.og_type = record.ogType ?? null;
4090
+ if (record.ogUrl !== void 0) update.og_url = record.ogUrl ?? null;
4091
+ if (record.ogSiteName !== void 0)
4092
+ update.og_site_name = record.ogSiteName ?? null;
4093
+ if (record.twitterCard !== void 0)
4094
+ update.twitter_card = record.twitterCard ?? null;
4095
+ if (record.twitterTitle !== void 0)
4096
+ update.twitter_title = record.twitterTitle ?? null;
4097
+ if (record.twitterDescription !== void 0)
4098
+ update.twitter_description = record.twitterDescription ?? null;
4099
+ if (record.twitterImageUrl !== void 0)
4100
+ update.twitter_image_url = record.twitterImageUrl ?? null;
4101
+ if (record.twitterSite !== void 0)
4102
+ update.twitter_site = record.twitterSite ?? null;
4103
+ if (record.twitterCreator !== void 0)
4104
+ update.twitter_creator = record.twitterCreator ?? null;
4105
+ if (record.canonicalUrl !== void 0)
4106
+ update.canonical_url = record.canonicalUrl ?? null;
4107
+ if (record.robots !== void 0) update.robots = record.robots ?? null;
4108
+ if (record.author !== void 0) update.author = record.author ?? null;
4109
+ if (record.publishedTime !== void 0)
4110
+ update.published_time = record.publishedTime?.toISOString() ?? null;
4111
+ if (record.modifiedTime !== void 0)
4112
+ update.modified_time = record.modifiedTime?.toISOString() ?? null;
4113
+ if (record.structuredData !== void 0)
4114
+ update.structured_data = record.structuredData ?? null;
4115
+ if (record.validationStatus !== void 0)
4116
+ update.validation_status = record.validationStatus ?? null;
4117
+ if (record.lastValidatedAt !== void 0)
4118
+ update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;
4119
+ if (record.validationErrors !== void 0)
4120
+ update.validation_errors = record.validationErrors ?? null;
4121
+ return update;
4122
+ }
4123
+ async function getSEORecords() {
3995
4124
  try {
3996
- const response = await fetch(imageUrl, {
3997
- headers: {
3998
- "User-Agent": "Mozilla/5.0 (compatible; SEO-Console/1.0; +https://example.com/bot)"
3999
- },
4000
- signal: AbortSignal.timeout(15e3)
4001
- // 15 second timeout for images
4002
- });
4003
- if (!response.ok) {
4125
+ const supabase = await createClient();
4126
+ const {
4127
+ data: { user }
4128
+ } = await supabase.auth.getUser();
4129
+ if (!user) {
4004
4130
  return {
4005
- isValid: false,
4006
- issues: [
4007
- {
4008
- field: "image",
4009
- severity: "critical",
4010
- message: `Failed to fetch image: ${response.status} ${response.statusText}`,
4011
- actual: imageUrl
4012
- }
4013
- ]
4131
+ success: false,
4132
+ error: new Error("User not authenticated")
4014
4133
  };
4015
4134
  }
4016
- const imageBuffer = await response.arrayBuffer();
4017
- const buffer = Buffer.from(imageBuffer);
4018
- const sizeInMB = buffer.length / (1024 * 1024);
4019
- if (sizeInMB > 1) {
4020
- issues.push({
4021
- field: "image",
4022
- severity: "warning",
4023
- message: "Image file size exceeds 1MB recommendation",
4024
- actual: `${sizeInMB.toFixed(2)}MB`
4025
- });
4135
+ const { data, error } = await supabase.from("seo_records").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
4136
+ if (error) {
4137
+ return { success: false, error };
4026
4138
  }
4027
- const metadata = await sharp(buffer).metadata();
4028
- const { width, height, format } = metadata;
4029
- if (!width || !height) {
4139
+ const records = (data || []).map(transformRowToSEORecord);
4140
+ return { success: true, data: records };
4141
+ } catch (error) {
4142
+ return {
4143
+ success: false,
4144
+ error: error instanceof Error ? error : new Error("Unknown error")
4145
+ };
4146
+ }
4147
+ }
4148
+ async function getSEORecordById(id) {
4149
+ try {
4150
+ const supabase = await createClient();
4151
+ const {
4152
+ data: { user }
4153
+ } = await supabase.auth.getUser();
4154
+ if (!user) {
4030
4155
  return {
4031
- isValid: false,
4032
- issues: [
4033
- {
4034
- field: "image",
4035
- severity: "critical",
4036
- message: "Could not determine image dimensions",
4037
- actual: imageUrl
4038
- }
4039
- ]
4156
+ success: false,
4157
+ error: new Error("User not authenticated")
4040
4158
  };
4041
4159
  }
4042
- const recommendedWidth = 1200;
4043
- const recommendedHeight = 630;
4044
- const aspectRatio = width / height;
4045
- const recommendedAspectRatio = recommendedWidth / recommendedHeight;
4046
- if (width < recommendedWidth || height < recommendedHeight) {
4047
- issues.push({
4048
- field: "image",
4049
- severity: "warning",
4050
- message: `Image dimensions below recommended size (${recommendedWidth}x${recommendedHeight})`,
4051
- expected: `${recommendedWidth}x${recommendedHeight}`,
4052
- actual: `${width}x${height}`
4053
- });
4160
+ const { data, error } = await supabase.from("seo_records").select("*").eq("id", id).eq("user_id", user.id).single();
4161
+ if (error) {
4162
+ return { success: false, error };
4054
4163
  }
4055
- if (Math.abs(aspectRatio - recommendedAspectRatio) > 0.1) {
4056
- issues.push({
4057
- field: "image",
4058
- severity: "info",
4059
- message: "Image aspect ratio differs from recommended 1.91:1",
4060
- expected: "1.91:1",
4061
- actual: `${aspectRatio.toFixed(2)}:1`
4062
- });
4164
+ if (!data) {
4165
+ return {
4166
+ success: false,
4167
+ error: new Error("SEO record not found")
4168
+ };
4063
4169
  }
4064
- const supportedFormats = ["jpeg", "jpg", "png", "webp", "avif", "gif"];
4065
- if (!format || !supportedFormats.includes(format.toLowerCase())) {
4066
- issues.push({
4067
- field: "image",
4068
- severity: "warning",
4069
- message: "Image format may not be optimal for social sharing",
4070
- expected: "JPEG, PNG, WebP, or AVIF",
4071
- actual: format || "unknown"
4072
- });
4170
+ return { success: true, data: transformRowToSEORecord(data) };
4171
+ } catch (error) {
4172
+ return {
4173
+ success: false,
4174
+ error: error instanceof Error ? error : new Error("Unknown error")
4175
+ };
4176
+ }
4177
+ }
4178
+ async function getSEORecordByRoute(routePath) {
4179
+ try {
4180
+ const supabase = await createClient();
4181
+ const {
4182
+ data: { user }
4183
+ } = await supabase.auth.getUser();
4184
+ if (!user) {
4185
+ return {
4186
+ success: false,
4187
+ error: new Error("User not authenticated")
4188
+ };
4073
4189
  }
4074
- if (expectedWidth && width !== expectedWidth) {
4075
- issues.push({
4076
- field: "image",
4077
- severity: "warning",
4078
- message: "Image width does not match expected value",
4079
- expected: `${expectedWidth}px`,
4080
- actual: `${width}px`
4081
- });
4190
+ const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
4191
+ if (error) {
4192
+ return { success: false, error };
4082
4193
  }
4083
- if (expectedHeight && height !== expectedHeight) {
4084
- issues.push({
4085
- field: "image",
4086
- severity: "warning",
4087
- message: "Image height does not match expected value",
4088
- expected: `${expectedHeight}px`,
4089
- actual: `${height}px`
4090
- });
4194
+ if (!data) {
4195
+ return { success: true, data: null };
4091
4196
  }
4092
- return {
4093
- isValid: issues.filter((i) => i.severity === "critical").length === 0,
4094
- issues,
4095
- metadata: {
4096
- width,
4097
- height,
4098
- format: format || "unknown",
4099
- size: buffer.length
4100
- }
4101
- };
4197
+ return { success: true, data: transformRowToSEORecord(data) };
4102
4198
  } catch (error) {
4103
4199
  return {
4104
- isValid: false,
4105
- issues: [
4106
- {
4107
- field: "image",
4108
- severity: "critical",
4109
- message: error instanceof Error ? error.message : "Failed to validate image",
4110
- actual: imageUrl
4111
- }
4112
- ]
4200
+ success: false,
4201
+ error: error instanceof Error ? error : new Error("Unknown error")
4113
4202
  };
4114
4203
  }
4115
4204
  }
4116
-
4117
- // src/lib/validation/html-validator.ts
4118
- import * as cheerio from "cheerio";
4119
- async function validateHTML(html, record, _baseUrl) {
4120
- const issues = [];
4121
- const $ = cheerio.load(html);
4122
- const title = $("title").text().trim();
4123
- if (record.title) {
4124
- if (!title) {
4125
- issues.push({
4126
- field: "title",
4127
- severity: "critical",
4128
- message: "Title tag is missing",
4129
- expected: record.title
4130
- });
4131
- } else if (title !== record.title) {
4132
- issues.push({
4133
- field: "title",
4134
- severity: "warning",
4135
- message: "Title tag does not match SEO record",
4136
- expected: record.title,
4137
- actual: title
4138
- });
4205
+ async function createSEORecord(record) {
4206
+ try {
4207
+ const supabase = await createClient();
4208
+ const {
4209
+ data: { user }
4210
+ } = await supabase.auth.getUser();
4211
+ if (!user) {
4212
+ return {
4213
+ success: false,
4214
+ error: new Error("User not authenticated")
4215
+ };
4139
4216
  }
4140
- if (title.length > 60) {
4141
- issues.push({
4142
- field: "title",
4143
- severity: "warning",
4144
- message: "Title exceeds recommended 60 characters",
4145
- actual: `${title.length} characters`
4146
- });
4217
+ const insertData = transformToInsert({ ...record, userId: user.id });
4218
+ const { data, error } = await supabase.from("seo_records").insert(insertData).select().single();
4219
+ if (error) {
4220
+ return { success: false, error };
4147
4221
  }
4222
+ return { success: true, data: transformRowToSEORecord(data) };
4223
+ } catch (error) {
4224
+ return {
4225
+ success: false,
4226
+ error: error instanceof Error ? error : new Error("Unknown error")
4227
+ };
4148
4228
  }
4149
- const metaDescription = $('meta[name="description"]').attr("content")?.trim();
4150
- if (record.description) {
4151
- if (!metaDescription) {
4152
- issues.push({
4153
- field: "description",
4154
- severity: "critical",
4155
- message: "Meta description is missing",
4156
- expected: record.description
4157
- });
4158
- } else if (metaDescription !== record.description) {
4159
- issues.push({
4160
- field: "description",
4161
- severity: "warning",
4162
- message: "Meta description does not match SEO record",
4163
- expected: record.description,
4164
- actual: metaDescription
4165
- });
4229
+ }
4230
+ async function updateSEORecord(record) {
4231
+ try {
4232
+ const supabase = await createClient();
4233
+ const {
4234
+ data: { user }
4235
+ } = await supabase.auth.getUser();
4236
+ if (!user) {
4237
+ return {
4238
+ success: false,
4239
+ error: new Error("User not authenticated")
4240
+ };
4166
4241
  }
4167
- if (metaDescription && metaDescription.length > 160) {
4168
- issues.push({
4169
- field: "description",
4170
- severity: "warning",
4171
- message: "Description exceeds recommended 160 characters",
4172
- actual: `${metaDescription.length} characters`
4173
- });
4242
+ const { id, ...updateData } = record;
4243
+ const transformedUpdate = transformToUpdate(updateData);
4244
+ const { data, error } = await supabase.from("seo_records").update(transformedUpdate).eq("id", id).eq("user_id", user.id).select().single();
4245
+ if (error) {
4246
+ return { success: false, error };
4247
+ }
4248
+ if (!data) {
4249
+ return {
4250
+ success: false,
4251
+ error: new Error("SEO record not found")
4252
+ };
4174
4253
  }
4254
+ return { success: true, data: transformRowToSEORecord(data) };
4255
+ } catch (error) {
4256
+ return {
4257
+ success: false,
4258
+ error: error instanceof Error ? error : new Error("Unknown error")
4259
+ };
4175
4260
  }
4176
- if (record.ogTitle || record.ogDescription || record.ogImageUrl) {
4177
- const ogTitle = $('meta[property="og:title"]').attr("content");
4178
- const ogDescription = $('meta[property="og:description"]').attr("content");
4179
- const ogImage = $('meta[property="og:image"]').attr("content");
4180
- const ogType = $('meta[property="og:type"]').attr("content");
4181
- const ogUrl = $('meta[property="og:url"]').attr("content");
4182
- if (record.ogTitle && !ogTitle) {
4183
- issues.push({
4184
- field: "og:title",
4185
- severity: "critical",
4186
- message: "Open Graph title is missing",
4187
- expected: record.ogTitle
4188
- });
4261
+ }
4262
+ async function deleteSEORecord(id) {
4263
+ try {
4264
+ const supabase = await createClient();
4265
+ const {
4266
+ data: { user }
4267
+ } = await supabase.auth.getUser();
4268
+ if (!user) {
4269
+ return {
4270
+ success: false,
4271
+ error: new Error("User not authenticated")
4272
+ };
4189
4273
  }
4190
- if (record.ogDescription && !ogDescription) {
4191
- issues.push({
4192
- field: "og:description",
4193
- severity: "warning",
4194
- message: "Open Graph description is missing",
4195
- expected: record.ogDescription
4196
- });
4274
+ const { error } = await supabase.from("seo_records").delete().eq("id", id).eq("user_id", user.id);
4275
+ if (error) {
4276
+ return { success: false, error };
4197
4277
  }
4198
- if (record.ogImageUrl && !ogImage) {
4199
- issues.push({
4200
- field: "og:image",
4201
- severity: "critical",
4202
- message: "Open Graph image is missing",
4203
- expected: record.ogImageUrl
4204
- });
4278
+ return { success: true, data: void 0 };
4279
+ } catch (error) {
4280
+ return {
4281
+ success: false,
4282
+ error: error instanceof Error ? error : new Error("Unknown error")
4283
+ };
4284
+ }
4285
+ }
4286
+
4287
+ // src/lib/storage/supabase-storage.ts
4288
+ var SupabaseStorage = class {
4289
+ constructor(supabaseUrl, supabaseKey) {
4290
+ this.supabaseUrl = supabaseUrl;
4291
+ this.supabaseKey = supabaseKey;
4292
+ if (typeof process !== "undefined") {
4293
+ process.env.NEXT_PUBLIC_SUPABASE_URL = supabaseUrl;
4294
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = supabaseKey;
4205
4295
  }
4206
- if (record.ogType && ogType !== record.ogType) {
4207
- issues.push({
4208
- field: "og:type",
4209
- severity: "warning",
4210
- message: "Open Graph type does not match",
4211
- expected: record.ogType,
4212
- actual: ogType
4213
- });
4296
+ }
4297
+ async isAvailable() {
4298
+ try {
4299
+ const result = await getSEORecords();
4300
+ return result.success;
4301
+ } catch {
4302
+ return false;
4214
4303
  }
4215
- if (record.ogUrl && ogUrl !== record.ogUrl) {
4216
- issues.push({
4217
- field: "og:url",
4218
- severity: "warning",
4219
- message: "Open Graph URL does not match",
4220
- expected: record.ogUrl,
4221
- actual: ogUrl
4222
- });
4304
+ }
4305
+ async getRecords() {
4306
+ const result = await getSEORecords();
4307
+ if (!result.success) {
4308
+ throw new Error(result.error?.message || "Failed to get records");
4223
4309
  }
4310
+ return result.data;
4224
4311
  }
4225
- if (record.twitterCard || record.twitterTitle || record.twitterImageUrl) {
4226
- const twitterCard = $('meta[name="twitter:card"]').attr("content");
4227
- const twitterTitle = $('meta[name="twitter:title"]').attr("content");
4228
- const _twitterDescription = $('meta[name="twitter:description"]').attr("content");
4229
- const twitterImage = $('meta[name="twitter:image"]').attr("content");
4230
- if (record.twitterCard && twitterCard !== record.twitterCard) {
4231
- issues.push({
4232
- field: "twitter:card",
4233
- severity: "warning",
4234
- message: "Twitter card type does not match",
4235
- expected: record.twitterCard,
4236
- actual: twitterCard
4237
- });
4312
+ async getRecordById(id) {
4313
+ const result = await getSEORecordById(id);
4314
+ if (!result.success) {
4315
+ if (result.error?.message?.includes("not found")) {
4316
+ return null;
4317
+ }
4318
+ throw new Error(result.error?.message || "Failed to get record");
4238
4319
  }
4239
- if (record.twitterTitle && !twitterTitle) {
4240
- issues.push({
4241
- field: "twitter:title",
4242
- severity: "warning",
4243
- message: "Twitter title is missing",
4244
- expected: record.twitterTitle
4245
- });
4320
+ return result.data || null;
4321
+ }
4322
+ async getRecordByRoute(routePath) {
4323
+ const result = await getSEORecordByRoute(routePath);
4324
+ if (!result.success) {
4325
+ if (result.error?.message?.includes("not found")) {
4326
+ return null;
4327
+ }
4328
+ throw new Error(result.error?.message || "Failed to get record");
4246
4329
  }
4247
- if (record.twitterImageUrl && !twitterImage) {
4248
- issues.push({
4249
- field: "twitter:image",
4250
- severity: "warning",
4251
- message: "Twitter image is missing",
4252
- expected: record.twitterImageUrl
4253
- });
4330
+ return result.data || null;
4331
+ }
4332
+ async createRecord(record) {
4333
+ const result = await createSEORecord(record);
4334
+ if (!result.success) {
4335
+ throw new Error(result.error?.message || "Failed to create record");
4254
4336
  }
4337
+ return result.data;
4255
4338
  }
4256
- if (record.canonicalUrl) {
4257
- const canonical = $('link[rel="canonical"]').attr("href");
4258
- if (!canonical) {
4259
- issues.push({
4260
- field: "canonical",
4261
- severity: "critical",
4262
- message: "Canonical URL is missing",
4263
- expected: record.canonicalUrl
4264
- });
4265
- } else if (canonical !== record.canonicalUrl) {
4266
- issues.push({
4267
- field: "canonical",
4268
- severity: "warning",
4269
- message: "Canonical URL does not match",
4270
- expected: record.canonicalUrl,
4271
- actual: canonical
4272
- });
4339
+ async updateRecord(record) {
4340
+ const result = await updateSEORecord(record);
4341
+ if (!result.success) {
4342
+ throw new Error(result.error?.message || "Failed to update record");
4273
4343
  }
4274
- if (canonical && !canonical.startsWith("http://") && !canonical.startsWith("https://")) {
4275
- issues.push({
4276
- field: "canonical",
4277
- severity: "warning",
4278
- message: "Canonical URL should be absolute",
4279
- actual: canonical
4280
- });
4344
+ return result.data;
4345
+ }
4346
+ async deleteRecord(id) {
4347
+ const result = await deleteSEORecord(id);
4348
+ if (!result.success) {
4349
+ throw new Error(result.error?.message || "Failed to delete record");
4281
4350
  }
4282
4351
  }
4352
+ };
4353
+
4354
+ // src/lib/storage/storage-factory.ts
4355
+ function createStorageAdapter(config) {
4356
+ switch (config.type) {
4357
+ case "file":
4358
+ return new FileStorage(config.filePath || "seo-records.json");
4359
+ case "supabase":
4360
+ if (!config.supabaseUrl || !config.supabaseKey) {
4361
+ throw new Error("Supabase URL and key are required for Supabase storage");
4362
+ }
4363
+ return new SupabaseStorage(config.supabaseUrl, config.supabaseKey);
4364
+ case "memory":
4365
+ return new FileStorage(":memory:");
4366
+ default:
4367
+ throw new Error(`Unsupported storage type: ${config.type}`);
4368
+ }
4369
+ }
4370
+ function detectStorageConfig() {
4371
+ if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
4372
+ return {
4373
+ type: "supabase",
4374
+ supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
4375
+ supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
4376
+ };
4377
+ }
4378
+ if (process.env.SEO_CONSOLE_STORAGE_PATH) {
4379
+ return {
4380
+ type: "file",
4381
+ filePath: process.env.SEO_CONSOLE_STORAGE_PATH
4382
+ };
4383
+ }
4283
4384
  return {
4284
- isValid: issues.filter((i) => i.severity === "critical").length === 0,
4285
- issues,
4286
- validatedAt: /* @__PURE__ */ new Date()
4385
+ type: "file",
4386
+ filePath: "seo-records.json"
4287
4387
  };
4288
4388
  }
4289
4389
  export {
@@ -4294,23 +4394,29 @@ export {
4294
4394
  CardFooter,
4295
4395
  CardHeader,
4296
4396
  CardTitle,
4397
+ FileStorage,
4297
4398
  Input,
4298
4399
  OGImagePreview,
4299
4400
  SEORecordForm,
4300
4401
  SEORecordList,
4301
4402
  Spinner,
4302
4403
  ValidationDashboard,
4303
- createSEORecord,
4404
+ crawlSiteForSEO,
4304
4405
  createSEORecordSchema,
4305
- deleteSEORecord,
4306
- getSEORecords as getAllSEORecords,
4307
- getRoutePathFromParams,
4308
- getSEORecordById,
4309
- getSEORecordByRoute,
4310
- updateSEORecord,
4406
+ createStorageAdapter,
4407
+ detectStorageConfig,
4408
+ discoverNextJSRoutes,
4409
+ extractMetadataFromHTML,
4410
+ extractMetadataFromURL,
4411
+ extractSitemapFromRobotsTxt,
4412
+ generateExamplePaths,
4413
+ generateRobotsTxt,
4414
+ generateSitemapFromRecords,
4415
+ generateSitemapXML,
4416
+ metadataToSEORecord,
4417
+ seoRecordsToSitemapEntries,
4418
+ updateRobotsTxtWithSitemap,
4311
4419
  updateSEORecordSchema,
4312
- useGenerateMetadata,
4313
- validateHTML,
4314
- validateOGImage
4420
+ validateSitemapEntry
4315
4421
  };
4316
4422
  //# sourceMappingURL=index.mjs.map