@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/components/index.d.mts +48 -3
- package/dist/components/index.d.ts +48 -3
- package/dist/components/index.js +3 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +3 -1
- package/dist/components/index.mjs.map +1 -1
- package/dist/hooks/index.js +443 -3
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +443 -3
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +71 -66
- package/dist/index.d.ts +71 -66
- package/dist/index.js +804 -692
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +788 -682
- package/dist/index.mjs.map +1 -1
- package/dist/robots-generator-B1KOf8vn.d.ts +166 -0
- package/dist/robots-generator-D6T5HVNx.d.mts +166 -0
- package/dist/{index-6lAOwFXQ.d.mts → seo-schema-D8EwzllB.d.mts} +1 -46
- package/dist/{index-6lAOwFXQ.d.ts → seo-schema-D8EwzllB.d.ts} +1 -46
- package/dist/server.d.mts +88 -0
- package/dist/server.d.ts +88 -0
- package/dist/server.js +1547 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +1485 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +13 -3
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
|
-
|
|
47
|
+
crawlSiteForSEO: () => crawlSiteForSEO,
|
|
47
48
|
createSEORecordSchema: () => createSEORecordSchema,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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 }) =>
|
|
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/
|
|
4052
|
-
var
|
|
4053
|
-
|
|
4054
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
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
|
-
|
|
4066
|
-
|
|
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
|
|
4077
|
-
|
|
4078
|
-
|
|
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
|
|
4088
|
-
|
|
4089
|
-
|
|
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
|
-
|
|
4092
|
-
|
|
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
|
|
4103
|
-
|
|
4104
|
-
|
|
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 (
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
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
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
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
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
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 (
|
|
4144
|
-
|
|
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
|
-
|
|
4165
|
-
|
|
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
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
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
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
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
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
const
|
|
4240
|
-
const
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
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
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
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
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
});
|
|
4362
|
+
}
|
|
4363
|
+
async isAvailable() {
|
|
4364
|
+
try {
|
|
4365
|
+
const result = await getSEORecords();
|
|
4366
|
+
return result.success;
|
|
4367
|
+
} catch {
|
|
4368
|
+
return false;
|
|
4274
4369
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
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
|
-
|
|
4286
|
-
const
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
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
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
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
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
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
|
-
|
|
4317
|
-
const
|
|
4318
|
-
if (!
|
|
4319
|
-
|
|
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
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
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
|
-
|
|
4345
|
-
|
|
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
|
-
|
|
4471
|
+
crawlSiteForSEO,
|
|
4365
4472
|
createSEORecordSchema,
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
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
|
-
|
|
4374
|
-
validateHTML,
|
|
4375
|
-
validateOGImage
|
|
4487
|
+
validateSitemapEntry
|
|
4376
4488
|
});
|
|
4377
4489
|
//# sourceMappingURL=index.js.map
|