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