nextblogkit 0.6.0
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/LICENSE +21 -0
- package/README.md +951 -0
- package/dist/admin/index.cjs +2465 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +44 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.js +2438 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/api/categories.cjs +82 -0
- package/dist/api/categories.cjs.map +1 -0
- package/dist/api/categories.d.cts +27 -0
- package/dist/api/categories.d.ts +27 -0
- package/dist/api/categories.js +77 -0
- package/dist/api/categories.js.map +1 -0
- package/dist/api/media.cjs +113 -0
- package/dist/api/media.cjs.map +1 -0
- package/dist/api/media.d.cts +22 -0
- package/dist/api/media.d.ts +22 -0
- package/dist/api/media.js +109 -0
- package/dist/api/media.js.map +1 -0
- package/dist/api/posts.cjs +103 -0
- package/dist/api/posts.cjs.map +1 -0
- package/dist/api/posts.d.cts +27 -0
- package/dist/api/posts.d.ts +27 -0
- package/dist/api/posts.js +98 -0
- package/dist/api/posts.js.map +1 -0
- package/dist/api/rss.cjs +25 -0
- package/dist/api/rss.cjs.map +1 -0
- package/dist/api/rss.d.cts +5 -0
- package/dist/api/rss.d.ts +5 -0
- package/dist/api/rss.js +23 -0
- package/dist/api/rss.js.map +1 -0
- package/dist/api/settings.cjs +40 -0
- package/dist/api/settings.cjs.map +1 -0
- package/dist/api/settings.d.cts +17 -0
- package/dist/api/settings.d.ts +17 -0
- package/dist/api/settings.js +37 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/sitemap.cjs +25 -0
- package/dist/api/sitemap.cjs.map +1 -0
- package/dist/api/sitemap.d.cts +5 -0
- package/dist/api/sitemap.d.ts +5 -0
- package/dist/api/sitemap.js +23 -0
- package/dist/api/sitemap.js.map +1 -0
- package/dist/chunk-4NKOJYWJ.js +68 -0
- package/dist/chunk-4NKOJYWJ.js.map +1 -0
- package/dist/chunk-4PY224XM.js +103 -0
- package/dist/chunk-4PY224XM.js.map +1 -0
- package/dist/chunk-64HUVJOZ.js +446 -0
- package/dist/chunk-64HUVJOZ.js.map +1 -0
- package/dist/chunk-6HKMZOI4.cjs +48 -0
- package/dist/chunk-6HKMZOI4.cjs.map +1 -0
- package/dist/chunk-A2S32RZN.js +138 -0
- package/dist/chunk-A2S32RZN.js.map +1 -0
- package/dist/chunk-E2QLTHKN.cjs +70 -0
- package/dist/chunk-E2QLTHKN.cjs.map +1 -0
- package/dist/chunk-JLPJKNRZ.js +37 -0
- package/dist/chunk-JLPJKNRZ.js.map +1 -0
- package/dist/chunk-JM7QRXXK.js +330 -0
- package/dist/chunk-JM7QRXXK.js.map +1 -0
- package/dist/chunk-KDZER3PU.cjs +43 -0
- package/dist/chunk-KDZER3PU.cjs.map +1 -0
- package/dist/chunk-N5MKAD7J.cjs +109 -0
- package/dist/chunk-N5MKAD7J.cjs.map +1 -0
- package/dist/chunk-QE4VLQYN.cjs +337 -0
- package/dist/chunk-QE4VLQYN.cjs.map +1 -0
- package/dist/chunk-R6MO3QIP.js +46 -0
- package/dist/chunk-R6MO3QIP.js.map +1 -0
- package/dist/chunk-U2ROR6AY.cjs +476 -0
- package/dist/chunk-U2ROR6AY.cjs.map +1 -0
- package/dist/chunk-ZP5XRVVH.cjs +141 -0
- package/dist/chunk-ZP5XRVVH.cjs.map +1 -0
- package/dist/cli/index.cjs +1308 -0
- package/dist/components/index.cjs +541 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.cts +165 -0
- package/dist/components/index.d.ts +165 -0
- package/dist/components/index.js +527 -0
- package/dist/components/index.js.map +1 -0
- package/dist/editor/index.cjs +1083 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +133 -0
- package/dist/editor/index.d.ts +133 -0
- package/dist/editor/index.js +1051 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index-Cgzphklp.d.ts +266 -0
- package/dist/index-vjlZDWNr.d.cts +266 -0
- package/dist/index.cjs +368 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +208 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +120 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +4 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/styles/admin.css +657 -0
- package/dist/styles/blog.css +851 -0
- package/dist/styles/editor.css +452 -0
- package/dist/styles/globals.css +270 -0
- package/dist/styles/prose.css +299 -0
- package/dist/types-CBEEBR4A.d.cts +732 -0
- package/dist/types-CBEEBR4A.d.ts +732 -0
- package/package.json +134 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getEnvConfig } from './chunk-64HUVJOZ.js';
|
|
2
|
+
import { PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
var clientInstance = null;
|
|
6
|
+
function getS3Client() {
|
|
7
|
+
if (clientInstance) return clientInstance;
|
|
8
|
+
const env = getEnvConfig();
|
|
9
|
+
clientInstance = new S3Client({
|
|
10
|
+
region: "auto",
|
|
11
|
+
endpoint: `https://${env.NEXTBLOGKIT_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
12
|
+
credentials: {
|
|
13
|
+
accessKeyId: env.NEXTBLOGKIT_R2_ACCESS_KEY,
|
|
14
|
+
secretAccessKey: env.NEXTBLOGKIT_R2_SECRET_KEY
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return clientInstance;
|
|
18
|
+
}
|
|
19
|
+
function generateKey(filename) {
|
|
20
|
+
const now = /* @__PURE__ */ new Date();
|
|
21
|
+
const year = now.getFullYear();
|
|
22
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
23
|
+
const uuid = randomUUID().split("-")[0];
|
|
24
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
|
|
25
|
+
return `blog/${year}/${month}/${uuid}-${safeName}`;
|
|
26
|
+
}
|
|
27
|
+
var R2StorageProvider = class {
|
|
28
|
+
async upload(file, filename, contentType) {
|
|
29
|
+
const env = getEnvConfig();
|
|
30
|
+
const client = getS3Client();
|
|
31
|
+
const key = generateKey(filename);
|
|
32
|
+
await client.send(
|
|
33
|
+
new PutObjectCommand({
|
|
34
|
+
Bucket: env.NEXTBLOGKIT_R2_BUCKET,
|
|
35
|
+
Key: key,
|
|
36
|
+
Body: file,
|
|
37
|
+
ContentType: contentType
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
url: `${env.NEXTBLOGKIT_R2_PUBLIC_URL}/${key}`,
|
|
43
|
+
size: file.length,
|
|
44
|
+
contentType
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async delete(key) {
|
|
48
|
+
const env = getEnvConfig();
|
|
49
|
+
const client = getS3Client();
|
|
50
|
+
await client.send(
|
|
51
|
+
new DeleteObjectCommand({
|
|
52
|
+
Bucket: env.NEXTBLOGKIT_R2_BUCKET,
|
|
53
|
+
Key: key
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
async list(prefix) {
|
|
58
|
+
const env = getEnvConfig();
|
|
59
|
+
const client = getS3Client();
|
|
60
|
+
const response = await client.send(
|
|
61
|
+
new ListObjectsV2Command({
|
|
62
|
+
Bucket: env.NEXTBLOGKIT_R2_BUCKET,
|
|
63
|
+
Prefix: prefix || "blog/"
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
return (response.Contents || []).map((obj) => ({
|
|
67
|
+
key: obj.Key || "",
|
|
68
|
+
size: obj.Size || 0,
|
|
69
|
+
lastModified: obj.LastModified || /* @__PURE__ */ new Date()
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/lib/image.ts
|
|
75
|
+
var RESPONSIVE_SIZES = [640, 768, 1024, 1280, 1920];
|
|
76
|
+
async function processImage(file, filename, storage) {
|
|
77
|
+
let sharp;
|
|
78
|
+
try {
|
|
79
|
+
sharp = (await import('sharp')).default;
|
|
80
|
+
} catch {
|
|
81
|
+
const result = await storage.upload(file, filename, getMimeType(filename));
|
|
82
|
+
return {
|
|
83
|
+
original: result,
|
|
84
|
+
width: 0,
|
|
85
|
+
height: 0,
|
|
86
|
+
format: getExtension(filename)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const image = sharp(file);
|
|
90
|
+
const metadata = await image.metadata();
|
|
91
|
+
const webpFilename = filename.replace(/\.[^.]+$/, ".webp");
|
|
92
|
+
const webpBuffer = await image.webp({ quality: 85 }).toBuffer();
|
|
93
|
+
const original = await storage.upload(webpBuffer, webpFilename, "image/webp");
|
|
94
|
+
generateResponsiveSizes(file, webpFilename, storage, metadata.width || 0).catch(
|
|
95
|
+
() => {
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
original,
|
|
100
|
+
width: metadata.width || 0,
|
|
101
|
+
height: metadata.height || 0,
|
|
102
|
+
format: "webp"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function generateResponsiveSizes(file, filename, storage, originalWidth) {
|
|
106
|
+
const sharp = (await import('sharp')).default;
|
|
107
|
+
const sizes = RESPONSIVE_SIZES.filter((s) => s < originalWidth);
|
|
108
|
+
await Promise.all(
|
|
109
|
+
sizes.map(async (width) => {
|
|
110
|
+
const resized = await sharp(file).resize(width).webp({ quality: 80 }).toBuffer();
|
|
111
|
+
const sizedFilename = filename.replace(".webp", `-${width}w.webp`);
|
|
112
|
+
await storage.upload(resized, sizedFilename, "image/webp");
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
const thumb = await sharp(file).resize(200, 200, { fit: "cover" }).webp({ quality: 70 }).toBuffer();
|
|
116
|
+
const thumbFilename = filename.replace(".webp", "-thumb.webp");
|
|
117
|
+
await storage.upload(thumb, thumbFilename, "image/webp");
|
|
118
|
+
}
|
|
119
|
+
function getMimeType(filename) {
|
|
120
|
+
const ext = getExtension(filename);
|
|
121
|
+
const mimeTypes = {
|
|
122
|
+
jpg: "image/jpeg",
|
|
123
|
+
jpeg: "image/jpeg",
|
|
124
|
+
png: "image/png",
|
|
125
|
+
gif: "image/gif",
|
|
126
|
+
webp: "image/webp",
|
|
127
|
+
svg: "image/svg+xml",
|
|
128
|
+
avif: "image/avif"
|
|
129
|
+
};
|
|
130
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
131
|
+
}
|
|
132
|
+
function getExtension(filename) {
|
|
133
|
+
return filename.split(".").pop()?.toLowerCase() || "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { R2StorageProvider, processImage };
|
|
137
|
+
//# sourceMappingURL=chunk-A2S32RZN.js.map
|
|
138
|
+
//# sourceMappingURL=chunk-A2S32RZN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/storage.ts","../src/lib/image.ts"],"names":[],"mappings":";;;;AA4BA,IAAI,cAAA,GAAkC,IAAA;AAEtC,SAAS,WAAA,GAAwB;AAC/B,EAAA,IAAI,gBAAgB,OAAO,cAAA;AAE3B,EAAA,MAAM,MAAM,YAAA,EAAa;AACzB,EAAA,cAAA,GAAiB,IAAI,QAAA,CAAS;AAAA,IAC5B,MAAA,EAAQ,MAAA;AAAA,IACR,QAAA,EAAU,CAAA,QAAA,EAAW,GAAA,CAAI,yBAAyB,CAAA,yBAAA,CAAA;AAAA,IAClD,WAAA,EAAa;AAAA,MACX,aAAa,GAAA,CAAI,yBAAA;AAAA,MACjB,iBAAiB,GAAA,CAAI;AAAA;AACvB,GACD,CAAA;AAED,EAAA,OAAO,cAAA;AACT;AAEA,SAAS,YAAY,QAAA,EAA0B;AAC7C,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY;AAC7B,EAAA,MAAM,KAAA,GAAQ,OAAO,GAAA,CAAI,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AACxD,EAAA,MAAM,OAAO,UAAA,EAAW,CAAE,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AACtC,EAAA,MAAM,WAAW,QAAA,CAAS,OAAA,CAAQ,kBAAA,EAAoB,GAAG,EAAE,WAAA,EAAY;AACvE,EAAA,OAAO,QAAQ,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,IAAI,IAAI,QAAQ,CAAA,CAAA;AAClD;AAEO,IAAM,oBAAN,MAAmD;AAAA,EACxD,MAAM,MAAA,CACJ,IAAA,EACA,QAAA,EACA,WAAA,EAC4B;AAC5B,IAAA,MAAM,MAAM,YAAA,EAAa;AACzB,IAAA,MAAM,SAAS,WAAA,EAAY;AAC3B,IAAA,MAAM,GAAA,GAAM,YAAY,QAAQ,CAAA;AAEhC,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACX,IAAI,gBAAA,CAAiB;AAAA,QACnB,QAAQ,GAAA,CAAI,qBAAA;AAAA,QACZ,GAAA,EAAK,GAAA;AAAA,QACL,IAAA,EAAM,IAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACd;AAAA,KACH;AAEA,IAAA,OAAO;AAAA,MACL,GAAA;AAAA,MACA,GAAA,EAAK,CAAA,EAAG,GAAA,CAAI,yBAAyB,IAAI,GAAG,CAAA,CAAA;AAAA,MAC5C,MAAM,IAAA,CAAK,MAAA;AAAA,MACX;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,MAAM,YAAA,EAAa;AACzB,IAAA,MAAM,SAAS,WAAA,EAAY;AAE3B,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACX,IAAI,mBAAA,CAAoB;AAAA,QACtB,QAAQ,GAAA,CAAI,qBAAA;AAAA,QACZ,GAAA,EAAK;AAAA,OACN;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,MAAA,EAA2C;AACpD,IAAA,MAAM,MAAM,YAAA,EAAa;AACzB,IAAA,MAAM,SAAS,WAAA,EAAY;AAE3B,IAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,IAAA;AAAA,MAC5B,IAAI,oBAAA,CAAqB;AAAA,QACvB,QAAQ,GAAA,CAAI,qBAAA;AAAA,QACZ,QAAQ,MAAA,IAAU;AAAA,OACnB;AAAA,KACH;AAEA,IAAA,OAAA,CAAQ,SAAS,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,MAC7C,GAAA,EAAK,IAAI,GAAA,IAAO,EAAA;AAAA,MAChB,IAAA,EAAM,IAAI,IAAA,IAAQ,CAAA;AAAA,MAClB,YAAA,EAAc,GAAA,CAAI,YAAA,oBAAgB,IAAI,IAAA;AAAK,KAC7C,CAAE,CAAA;AAAA,EACJ;AACF;;;ACtGA,IAAM,mBAAmB,CAAC,GAAA,EAAK,GAAA,EAAK,IAAA,EAAM,MAAM,IAAI,CAAA;AAEpD,eAAsB,YAAA,CACpB,IAAA,EACA,QAAA,EACA,OAAA,EACyB;AACzB,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI;AACF,IAAA,KAAA,GAAA,CAAS,MAAM,OAAO,OAAO,CAAA,EAAG,OAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AAEN,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,MAAA,CAAO,MAAM,QAAA,EAAU,WAAA,CAAY,QAAQ,CAAC,CAAA;AACzE,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,MAAA;AAAA,MACV,KAAA,EAAO,CAAA;AAAA,MACP,MAAA,EAAQ,CAAA;AAAA,MACR,MAAA,EAAQ,aAAa,QAAQ;AAAA,KAC/B;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAM,IAAI,CAAA;AACxB,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,EAAS;AAGtC,EAAA,MAAM,YAAA,GAAe,QAAA,CAAS,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA;AACzD,EAAA,MAAM,UAAA,GAAa,MAAM,KAAA,CAAM,IAAA,CAAK,EAAE,OAAA,EAAS,EAAA,EAAI,CAAA,CAAE,QAAA,EAAS;AAE9D,EAAA,MAAM,WAAW,MAAM,OAAA,CAAQ,MAAA,CAAO,UAAA,EAAY,cAAc,YAAY,CAAA;AAG5E,EAAA,uBAAA,CAAwB,MAAM,YAAA,EAAc,OAAA,EAAS,QAAA,CAAS,KAAA,IAAS,CAAC,CAAA,CAAE,KAAA;AAAA,IACxE,MAAM;AAAA,IAEN;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,KAAA,EAAO,SAAS,KAAA,IAAS,CAAA;AAAA,IACzB,MAAA,EAAQ,SAAS,MAAA,IAAU,CAAA;AAAA,IAC3B,MAAA,EAAQ;AAAA,GACV;AACF;AAEA,eAAe,uBAAA,CACb,IAAA,EACA,QAAA,EACA,OAAA,EACA,aAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAA,CAAS,MAAM,OAAO,OAAO,CAAA,EAAG,OAAA;AAEtC,EAAA,MAAM,QAAQ,gBAAA,CAAiB,MAAA,CAAO,CAAC,CAAA,KAAM,IAAI,aAAa,CAAA;AAE9D,EAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,IACZ,KAAA,CAAM,GAAA,CAAI,OAAO,KAAA,KAAU;AACzB,MAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,IAAI,EAC7B,MAAA,CAAO,KAAK,CAAA,CACZ,IAAA,CAAK,EAAE,OAAA,EAAS,EAAA,EAAI,EACpB,QAAA,EAAS;AAEZ,MAAA,MAAM,gBAAgB,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAS,CAAA,CAAA,EAAI,KAAK,CAAA,MAAA,CAAQ,CAAA;AACjE,MAAA,MAAM,OAAA,CAAQ,MAAA,CAAO,OAAA,EAAS,aAAA,EAAe,YAAY,CAAA;AAAA,IAC3D,CAAC;AAAA,GACH;AAGA,EAAA,MAAM,QAAQ,MAAM,KAAA,CAAM,IAAI,CAAA,CAC3B,MAAA,CAAO,KAAK,GAAA,EAAK,EAAE,KAAK,OAAA,EAAS,EACjC,IAAA,CAAK,EAAE,SAAS,EAAA,EAAI,EACpB,QAAA,EAAS;AAEZ,EAAA,MAAM,aAAA,GAAgB,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAS,aAAa,CAAA;AAC7D,EAAA,MAAM,OAAA,CAAQ,MAAA,CAAO,KAAA,EAAO,aAAA,EAAe,YAAY,CAAA;AACzD;AAEA,SAAS,YAAY,QAAA,EAA0B;AAC7C,EAAA,MAAM,GAAA,GAAM,aAAa,QAAQ,CAAA;AACjC,EAAA,MAAM,SAAA,GAAoC;AAAA,IACxC,GAAA,EAAK,YAAA;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,GAAA,EAAK,WAAA;AAAA,IACL,GAAA,EAAK,WAAA;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,GAAA,EAAK,eAAA;AAAA,IACL,IAAA,EAAM;AAAA,GACR;AACA,EAAA,OAAO,SAAA,CAAU,GAAG,CAAA,IAAK,0BAAA;AAC3B;AAEA,SAAS,aAAa,QAAA,EAA0B;AAC9C,EAAA,OAAO,SAAS,KAAA,CAAM,GAAG,EAAE,GAAA,EAAI,EAAG,aAAY,IAAK,EAAA;AACrD","file":"chunk-A2S32RZN.js","sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n} from '@aws-sdk/client-s3';\nimport { getEnvConfig } from './config';\nimport { randomUUID } from 'crypto';\n\nexport interface MediaUploadResult {\n key: string;\n url: string;\n size: number;\n contentType: string;\n}\n\nexport interface StorageObject {\n key: string;\n size: number;\n lastModified: Date;\n}\n\nexport interface StorageProvider {\n upload(file: Buffer, filename: string, contentType: string): Promise<MediaUploadResult>;\n delete(key: string): Promise<void>;\n list(prefix?: string): Promise<StorageObject[]>;\n}\n\nlet clientInstance: S3Client | null = null;\n\nfunction getS3Client(): S3Client {\n if (clientInstance) return clientInstance;\n\n const env = getEnvConfig();\n clientInstance = new S3Client({\n region: 'auto',\n endpoint: `https://${env.NEXTBLOGKIT_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: env.NEXTBLOGKIT_R2_ACCESS_KEY,\n secretAccessKey: env.NEXTBLOGKIT_R2_SECRET_KEY,\n },\n });\n\n return clientInstance;\n}\n\nfunction generateKey(filename: string): string {\n const now = new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const uuid = randomUUID().split('-')[0];\n const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase();\n return `blog/${year}/${month}/${uuid}-${safeName}`;\n}\n\nexport class R2StorageProvider implements StorageProvider {\n async upload(\n file: Buffer,\n filename: string,\n contentType: string\n ): Promise<MediaUploadResult> {\n const env = getEnvConfig();\n const client = getS3Client();\n const key = generateKey(filename);\n\n await client.send(\n new PutObjectCommand({\n Bucket: env.NEXTBLOGKIT_R2_BUCKET,\n Key: key,\n Body: file,\n ContentType: contentType,\n })\n );\n\n return {\n key,\n url: `${env.NEXTBLOGKIT_R2_PUBLIC_URL}/${key}`,\n size: file.length,\n contentType,\n };\n }\n\n async delete(key: string): Promise<void> {\n const env = getEnvConfig();\n const client = getS3Client();\n\n await client.send(\n new DeleteObjectCommand({\n Bucket: env.NEXTBLOGKIT_R2_BUCKET,\n Key: key,\n })\n );\n }\n\n async list(prefix?: string): Promise<StorageObject[]> {\n const env = getEnvConfig();\n const client = getS3Client();\n\n const response = await client.send(\n new ListObjectsV2Command({\n Bucket: env.NEXTBLOGKIT_R2_BUCKET,\n Prefix: prefix || 'blog/',\n })\n );\n\n return (response.Contents || []).map((obj) => ({\n key: obj.Key || '',\n size: obj.Size || 0,\n lastModified: obj.LastModified || new Date(),\n }));\n }\n}\n","import type { R2StorageProvider, MediaUploadResult } from './storage';\n\nexport interface ProcessedImage {\n original: MediaUploadResult;\n width: number;\n height: number;\n format: string;\n}\n\nconst RESPONSIVE_SIZES = [640, 768, 1024, 1280, 1920];\n\nexport async function processImage(\n file: Buffer,\n filename: string,\n storage: R2StorageProvider\n): Promise<ProcessedImage> {\n let sharp: typeof import('sharp');\n try {\n sharp = (await import('sharp')).default;\n } catch {\n // sharp not available — upload raw file\n const result = await storage.upload(file, filename, getMimeType(filename));\n return {\n original: result,\n width: 0,\n height: 0,\n format: getExtension(filename),\n };\n }\n\n const image = sharp(file);\n const metadata = await image.metadata();\n\n // Convert to WebP\n const webpFilename = filename.replace(/\\.[^.]+$/, '.webp');\n const webpBuffer = await image.webp({ quality: 85 }).toBuffer();\n\n const original = await storage.upload(webpBuffer, webpFilename, 'image/webp');\n\n // Generate responsive sizes in background (non-blocking for the main upload)\n generateResponsiveSizes(file, webpFilename, storage, metadata.width || 0).catch(\n () => {\n // Silently fail responsive generation — originals are sufficient\n }\n );\n\n return {\n original,\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: 'webp',\n };\n}\n\nasync function generateResponsiveSizes(\n file: Buffer,\n filename: string,\n storage: R2StorageProvider,\n originalWidth: number\n): Promise<void> {\n const sharp = (await import('sharp')).default;\n\n const sizes = RESPONSIVE_SIZES.filter((s) => s < originalWidth);\n\n await Promise.all(\n sizes.map(async (width) => {\n const resized = await sharp(file)\n .resize(width)\n .webp({ quality: 80 })\n .toBuffer();\n\n const sizedFilename = filename.replace('.webp', `-${width}w.webp`);\n await storage.upload(resized, sizedFilename, 'image/webp');\n })\n );\n\n // Generate thumbnail\n const thumb = await sharp(file)\n .resize(200, 200, { fit: 'cover' })\n .webp({ quality: 70 })\n .toBuffer();\n\n const thumbFilename = filename.replace('.webp', '-thumb.webp');\n await storage.upload(thumb, thumbFilename, 'image/webp');\n}\n\nfunction getMimeType(filename: string): string {\n const ext = getExtension(filename);\n const mimeTypes: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n avif: 'image/avif',\n };\n return mimeTypes[ext] || 'application/octet-stream';\n}\n\nfunction getExtension(filename: string): string {\n return filename.split('.').pop()?.toLowerCase() || '';\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkU2ROR6AY_cjs = require('./chunk-U2ROR6AY.cjs');
|
|
4
|
+
|
|
5
|
+
// src/lib/sitemap.ts
|
|
6
|
+
async function generateSitemap() {
|
|
7
|
+
const env = chunkU2ROR6AY_cjs.getEnvConfig();
|
|
8
|
+
const posts = await chunkU2ROR6AY_cjs.getCollection("nbk_posts");
|
|
9
|
+
const categories = await chunkU2ROR6AY_cjs.getCollection("nbk_categories");
|
|
10
|
+
const entries = [];
|
|
11
|
+
entries.push({
|
|
12
|
+
loc: `${env.NEXTBLOGKIT_SITE_URL}/blog`,
|
|
13
|
+
changefreq: "daily",
|
|
14
|
+
priority: "0.9"
|
|
15
|
+
});
|
|
16
|
+
const publishedPosts = await posts.find({ status: "published" }).sort({ publishedAt: -1 }).project({ slug: 1, updatedAt: 1, publishedAt: 1 }).toArray();
|
|
17
|
+
for (const post of publishedPosts) {
|
|
18
|
+
const lastmod = post.updatedAt || post.publishedAt;
|
|
19
|
+
const daysSincePublish = lastmod ? Math.floor((Date.now() - new Date(lastmod).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
|
|
20
|
+
let changefreq = "monthly";
|
|
21
|
+
if (daysSincePublish < 7) changefreq = "daily";
|
|
22
|
+
else if (daysSincePublish < 30) changefreq = "weekly";
|
|
23
|
+
entries.push({
|
|
24
|
+
loc: `${env.NEXTBLOGKIT_SITE_URL}/blog/${post.slug}`,
|
|
25
|
+
lastmod: lastmod ? new Date(lastmod).toISOString().split("T")[0] : void 0,
|
|
26
|
+
changefreq,
|
|
27
|
+
priority: "0.8"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const allCategories = await categories.find({}).sort({ order: 1 }).project({ slug: 1 }).toArray();
|
|
31
|
+
for (const cat of allCategories) {
|
|
32
|
+
entries.push({
|
|
33
|
+
loc: `${env.NEXTBLOGKIT_SITE_URL}/blog/category/${cat.slug}`,
|
|
34
|
+
changefreq: "weekly",
|
|
35
|
+
priority: "0.6"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const totalPosts = publishedPosts.length;
|
|
39
|
+
const postsPerPage = 10;
|
|
40
|
+
const totalPages = Math.ceil(totalPosts / postsPerPage);
|
|
41
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
42
|
+
entries.push({
|
|
43
|
+
loc: `${env.NEXTBLOGKIT_SITE_URL}/blog?page=${page}`,
|
|
44
|
+
changefreq: "weekly",
|
|
45
|
+
priority: "0.5"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return buildXML(entries);
|
|
49
|
+
}
|
|
50
|
+
function buildXML(entries) {
|
|
51
|
+
const urls = entries.map(
|
|
52
|
+
(entry) => ` <url>
|
|
53
|
+
<loc>${escapeXml(entry.loc)}</loc>${entry.lastmod ? `
|
|
54
|
+
<lastmod>${entry.lastmod}</lastmod>` : ""}${entry.changefreq ? `
|
|
55
|
+
<changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority ? `
|
|
56
|
+
<priority>${entry.priority}</priority>` : ""}
|
|
57
|
+
</url>`
|
|
58
|
+
).join("\n");
|
|
59
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
60
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
61
|
+
${urls}
|
|
62
|
+
</urlset>`;
|
|
63
|
+
}
|
|
64
|
+
function escapeXml(str) {
|
|
65
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.generateSitemap = generateSitemap;
|
|
69
|
+
//# sourceMappingURL=chunk-E2QLTHKN.cjs.map
|
|
70
|
+
//# sourceMappingURL=chunk-E2QLTHKN.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/sitemap.ts"],"names":["getEnvConfig","getCollection"],"mappings":"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","file":"chunk-E2QLTHKN.cjs","sourcesContent":["import { getCollection } from './db';\nimport { getEnvConfig } from './config';\n\ninterface SitemapEntry {\n loc: string;\n lastmod?: string;\n changefreq?: string;\n priority?: string;\n}\n\nexport async function generateSitemap(): Promise<string> {\n const env = getEnvConfig();\n const posts = await getCollection('nbk_posts');\n const categories = await getCollection('nbk_categories');\n\n const entries: SitemapEntry[] = [];\n\n // Blog listing page\n entries.push({\n loc: `${env.NEXTBLOGKIT_SITE_URL}/blog`,\n changefreq: 'daily',\n priority: '0.9',\n });\n\n // Published posts\n const publishedPosts = await posts\n .find({ status: 'published' })\n .sort({ publishedAt: -1 })\n .project({ slug: 1, updatedAt: 1, publishedAt: 1 })\n .toArray();\n\n for (const post of publishedPosts) {\n const lastmod = post.updatedAt || post.publishedAt;\n const daysSincePublish = lastmod\n ? Math.floor((Date.now() - new Date(lastmod).getTime()) / (1000 * 60 * 60 * 24))\n : 0;\n\n let changefreq = 'monthly';\n if (daysSincePublish < 7) changefreq = 'daily';\n else if (daysSincePublish < 30) changefreq = 'weekly';\n\n entries.push({\n loc: `${env.NEXTBLOGKIT_SITE_URL}/blog/${post.slug}`,\n lastmod: lastmod ? new Date(lastmod).toISOString().split('T')[0] : undefined,\n changefreq,\n priority: '0.8',\n });\n }\n\n // Category pages\n const allCategories = await categories\n .find({})\n .sort({ order: 1 })\n .project({ slug: 1 })\n .toArray();\n\n for (const cat of allCategories) {\n entries.push({\n loc: `${env.NEXTBLOGKIT_SITE_URL}/blog/category/${cat.slug}`,\n changefreq: 'weekly',\n priority: '0.6',\n });\n }\n\n // Paginated listing pages\n const totalPosts = publishedPosts.length;\n const postsPerPage = 10;\n const totalPages = Math.ceil(totalPosts / postsPerPage);\n for (let page = 2; page <= totalPages; page++) {\n entries.push({\n loc: `${env.NEXTBLOGKIT_SITE_URL}/blog?page=${page}`,\n changefreq: 'weekly',\n priority: '0.5',\n });\n }\n\n return buildXML(entries);\n}\n\nfunction buildXML(entries: SitemapEntry[]): string {\n const urls = entries\n .map(\n (entry) => ` <url>\n <loc>${escapeXml(entry.loc)}</loc>${entry.lastmod ? `\\n <lastmod>${entry.lastmod}</lastmod>` : ''}${entry.changefreq ? `\\n <changefreq>${entry.changefreq}</changefreq>` : ''}${entry.priority ? `\\n <priority>${entry.priority}</priority>` : ''}\n </url>`\n )\n .join('\\n');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls}\n</urlset>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getEnvConfig } from './chunk-64HUVJOZ.js';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
function jsonSuccess(data, meta, status = 200) {
|
|
5
|
+
return NextResponse.json({ success: true, data, meta }, { status });
|
|
6
|
+
}
|
|
7
|
+
function jsonError(code, message, status = 400) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ success: false, error: { code, message } },
|
|
10
|
+
{ status }
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function requireAuth(request) {
|
|
14
|
+
const env = getEnvConfig();
|
|
15
|
+
const authHeader = request.headers.get("authorization");
|
|
16
|
+
if (!authHeader) {
|
|
17
|
+
return jsonError("UNAUTHORIZED", "Authorization header is required", 401);
|
|
18
|
+
}
|
|
19
|
+
const token = authHeader.replace(/^Bearer\s+/i, "");
|
|
20
|
+
if (token !== env.NEXTBLOGKIT_API_KEY) {
|
|
21
|
+
return jsonError("FORBIDDEN", "Invalid API key", 403);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function getSearchParams(request) {
|
|
26
|
+
const url = new URL(request.url);
|
|
27
|
+
return Object.fromEntries(url.searchParams.entries());
|
|
28
|
+
}
|
|
29
|
+
function parseIntParam(value, defaultValue) {
|
|
30
|
+
if (!value) return defaultValue;
|
|
31
|
+
const parsed = parseInt(value, 10);
|
|
32
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { getSearchParams, jsonError, jsonSuccess, parseIntParam, requireAuth };
|
|
36
|
+
//# sourceMappingURL=chunk-JLPJKNRZ.js.map
|
|
37
|
+
//# sourceMappingURL=chunk-JLPJKNRZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api/middleware.ts"],"names":[],"mappings":";;;AAIO,SAAS,WAAA,CAAY,IAAA,EAAe,IAAA,EAAgC,MAAA,GAAS,GAAA,EAAK;AACvF,EAAA,OAAO,YAAA,CAAa,IAAA,CAAK,EAAE,OAAA,EAAS,IAAA,EAAM,MAAM,IAAA,EAAK,EAAG,EAAE,MAAA,EAAQ,CAAA;AACpE;AAEO,SAAS,SAAA,CAAU,IAAA,EAAc,OAAA,EAAiB,MAAA,GAAS,GAAA,EAAqC;AACrG,EAAA,OAAO,YAAA,CAAa,IAAA;AAAA,IAClB,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,EAAE,IAAA,EAAM,SAAQ,EAAE;AAAA,IAC3C,EAAE,MAAA;AAAO,GACX;AACF;AAEO,SAAS,YAAY,OAAA,EAAyD;AACnF,EAAA,MAAM,MAAM,YAAA,EAAa;AACzB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAEtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,SAAA,CAAU,cAAA,EAAgB,kCAAA,EAAoC,GAAG,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAClD,EAAA,IAAI,KAAA,KAAU,IAAI,mBAAA,EAAqB;AACrC,IAAA,OAAO,SAAA,CAAU,WAAA,EAAa,iBAAA,EAAmB,GAAG,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,gBAAgB,OAAA,EAAkB;AAChD,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,OAAO,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,YAAA,CAAa,SAAS,CAAA;AACtD;AAEO,SAAS,aAAA,CAAc,OAA2B,YAAA,EAA8B;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,YAAA;AACnB,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AACjC,EAAA,OAAO,KAAA,CAAM,MAAM,CAAA,GAAI,YAAA,GAAe,MAAA;AACxC","file":"chunk-JLPJKNRZ.js","sourcesContent":["import { NextResponse } from 'next/server';\nimport { getEnvConfig } from '../lib/config';\nimport type { ApiErrorResponse } from '../lib/types';\n\nexport function jsonSuccess(data: unknown, meta?: Record<string, unknown>, status = 200) {\n return NextResponse.json({ success: true, data, meta }, { status });\n}\n\nexport function jsonError(code: string, message: string, status = 400): NextResponse<ApiErrorResponse> {\n return NextResponse.json(\n { success: false, error: { code, message } },\n { status }\n ) as NextResponse<ApiErrorResponse>;\n}\n\nexport function requireAuth(request: Request): NextResponse<ApiErrorResponse> | null {\n const env = getEnvConfig();\n const authHeader = request.headers.get('authorization');\n\n if (!authHeader) {\n return jsonError('UNAUTHORIZED', 'Authorization header is required', 401);\n }\n\n const token = authHeader.replace(/^Bearer\\s+/i, '');\n if (token !== env.NEXTBLOGKIT_API_KEY) {\n return jsonError('FORBIDDEN', 'Invalid API key', 403);\n }\n\n return null;\n}\n\nexport function getSearchParams(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n}\n\nexport function parseIntParam(value: string | undefined, defaultValue: number): number {\n if (!value) return defaultValue;\n const parsed = parseInt(value, 10);\n return isNaN(parsed) ? defaultValue : parsed;\n}\n"]}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { getEnvConfig } from './chunk-64HUVJOZ.js';
|
|
2
|
+
|
|
3
|
+
// src/lib/search.ts
|
|
4
|
+
async function searchPosts(collection, query, limit = 10) {
|
|
5
|
+
if (!query.trim()) return [];
|
|
6
|
+
const results = await collection.find(
|
|
7
|
+
{
|
|
8
|
+
$text: { $search: query },
|
|
9
|
+
status: "published"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
projection: {
|
|
13
|
+
slug: 1,
|
|
14
|
+
title: 1,
|
|
15
|
+
excerpt: 1,
|
|
16
|
+
score: { $meta: "textScore" }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
).sort({ score: { $meta: "textScore" } }).limit(limit).toArray();
|
|
20
|
+
return results.map((doc) => ({
|
|
21
|
+
slug: doc.slug,
|
|
22
|
+
title: doc.title,
|
|
23
|
+
excerpt: doc.excerpt,
|
|
24
|
+
score: doc.score
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/lib/seo.ts
|
|
29
|
+
function generateMetaTags(post) {
|
|
30
|
+
const env = getEnvConfig();
|
|
31
|
+
const postUrl = `${env.NEXTBLOGKIT_SITE_URL}/blog/${post.slug}`;
|
|
32
|
+
const title = post.seo?.metaTitle || post.title;
|
|
33
|
+
const description = post.seo?.metaDescription || post.excerpt;
|
|
34
|
+
const canonical = post.seo?.canonicalUrl || postUrl;
|
|
35
|
+
const ogImage = post.seo?.ogImage || post.coverImage?.url;
|
|
36
|
+
return {
|
|
37
|
+
title: `${title} | ${env.NEXTBLOGKIT_SITE_NAME}`,
|
|
38
|
+
description,
|
|
39
|
+
canonical,
|
|
40
|
+
openGraph: {
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
url: postUrl,
|
|
44
|
+
siteName: env.NEXTBLOGKIT_SITE_NAME,
|
|
45
|
+
type: post.seo?.ogType || "article",
|
|
46
|
+
images: ogImage ? [
|
|
47
|
+
{
|
|
48
|
+
url: ogImage,
|
|
49
|
+
width: 1200,
|
|
50
|
+
height: 630,
|
|
51
|
+
alt: title
|
|
52
|
+
}
|
|
53
|
+
] : [],
|
|
54
|
+
article: {
|
|
55
|
+
publishedTime: post.publishedAt?.toISOString() || "",
|
|
56
|
+
modifiedTime: post.updatedAt.toISOString(),
|
|
57
|
+
section: post.categories[0],
|
|
58
|
+
tags: post.tags
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
twitter: {
|
|
62
|
+
card: "summary_large_image",
|
|
63
|
+
title,
|
|
64
|
+
description,
|
|
65
|
+
images: ogImage ? [ogImage] : []
|
|
66
|
+
},
|
|
67
|
+
robots: post.seo?.noIndex ? "noindex, nofollow" : void 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function generateStructuredData(post) {
|
|
71
|
+
const env = getEnvConfig();
|
|
72
|
+
const postUrl = `${env.NEXTBLOGKIT_SITE_URL}/blog/${post.slug}`;
|
|
73
|
+
return {
|
|
74
|
+
"@context": "https://schema.org",
|
|
75
|
+
"@type": "BlogPosting",
|
|
76
|
+
headline: post.title,
|
|
77
|
+
description: post.excerpt,
|
|
78
|
+
image: post.coverImage?.url || post.seo?.ogImage,
|
|
79
|
+
datePublished: post.publishedAt?.toISOString(),
|
|
80
|
+
dateModified: post.updatedAt.toISOString(),
|
|
81
|
+
author: {
|
|
82
|
+
"@type": "Person",
|
|
83
|
+
name: post.author.name,
|
|
84
|
+
url: post.author.url
|
|
85
|
+
},
|
|
86
|
+
publisher: {
|
|
87
|
+
"@type": "Organization",
|
|
88
|
+
name: env.NEXTBLOGKIT_SITE_NAME
|
|
89
|
+
},
|
|
90
|
+
mainEntityOfPage: {
|
|
91
|
+
"@type": "WebPage",
|
|
92
|
+
"@id": postUrl
|
|
93
|
+
},
|
|
94
|
+
wordCount: post.wordCount,
|
|
95
|
+
articleSection: post.categories[0]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function generateFAQStructuredData(faqItems) {
|
|
99
|
+
if (!faqItems.length) return null;
|
|
100
|
+
return {
|
|
101
|
+
"@context": "https://schema.org",
|
|
102
|
+
"@type": "FAQPage",
|
|
103
|
+
mainEntity: faqItems.map((item) => ({
|
|
104
|
+
"@type": "Question",
|
|
105
|
+
name: item.question,
|
|
106
|
+
acceptedAnswer: {
|
|
107
|
+
"@type": "Answer",
|
|
108
|
+
text: item.answer
|
|
109
|
+
}
|
|
110
|
+
}))
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function generateBreadcrumbs(post, categoryName) {
|
|
114
|
+
const env = getEnvConfig();
|
|
115
|
+
const items = [
|
|
116
|
+
{
|
|
117
|
+
"@type": "ListItem",
|
|
118
|
+
position: 1,
|
|
119
|
+
name: "Home",
|
|
120
|
+
item: env.NEXTBLOGKIT_SITE_URL
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"@type": "ListItem",
|
|
124
|
+
position: 2,
|
|
125
|
+
name: "Blog",
|
|
126
|
+
item: `${env.NEXTBLOGKIT_SITE_URL}/blog`
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
if (categoryName && post.categories[0]) {
|
|
130
|
+
items.push({
|
|
131
|
+
"@type": "ListItem",
|
|
132
|
+
position: 3,
|
|
133
|
+
name: categoryName,
|
|
134
|
+
item: `${env.NEXTBLOGKIT_SITE_URL}/blog/category/${post.categories[0]}`
|
|
135
|
+
});
|
|
136
|
+
items.push({
|
|
137
|
+
"@type": "ListItem",
|
|
138
|
+
position: 4,
|
|
139
|
+
name: post.title
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
items.push({
|
|
143
|
+
"@type": "ListItem",
|
|
144
|
+
position: 3,
|
|
145
|
+
name: post.title
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
"@context": "https://schema.org",
|
|
150
|
+
"@type": "BreadcrumbList",
|
|
151
|
+
itemListElement: items
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/lib/seo-scorer.ts
|
|
156
|
+
function check(id, status, message) {
|
|
157
|
+
return { id, status, message };
|
|
158
|
+
}
|
|
159
|
+
function calculateSEOScore(post) {
|
|
160
|
+
const checks = [];
|
|
161
|
+
const keyword = post.seo?.focusKeyword?.toLowerCase() || "";
|
|
162
|
+
const title = post.title.toLowerCase();
|
|
163
|
+
const slug = post.slug.toLowerCase();
|
|
164
|
+
const excerpt = (post.seo?.metaDescription || post.excerpt || "").toLowerCase();
|
|
165
|
+
const contentText = post.contentText?.toLowerCase() || "";
|
|
166
|
+
const contentHTML = post.contentHTML || "";
|
|
167
|
+
if (!keyword) {
|
|
168
|
+
checks.push(check("focus-keyword-in-title", "warn", "No focus keyword set"));
|
|
169
|
+
} else if (title.includes(keyword)) {
|
|
170
|
+
checks.push(check("focus-keyword-in-title", "pass", "Focus keyword appears in title"));
|
|
171
|
+
} else {
|
|
172
|
+
checks.push(check("focus-keyword-in-title", "fail", "Focus keyword not found in title"));
|
|
173
|
+
}
|
|
174
|
+
if (!keyword) {
|
|
175
|
+
checks.push(check("focus-keyword-in-slug", "warn", "No focus keyword set"));
|
|
176
|
+
} else if (slug.includes(keyword.replace(/\s+/g, "-"))) {
|
|
177
|
+
checks.push(check("focus-keyword-in-slug", "pass", "Focus keyword appears in URL slug"));
|
|
178
|
+
} else {
|
|
179
|
+
checks.push(check("focus-keyword-in-slug", "fail", "Focus keyword not found in URL slug"));
|
|
180
|
+
}
|
|
181
|
+
if (!keyword) {
|
|
182
|
+
checks.push(check("focus-keyword-in-excerpt", "warn", "No focus keyword set"));
|
|
183
|
+
} else if (excerpt.includes(keyword)) {
|
|
184
|
+
checks.push(check("focus-keyword-in-excerpt", "pass", "Focus keyword appears in meta description"));
|
|
185
|
+
} else {
|
|
186
|
+
checks.push(check("focus-keyword-in-excerpt", "fail", "Focus keyword not found in meta description"));
|
|
187
|
+
}
|
|
188
|
+
if (keyword) {
|
|
189
|
+
const h2Regex = /<h2[^>]*>(.*?)<\/h2>/gi;
|
|
190
|
+
const h2s = contentHTML.match(h2Regex) || [];
|
|
191
|
+
const keywordInH2 = h2s.some((h2) => h2.toLowerCase().includes(keyword));
|
|
192
|
+
if (keywordInH2) {
|
|
193
|
+
checks.push(check("focus-keyword-in-h2", "pass", "Focus keyword found in a subheading"));
|
|
194
|
+
} else {
|
|
195
|
+
checks.push(check("focus-keyword-in-h2", "warn", "Focus keyword not found in any H2 subheading"));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (keyword && contentText) {
|
|
199
|
+
const first150Words = contentText.split(/\s+/).slice(0, 150).join(" ");
|
|
200
|
+
if (first150Words.includes(keyword)) {
|
|
201
|
+
checks.push(check("focus-keyword-in-first-paragraph", "pass", "Focus keyword appears early in content"));
|
|
202
|
+
} else {
|
|
203
|
+
checks.push(check("focus-keyword-in-first-paragraph", "warn", "Focus keyword not found in the first paragraph"));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (keyword && contentText) {
|
|
207
|
+
const words = contentText.split(/\s+/).length;
|
|
208
|
+
const keywordCount = (contentText.match(new RegExp(keyword, "g")) || []).length;
|
|
209
|
+
const density = keywordCount / words * 100;
|
|
210
|
+
if (density >= 0.5 && density <= 2.5) {
|
|
211
|
+
checks.push(check("focus-keyword-density", "pass", `Keyword density is ${density.toFixed(1)}% (ideal)`));
|
|
212
|
+
} else if (density < 0.5) {
|
|
213
|
+
checks.push(check("focus-keyword-density", "warn", `Keyword density is ${density.toFixed(1)}% (too low, aim for 0.5-2.5%)`));
|
|
214
|
+
} else {
|
|
215
|
+
checks.push(check("focus-keyword-density", "warn", `Keyword density is ${density.toFixed(1)}% (too high, aim for 0.5-2.5%)`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const metaTitle = post.seo?.metaTitle || post.title;
|
|
219
|
+
if (metaTitle.length >= 50 && metaTitle.length <= 60) {
|
|
220
|
+
checks.push(check("title-length", "pass", `Title is ${metaTitle.length} characters (ideal)`));
|
|
221
|
+
} else if (metaTitle.length < 30) {
|
|
222
|
+
checks.push(check("title-length", "fail", `Title is ${metaTitle.length} characters (too short, aim for 50-60)`));
|
|
223
|
+
} else if (metaTitle.length > 70) {
|
|
224
|
+
checks.push(check("title-length", "warn", `Title is ${metaTitle.length} characters (too long, aim for 50-60)`));
|
|
225
|
+
} else {
|
|
226
|
+
checks.push(check("title-length", "warn", `Title is ${metaTitle.length} characters (aim for 50-60)`));
|
|
227
|
+
}
|
|
228
|
+
const metaDesc = post.seo?.metaDescription || post.excerpt || "";
|
|
229
|
+
if (metaDesc.length >= 150 && metaDesc.length <= 160) {
|
|
230
|
+
checks.push(check("meta-description-length", "pass", `Meta description is ${metaDesc.length} characters (ideal)`));
|
|
231
|
+
} else if (metaDesc.length < 120) {
|
|
232
|
+
checks.push(check("meta-description-length", "warn", `Meta description is ${metaDesc.length} characters (too short, aim for 150-160)`));
|
|
233
|
+
} else if (metaDesc.length > 170) {
|
|
234
|
+
checks.push(check("meta-description-length", "warn", `Meta description is ${metaDesc.length} characters (too long, may be truncated)`));
|
|
235
|
+
} else {
|
|
236
|
+
checks.push(check("meta-description-length", "pass", `Meta description is ${metaDesc.length} characters`));
|
|
237
|
+
}
|
|
238
|
+
if (post.slug.length <= 75) {
|
|
239
|
+
checks.push(check("slug-length", "pass", `URL slug is ${post.slug.length} characters`));
|
|
240
|
+
} else {
|
|
241
|
+
checks.push(check("slug-length", "warn", `URL slug is ${post.slug.length} characters (should be under 75)`));
|
|
242
|
+
}
|
|
243
|
+
if (post.wordCount >= 300) {
|
|
244
|
+
checks.push(check("content-length", "pass", `Content is ${post.wordCount} words`));
|
|
245
|
+
} else {
|
|
246
|
+
checks.push(check("content-length", "fail", `Content is only ${post.wordCount} words (aim for 300+)`));
|
|
247
|
+
}
|
|
248
|
+
const headingRegex = /<(h[2-6])[^>]*>/gi;
|
|
249
|
+
const headings = [...contentHTML.matchAll(headingRegex)].map((m) => parseInt(m[1][1]));
|
|
250
|
+
let hierarchyOk = true;
|
|
251
|
+
for (let i = 1; i < headings.length; i++) {
|
|
252
|
+
if (headings[i] > headings[i - 1] + 1) {
|
|
253
|
+
hierarchyOk = false;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (headings.length === 0) {
|
|
258
|
+
checks.push(check("heading-hierarchy", "warn", "No subheadings found \u2014 add H2s to structure content"));
|
|
259
|
+
} else if (hierarchyOk) {
|
|
260
|
+
checks.push(check("heading-hierarchy", "pass", "Heading hierarchy is correct"));
|
|
261
|
+
} else {
|
|
262
|
+
checks.push(check("heading-hierarchy", "warn", "Heading levels are skipped (e.g., H2 \u2192 H4)"));
|
|
263
|
+
}
|
|
264
|
+
const imgRegex = /<img[^>]*>/gi;
|
|
265
|
+
const images = contentHTML.match(imgRegex) || [];
|
|
266
|
+
const missingAlt = images.filter((img) => !img.includes("alt=") || img.includes('alt=""'));
|
|
267
|
+
if (images.length === 0) {
|
|
268
|
+
checks.push(check("image-alt-text", "warn", "No images found in content"));
|
|
269
|
+
} else if (missingAlt.length === 0) {
|
|
270
|
+
checks.push(check("image-alt-text", "pass", "All images have alt text"));
|
|
271
|
+
} else {
|
|
272
|
+
checks.push(check("image-alt-text", "fail", `${missingAlt.length} image(s) missing alt text`));
|
|
273
|
+
}
|
|
274
|
+
const internalLinkRegex = /href=["']\/[^"']*["']/gi;
|
|
275
|
+
const internalLinks = contentHTML.match(internalLinkRegex) || [];
|
|
276
|
+
if (internalLinks.length > 0) {
|
|
277
|
+
checks.push(check("internal-links", "pass", `${internalLinks.length} internal link(s) found`));
|
|
278
|
+
} else {
|
|
279
|
+
checks.push(check("internal-links", "warn", "No internal links found \u2014 add links to related content"));
|
|
280
|
+
}
|
|
281
|
+
const externalLinkRegex = /href=["']https?:\/\/[^"']*["']/gi;
|
|
282
|
+
const externalLinks = contentHTML.match(externalLinkRegex) || [];
|
|
283
|
+
if (externalLinks.length > 0) {
|
|
284
|
+
checks.push(check("external-links", "pass", `${externalLinks.length} external link(s) found`));
|
|
285
|
+
} else {
|
|
286
|
+
checks.push(check("external-links", "warn", "No external links found"));
|
|
287
|
+
}
|
|
288
|
+
const paragraphs = contentHTML.split(/<\/p>/i).filter((p) => p.trim());
|
|
289
|
+
const longParagraphs = paragraphs.filter((p) => {
|
|
290
|
+
const text = p.replace(/<[^>]+>/g, "");
|
|
291
|
+
return text.split(/\s+/).length > 300;
|
|
292
|
+
});
|
|
293
|
+
if (longParagraphs.length === 0) {
|
|
294
|
+
checks.push(check("paragraph-length", "pass", "All paragraphs are a reasonable length"));
|
|
295
|
+
} else {
|
|
296
|
+
checks.push(check("paragraph-length", "warn", `${longParagraphs.length} paragraph(s) exceed 300 words`));
|
|
297
|
+
}
|
|
298
|
+
if (post.coverImage?.url) {
|
|
299
|
+
checks.push(check("cover-image", "pass", "Post has a cover image"));
|
|
300
|
+
} else {
|
|
301
|
+
checks.push(check("cover-image", "warn", "No cover image set \u2014 social shares may look plain"));
|
|
302
|
+
}
|
|
303
|
+
if (contentText) {
|
|
304
|
+
const sentences = contentText.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
305
|
+
const words = contentText.split(/\s+/).length;
|
|
306
|
+
const avgWordsPerSentence = sentences.length > 0 ? words / sentences.length : 0;
|
|
307
|
+
if (avgWordsPerSentence <= 20) {
|
|
308
|
+
checks.push(check("readability-score", "pass", `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words`));
|
|
309
|
+
} else if (avgWordsPerSentence <= 25) {
|
|
310
|
+
checks.push(check("readability-score", "warn", `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words (try to keep under 20)`));
|
|
311
|
+
} else {
|
|
312
|
+
checks.push(check("readability-score", "fail", `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words (too long, aim for under 20)`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
316
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
317
|
+
let overall;
|
|
318
|
+
if (fails >= 3) {
|
|
319
|
+
overall = "poor";
|
|
320
|
+
} else if (fails >= 1 || warns >= 5) {
|
|
321
|
+
overall = "ok";
|
|
322
|
+
} else {
|
|
323
|
+
overall = "good";
|
|
324
|
+
}
|
|
325
|
+
return { overall, checks };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export { calculateSEOScore, generateBreadcrumbs, generateFAQStructuredData, generateMetaTags, generateStructuredData, searchPosts };
|
|
329
|
+
//# sourceMappingURL=chunk-JM7QRXXK.js.map
|
|
330
|
+
//# sourceMappingURL=chunk-JM7QRXXK.js.map
|