@lexho111/plainblog 0.5.9 → 0.5.11

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/Blog.js CHANGED
@@ -11,8 +11,9 @@ import path from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import { exec } from "child_process";
13
13
  import { promisify } from "util";
14
+ import { compileStyles, mergeStyles } from "./build-styles.js";
14
15
 
15
- const execPromise = promisify(exec);
16
+ const execPromise = promisify(exec); // x
16
17
 
17
18
  export default class Blog {
18
19
  constructor() {
@@ -21,31 +22,53 @@ export default class Blog {
21
22
  username: "user",
22
23
  password: "password",
23
24
  host: "localhost",
24
- dbname: "blog.json",
25
+ dbname: "blog.json", // x
25
26
  };
26
27
  this.#title = "";
27
- this.articles = [];
28
- this.filename = null;
28
+ this.#articles = [];
29
29
  this.#server = null;
30
30
  this.#password = "admin";
31
- this.styles = "body { font-family: Arial; }";
32
- this.scripts = "";
31
+ this.#styles = "body { font-family: Arial; }";
32
+ //this.scripts = "";
33
33
  this.compiledStyles = "";
34
- this.compiledScripts = "";
34
+ //this.compiledScripts = "";
35
35
  this.reloadStylesOnGET = false;
36
36
  this.sessions = new Set();
37
37
 
38
- const version = pkg.version;
39
- console.log(`version: ${version}`);
38
+ this.#version = pkg.version;
39
+ console.log(`version: ${this.#version}`);
40
+ }
41
+
42
+ json() {
43
+ const serverInfo = this.#server ? {
44
+ listening: this.#server.listening,
45
+ address: this.#server.address()
46
+ } : null;
47
+
48
+ const json = {
49
+ "version": this.#version,
50
+ "title": this.#title,
51
+ "articles": this.#articles,
52
+ "server": serverInfo,
53
+ "compiledStyles": this.compiledStyles,
54
+ "sessions": this.sessions,
55
+ "database": this.database,
56
+ "password": this.#password, "styles": this.#styles, "reloadStylesOnGET": this.reloadStylesOnGET
57
+ }
58
+
59
+ return JSON.parse(JSON.stringify(json));
40
60
  }
41
61
 
42
62
  // Private fields
63
+ #version = null;
43
64
  #server = null;
44
65
  #password = null;
45
66
  #databaseModel;
46
67
  #isExternalAPI = false;
47
68
  #apiUrl = "";
48
69
  #title = "";
70
+ #articles = [];
71
+ #styles = "";
49
72
  #stylesHash = "";
50
73
  #scriptsHash = "";
51
74
  #stylesheetPath = "";
@@ -59,7 +82,7 @@ export default class Blog {
59
82
  }
60
83
 
61
84
  setStyle(style) {
62
- this.styles += style;
85
+ this.#styles += style;
63
86
  }
64
87
 
65
88
  set title(t) {
@@ -88,7 +111,7 @@ export default class Blog {
88
111
  }
89
112
 
90
113
  set style(style) {
91
- this.styles += style;
114
+ this.#styles += style;
92
115
  }
93
116
 
94
117
  set stylesheetPath(files) {
@@ -97,68 +120,71 @@ export default class Blog {
97
120
  }
98
121
 
99
122
  addArticle(article) {
100
- this.articles.push(article);
123
+ this.#articles.push(article);
101
124
  }
102
125
 
103
- isAuthenticated(req) {
126
+ #isAuthenticated(req) {
104
127
  if (!req.headers.cookie) return false;
105
128
  const params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
106
129
  return this.sessions.has(params.get("session"));
107
- }
130
+ }
108
131
 
109
- async handleLogin(req, res) {
110
- const body = await new Promise((resolve, reject) => {
111
- let data = "";
112
- req.on("data", (chunk) => (data += chunk.toString()));
113
- req.on("end", () => resolve(data));
114
- req.on("error", reject);
132
+ async #handleLogin(req, res) {
133
+ const body = await new Promise((resolve, reject) => {
134
+ let data = "";
135
+ req.on("data", (chunk) => (data += chunk.toString()));
136
+ req.on("end", () => resolve(data));
137
+ req.on("error", reject);
138
+ });
139
+ const params = new URLSearchParams(body);
140
+
141
+ if (params.get("password") === this.#password) {
142
+ const id = crypto.randomUUID();
143
+ this.sessions.add(id);
144
+ res.writeHead(303, {
145
+ "Set-Cookie": `session=${id}; HttpOnly; Path=/`,
146
+ Location: "/",
115
147
  });
116
- const params = new URLSearchParams(body);
117
-
118
- if (params.get("password") === this.#password) {
119
- const id = crypto.randomUUID();
120
- this.sessions.add(id);
121
- res.writeHead(303, {
122
- "Set-Cookie": `session=${id}; HttpOnly; Path=/`,
123
- Location: "/",
124
- });
125
- res.end();
126
- } else {
127
- res.writeHead(401, { "Content-Type": "text/html" });
128
- res.end(`${header("My Blog")}
148
+ res.end();
149
+ } else {
150
+ res.writeHead(401, { "Content-Type": "text/html" });
151
+ res.end(`${header("My Blog")}
129
152
  <body>
130
153
  <h1>Unauthorized</h1><p>Please enter the password.<form method="POST">
131
154
  <input type="password" name="password" placeholder="Password" />
132
155
  <button style="margin: 2px;">Login</button></form>
133
156
  </body></html>`);
134
157
  }
135
- }
158
+ }
136
159
 
137
- handleLogout(req, res) {
138
- if (req.headers.cookie) {
139
- const params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
140
- const sessionId = params.get("session");
141
- if (this.sessions.has(sessionId)) {
142
- this.sessions.delete(sessionId);
143
- }
160
+ #handleLogout(req, res) {
161
+ if (req.headers.cookie) {
162
+ const params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
163
+ const sessionId = params.get("session");
164
+ if (this.sessions.has(sessionId)) {
165
+ this.sessions.delete(sessionId);
144
166
  }
145
- res.writeHead(303, {
146
- "Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
147
- Location: "/",
148
- });
149
- res.end();
150
167
  }
168
+ res.writeHead(303, {
169
+ "Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
170
+ Location: "/",
171
+ });
172
+ res.end();
173
+ }
151
174
 
152
175
  /** initializes database */
153
176
  async init() {
154
- //await this.buildFrontend();
155
177
  //this.loadStyles();
156
178
  //this.loadScripts();
179
+ // if there is a stylesheet path provided, process it
157
180
  if(this.#stylesheetPath != null) {
158
- await this.processStylesheets(this.#stylesheetPath);
181
+ // read file from stylesheet path, compare checksums and write to public/styles.min.css
182
+ await this.#processStylesheets(this.#stylesheetPath);
159
183
  }
160
184
  if(!this.#stylesheetPath) {
161
- // compile and merge hardcoded styles with src/styles.css and write to file "styles.min.css"
185
+ // this.#styles
186
+ // src/styles.css
187
+ // compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
162
188
  // which will be imported by webbrowser via '<link rel="stylesheet" href="styles.min.css"...'
163
189
 
164
190
  const __filename = fileURLToPath(import.meta.url);
@@ -167,32 +193,26 @@ export default class Blog {
167
193
  const publicStylePath = path.join(__dirname, "public", "styles.min.css");
168
194
 
169
195
  let publicHash = null;
170
- try {
171
- const publicCSS = await fs.promises.readFile(publicStylePath, "utf8");
172
- const match = publicCSS.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
173
- if (match) {
174
- publicHash = match[1];
175
- }
176
- } catch (err) {
177
- // public/styles.min.css doesn't exist, will be created.
178
- }
179
-
180
196
  let srcStyles = "";
181
- try {
182
- srcStyles = await fs.promises.readFile(srcStylePath, "utf8");
183
- } catch (err) {
184
- // ignore if src/styles.css doesn't exist
185
- }
186
197
 
187
- const combinedStyles = this.styles + srcStyles;
198
+ await Promise.all([
199
+ fs.promises.readFile(publicStylePath, "utf8").then((publicCSS) => {
200
+ const match = publicCSS.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
201
+ if (match) publicHash = match[1];
202
+ }).catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
203
+ fs.promises.readFile(srcStylePath, "utf8").then((content) => {
204
+ srcStyles = content;
205
+ }).catch((err) => console.error(err)) // ignore if src/styles.css doesn't exist
206
+ ]);
207
+
208
+ const combinedStyles = this.#styles + srcStyles;
188
209
  const srcHash = crypto.createHash("sha256").update(combinedStyles).digest("hex");
189
210
 
190
211
  if (srcHash !== publicHash) {
191
212
  console.log("Styles have changed. Recompiling...");
192
- //const finalStyles = await mergeStyles(this.styles, srcStyles);
193
- const finalStyles = this.styles + " " + srcStyles;
213
+ const finalStyles = await mergeStyles(this.#styles, srcStyles);
194
214
  try {
195
- await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
215
+ //await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
196
216
  await fs.promises.writeFile(publicStylePath, finalStyles + `\n/* source-hash: ${srcHash} */`);
197
217
  } catch (err) {
198
218
  console.error("Failed to write styles to public folder:", err);
@@ -201,7 +221,7 @@ export default class Blog {
201
221
  }
202
222
  if (this.#isExternalAPI) {
203
223
  console.log("external API");
204
- await this.loadFromAPI();
224
+ await this.#loadFromAPI();
205
225
  } else {
206
226
  console.log(`database: ${this.database.type}`);
207
227
  if (!this.#databaseModel) {
@@ -209,10 +229,11 @@ export default class Blog {
209
229
  }
210
230
  console.log(`connected to database`);
211
231
  await this.#databaseModel.initialize();
212
- const dbTitle = await this.#databaseModel.getBlogTitle();
213
- const dbArticles = await this.#databaseModel.findAll();
214
- //console.log(`articles: ${JSON.stringify(dbarticles)}`)
215
- console.log(`dbArticles.length: ${dbArticles.length}`)
232
+ const [dbTitle, dbArticles] = await Promise.all([
233
+ this.#databaseModel.getBlogTitle(),
234
+ this.#databaseModel.findAll(),
235
+ ]);
236
+
216
237
  if(dbArticles.length == 0) {
217
238
  dbArticles.push(new Article("Sample Entry #1", "Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.", new Date()));
218
239
  dbArticles.push(new Article("Sample Entry #2", "Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.", new Date()));
@@ -244,12 +265,14 @@ export default class Blog {
244
265
  const newArticleData = { title, content };
245
266
  try {
246
267
  // Save the new article to the database via the ApiServer
247
- if (this.#databaseModel) await this.#databaseModel.save(newArticleData);
248
- if (this.#isExternalAPI) await postData(this.#apiUrl, newArticleData);
268
+ const promises = [];
269
+ if (this.#databaseModel) promises.push(this.#databaseModel.save(newArticleData));
270
+ if (this.#isExternalAPI) promises.push(postData(this.#apiUrl, newArticleData));
271
+ await Promise.all(promises);
249
272
  // Add the article to the local list for immediate display
250
- this.articles.unshift(new Article(title, content, new Date()));
273
+ this.#articles.unshift(new Article(title, content, new Date()));
251
274
  // remove sample entries
252
- this.articles = this.articles.filter(
275
+ this.#articles = this.#articles.filter(
253
276
  (art) =>
254
277
  art.title !== "Sample Entry #1" && art.title !== "Sample Entry #2"
255
278
  );
@@ -286,18 +309,18 @@ export default class Blog {
286
309
  </body></html>`);
287
310
  return;
288
311
  } else if (req.method === "POST") {
289
- await this.handleLogin(req, res);
312
+ await this.#handleLogin(req, res);
290
313
  return;
291
314
  }
292
315
  }
293
316
 
294
317
  if (req.url === "/logout") {
295
- this.handleLogout(req, res);
318
+ this.#handleLogout(req, res);
296
319
  return;
297
320
  }
298
321
  // POST new article
299
322
  if (req.method === "POST" && req.url === "/") {
300
- if (!this.isAuthenticated(req)) {
323
+ if (!this.#isAuthenticated(req)) {
301
324
  res.writeHead(403, { "Content-Type": "text/plain" });
302
325
  res.end("Forbidden");
303
326
  return;
@@ -310,12 +333,12 @@ export default class Blog {
310
333
  // reload styles and scripts on (every) request
311
334
  if(this.reloadStylesOnGET) {
312
335
  if (this.#stylesheetPath) {
313
- await this.processStylesheets(this.#stylesheetPath);
336
+ await this.#processStylesheets(this.#stylesheetPath);
314
337
  }
315
338
  }
316
339
 
317
340
  let loggedin = false;
318
- if (!this.isAuthenticated(req)) {
341
+ if (!this.#isAuthenticated(req)) {
319
342
  // login
320
343
  loggedin = false;
321
344
  } else {
@@ -374,8 +397,11 @@ export default class Blog {
374
397
 
375
398
  this.#server = server;
376
399
 
377
- return new Promise((resolve) => {
378
- this.#server.listen(port, () => {
400
+ return new Promise((resolve, reject) => {
401
+ const errorHandler = (err) => reject(err);
402
+ this.#server.once("error", errorHandler);
403
+ this.#server.listen(port, '127.0.0.1', () => {
404
+ this.#server.removeListener("error", errorHandler);
379
405
  console.log(`server running at http://localhost:${port}/`);
380
406
  resolve(); // Resolve the promise when the server is listening
381
407
  });
@@ -386,7 +412,7 @@ export default class Blog {
386
412
  return new Promise((resolve, reject) => {
387
413
  if (this.#server) {
388
414
  this.#server.close((err) => {
389
- if (err) return reject(err);
415
+ if (err && err.code !== "ERR_SERVER_NOT_RUNNING") return reject(err);
390
416
  console.log("Server closed.");
391
417
  resolve();
392
418
  });
@@ -398,19 +424,19 @@ export default class Blog {
398
424
 
399
425
  /** Populates the blog's title and articles from a data object. */
400
426
  #applyBlogData(data) {
401
- this.articles = []; // Clear existing articles before loading new ones
427
+ this.#articles = []; // Clear existing articles before loading new ones
402
428
  this.#title = data.title;
403
- // Assuming the API returns an array of objects with title and content
429
+ // Assuming data contains a title and an array of articles with title and content
404
430
  if (data.articles && Array.isArray(data.articles)) {
405
431
  for (const articleData of data.articles) {
406
432
  const article = new Article(articleData.title, articleData.content, articleData.createdAt);
407
- article.id = articleData.id;
433
+ article.id = articleData.id; // TODO x
408
434
  this.addArticle(article);
409
435
  }
410
436
  }
411
437
  }
412
438
 
413
- async loadFromAPI() {
439
+ async #loadFromAPI() {
414
440
  const data = await fetchData(this.#apiUrl);
415
441
  if (data) {
416
442
  this.#applyBlogData(data);
@@ -451,7 +477,7 @@ export default class Blog {
451
477
 
452
478
  // POST a new article
453
479
  } else if (req.method === "POST" && pathname === "/api/articles") {
454
- if (!this.isAuthenticated(req)) {
480
+ if (!this.#isAuthenticated(req)) {
455
481
  res.writeHead(403, { "Content-Type": "application/json" });
456
482
  res.end(JSON.stringify({ error: "Forbidden" }));
457
483
  return;
@@ -481,7 +507,7 @@ export default class Blog {
481
507
  print() {
482
508
  const data = {
483
509
  title: this.title,
484
- articles: this.articles,
510
+ articles: this.#articles,
485
511
  };
486
512
  const markdown = formatMarkdown(data);
487
513
  console.log(markdown);
@@ -491,7 +517,7 @@ export default class Blog {
491
517
  async toHTML(loggedin) {
492
518
  const data = {
493
519
  title: this.title,
494
- articles: this.articles,
520
+ articles: this.#articles,
495
521
  loggedin,
496
522
  login: ""
497
523
  };
@@ -505,19 +531,16 @@ export default class Blog {
505
531
  }
506
532
 
507
533
  /**
508
- * Loads files, generates checksums and compiles sass files to css if anything changed
534
+ * read files, compare checksums, compile and write to public/styles.min.css
509
535
  * @param {string[]} files - Array of css/scss file paths to process.
510
536
  */
511
- async processStylesheets(files) {
537
+ async #processStylesheets(files) {
512
538
  console.log("process stylesheets")
513
539
 
514
540
  // Normalize input to array (handles string or array)
515
541
  // "file1.css" --> ["file1.css"]
516
542
  // ["file1.css", "file2.css",...]
517
543
  const fileList = Array.isArray(files) ? files : [files];
518
-
519
- const __filename = fileURLToPath(import.meta.url);
520
- const __dirname = path.dirname(__filename);
521
544
  const styleFiles = fileList.filter(
522
545
  (f) => typeof f === "string" && (f.endsWith(".scss") || f.endsWith(".css")) && !f.endsWith(".min.css")
523
546
  );
@@ -552,8 +575,7 @@ export default class Blog {
552
575
  this.#stylesHash = currentHash;
553
576
 
554
577
  // Compile styles using the standalone script from build-styles.js
555
- //this.compiledStyles = await compileStyles(fileData);
556
- this.compiledStyles = fileData.map((f) => f.content).join("\n");
578
+ this.compiledStyles = await compileStyles(fileData);
557
579
 
558
580
  // generate a file
559
581
  const __filename = fileURLToPath(import.meta.url);
@@ -0,0 +1,59 @@
1
+ import path from "path";
2
+ import { pathToFileURL } from "url";
3
+ import postcss from "postcss";
4
+ import autoprefixer from "autoprefixer";
5
+ import cssnano from "cssnano";
6
+
7
+ // array of files or a single file
8
+ export async function compileStyles(fileData) {
9
+ try {
10
+ let combinedCss = "";
11
+
12
+ if (fileData) {
13
+ const scssFiles = fileData.filter(
14
+ (f) =>
15
+ f.path.endsWith(".scss") && !path.basename(f.path).startsWith("_")
16
+ );
17
+
18
+ for (const file of scssFiles) {
19
+ console.error("sass files are not supported.");
20
+ }
21
+
22
+ const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
23
+ for (const file of cssFiles) {
24
+ combinedCss += file.content + "\n";
25
+ }
26
+ }
27
+
28
+ // 2. PostCSS (Autoprefixer + CSSNano)
29
+ if (combinedCss) {
30
+ const plugins = [autoprefixer(), cssnano()];
31
+ const result = await postcss(plugins).process(combinedCss, {
32
+ from: undefined,
33
+ });
34
+ return result.css;
35
+ }
36
+ return "";
37
+ } catch (error) {
38
+ console.error("Build failed:", error);
39
+ return "";
40
+ }
41
+ }
42
+
43
+ export async function mergeStyles(...cssContents) {
44
+ try {
45
+ const combinedCss = cssContents.join("\n");
46
+
47
+ if (combinedCss) {
48
+ const plugins = [autoprefixer(), cssnano()];
49
+ const result = await postcss(plugins).process(combinedCss, {
50
+ from: undefined,
51
+ });
52
+ return result.css;
53
+ }
54
+ return "";
55
+ } catch (error) {
56
+ console.error("Merge failed:", error);
57
+ return "";
58
+ }
59
+ }
@@ -12,6 +12,7 @@ export default class PostgresAdapter extends SequelizeAdapter {
12
12
  this.host = options.host;
13
13
  if (options.dbname) this.dbname = options.dbname;
14
14
  if (options.dbport) this.dbport = options.dbport;
15
+ else this.dbport = 5432;
15
16
  if (!this.username || !this.password || !this.host) {
16
17
  throw new Error(
17
18
  "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
@@ -21,24 +22,39 @@ export default class PostgresAdapter extends SequelizeAdapter {
21
22
 
22
23
  async initialize() {
23
24
  console.log("initialize database");
25
+ const maxRetries = 10;
26
+ const retryDelay = 3000;
24
27
 
25
- try {
26
- console.log(
27
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
28
- );
28
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
29
+ try {
30
+ console.log(
31
+ `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
32
+ );
29
33
 
30
- this.sequelize = new Sequelize(
31
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
32
- { logging: false }
33
- );
34
- } catch (err) {
35
- if (err.message.includes("Please install")) {
36
- throw new Error(
37
- "PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
34
+ this.sequelize = new Sequelize(
35
+ `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
36
+ { logging: false }
37
+ );
38
+ await this.sequelize.authenticate();
39
+ await this.initializeModels();
40
+ console.log("Database connection established successfully.");
41
+ return;
42
+ } catch (err) {
43
+ if (err.message.includes("Please install")) {
44
+ throw new Error(
45
+ "PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
46
+ );
47
+ }
48
+ console.error(
49
+ `Database connection attempt ${attempt} failed: ${err.message}`
38
50
  );
51
+ if (attempt === maxRetries) {
52
+ console.error("Max retries reached. Exiting.");
53
+ throw err;
54
+ }
55
+ console.log(`Retrying in ${retryDelay / 1000} seconds...`);
56
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
39
57
  }
40
- throw err;
41
58
  }
42
- await this.initializeModels();
43
59
  }
44
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -17,11 +17,15 @@
17
17
  "author": "lexho111",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
+ "autoprefixer": "^10.4.23",
20
21
  "child_process": "^1.0.2",
22
+ "cssnano": "^7.1.2",
21
23
  "fs": "^0.0.1-security",
22
24
  "http": "^0.0.1-security",
23
25
  "node-fetch": "^3.3.2",
24
26
  "path": "^0.12.7",
27
+ "postcss": "^8.5.6",
28
+ "postcss-preset-env": "^10.6.0",
25
29
  "sass": "^1.97.1",
26
30
  "url": "^0.11.4",
27
31
  "util": "^0.12.5"
@@ -1,68 +1,2 @@
1
- body { font-family: Arial; } .grid {
2
- border: 0 solid #000;
3
- display: grid;
4
- gap: 0.25rem;
5
- grid-template-columns: 1fr;
6
- }
7
- .grid article {
8
- border: 0 solid #ccc;
9
- border-radius: 4px;
10
- min-width: 0;
11
- overflow-wrap: break-word;
12
- padding: 0.25rem;
13
- }
14
- .grid article h2 {
15
- color: rgb(53, 53, 53);
16
- margin-bottom: 5px;
17
- }
18
-
19
- .grid article .datetime {
20
- margin: 0;
21
- color: rgb(117, 117, 117);
22
- }
23
-
24
- .grid article p {
25
- margin-top: 10px;
26
- margin-bottom: 0;
27
- }
28
-
29
- article a {
30
- color: rgb(105, 105, 105);
31
- }
32
-
33
- article a:visited {
34
- color: rgb(105, 105, 105);
35
- }
36
-
37
- h1 {
38
- color: #696969;
39
- }
40
- nav a {
41
- color: #3b40c1;
42
- font-size: 20px;
43
- text-decoration: underline;
44
- }
45
- nav a:visited {
46
- color: #3b40c1;
47
- text-decoration-color: #3b40c1;
48
- }
49
-
50
- #wrapper {
51
- max-width: 500px;
52
- width: 100%;
53
- }
54
-
55
- /* Mobile Layout (screens smaller than 1000px) */
56
- @media screen and (max-width: 1000px) {
57
- * {
58
- font-size: 4vw;
59
- }
60
- #wrapper {
61
- max-width: 100%;
62
- width: 100%;
63
- padding: 0 10px; /* Prevents text from touching the edges */
64
- box-sizing: border-box;
65
- }
66
- }
67
-
68
- /* source-hash: bcb6644ec5b5c6f9685c9ad6c14ee551a6f908b8a2c372d3294e2d2e80d17fb7 */
1
+ body{background-color:#fdfdfd;font-family:Arial}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}.datetime{color:#434343;font-style:normal}h2{color:#a9a9a9;margin:0 0 5px}p{margin-top:10px}span{margin:0}
2
+ /* source-hash: fa9deb7a7f0781f463cd3e8fd3c3ceddec518535b0a6d13af7309ef9a2f76c32 */
package/test/blog.test.js CHANGED
@@ -6,6 +6,46 @@ import Article from "../Article.js";
6
6
  import { jest } from "@jest/globals";
7
7
 
8
8
  describe("test blog", () => {
9
+ test("blog bootstrap stage 1", () => {
10
+ const myblog = new Blog();
11
+ const json = myblog.json();
12
+ expect(json.version).toContain(".");
13
+ const database = json.database;
14
+ expect(database.type).toBe("file");
15
+ expect(database.username).toBe("user");
16
+ expect(database.password).toBe("password");
17
+ expect(database.host).toBe("localhost");
18
+ expect(database.dbname).toBe("articles.txt"); //TODO switch blog.json to articles.txt, bloginfo.json
19
+ expect(json.password).toBe("admin");
20
+ expect(json.styles).toContain("body { font-family: Arial; }");
21
+ expect(json.reloadStylesOnGET).not.toBeTruthy();
22
+ });
23
+ test("blog bootstrap stage 2", async () => {
24
+ const myblog = new Blog();
25
+ await myblog.init();
26
+ const json = myblog.json();
27
+ console.log(json);
28
+ expect(json.title).toBe("Test Blog Title");
29
+ expect(json.articles.length).toBeGreaterThan(2);
30
+ expect(json.server).toBeNull();
31
+ });
32
+ test("blog bootstrap stage 3", async () => {
33
+ const myblog = new Blog();
34
+ await myblog.init();
35
+ try {
36
+ await myblog.startServer(8080);
37
+ const json = myblog.json();
38
+ console.log(json);
39
+ expect(json.title).toBe("Test Blog Title"); // from bloginfo.json
40
+ expect(json.articles.length).toBeGreaterThan(2);
41
+ expect(json.server.listening).toBeTruthy();
42
+ expect(json.server.address.address).toBe("127.0.0.1");
43
+ expect(json.server.address.family).toBe("IPv4");
44
+ expect(json.server.address.port).toBe(8080);
45
+ } finally {
46
+ await myblog.closeServer();
47
+ }
48
+ });
9
49
  test("is valid html", async () => {
10
50
  const myblog = new Blog();
11
51
  const html = await myblog.toHTML();
@@ -35,21 +75,27 @@ describe("test blog", () => {
35
75
  const article = new Article("", "");
36
76
  myblog.addArticle(article);
37
77
  const html = await myblog.toHTML();
78
+ const json = myblog.json();
38
79
  expect(html).toContain("<article");
39
- expect(myblog.articles.length).toBe(1);
80
+ expect(json.articles).toHaveLength(1);
40
81
  });
41
82
  test("add articles", async () => {
42
83
  const myblog = new Blog();
43
- expect(myblog.articles.length).toBe(0);
84
+ const json = myblog.json();
85
+ expect(json.articles).toHaveLength(0);
44
86
  const size = 10;
45
87
  for (let i = 1; i <= size; i++) {
46
88
  const article = new Article("", "");
47
89
  myblog.addArticle(article);
48
- expect(myblog.articles.length).toBe(i);
90
+ const json = myblog.json();
91
+ expect(json.articles).toHaveLength(i);
92
+ }
93
+ {
94
+ const html = await myblog.toHTML();
95
+ expect(html).toContain("<article");
96
+ const json = myblog.json();
97
+ expect(json.articles).toHaveLength(size);
49
98
  }
50
- const html = await myblog.toHTML();
51
- expect(html).toContain("<article");
52
- expect(myblog.articles.length).toBe(size);
53
99
  });
54
100
  const __filename = fileURLToPath(import.meta.url);
55
101
  const __dirname = path.dirname(__filename);
@@ -58,13 +104,13 @@ describe("test blog", () => {
58
104
  const styles = [
59
105
  {
60
106
  style: "body { font-family: Courier; }",
61
- expected: "font-family: Courier",
107
+ expected: "font-family:Courier",
62
108
  },
63
109
  {
64
110
  style: "body{ background-color:black; color:white; }",
65
- expected: "background-color:black; color:white",
111
+ expected: "background-color:#000;color:#fff;",
66
112
  },
67
- { style: "body{ font-size: 1.2em; }", expected: "font-size: 1.2em" },
113
+ { style: "body{ font-size: 1.2em; }", expected: "font-size:1.2em" },
68
114
  ];
69
115
  for (const style of styles) {
70
116
  const cssPath = path.join(publicDir, "styles.min.css");
@@ -13,6 +13,7 @@ import {
13
13
  } from "../model/FileModel.js";
14
14
 
15
15
  import SqliteAdapter from "../model/SqliteAdapter.js";
16
+ import FileAdapter from "../model/FileAdapter.js";
16
17
 
17
18
  function generateRandomContent(length) {
18
19
  let str = "";
@@ -25,44 +26,6 @@ function generateRandomContent(length) {
25
26
  return str;
26
27
  }
27
28
 
28
- describe("File Model test", () => {
29
- it("should init files and load info", async () => {
30
- const infoFile = "test_bloginfo.json";
31
- const articlesFile = "test_articles.txt";
32
-
33
- // Ensure clean state
34
- if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
35
- if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
36
-
37
- await initFiles(infoFile, articlesFile);
38
-
39
- const info = await loadInfo(infoFile);
40
- expect(info.title).toBe("Blog");
41
-
42
- // Cleanup
43
- fs.unlinkSync(infoFile);
44
- fs.unlinkSync(articlesFile);
45
- });
46
-
47
- it("should write to articles.txt file", async () => {
48
- const content = generateRandomContent(200);
49
- const article = new Article("hello", content, new Date().toISOString());
50
- const filename = "test_articles.txt";
51
-
52
- await appendArticle(filename, article);
53
- const articles = await loadArticles(filename);
54
-
55
- // Compare properties since 'articles' contains plain objects, not Article instances
56
- expect(articles.length).toBeGreaterThan(0);
57
- const lastArticle = articles[articles.length - 1];
58
- expect(lastArticle.title).toBe(article.title);
59
- expect(lastArticle.content).toBe(article.content);
60
-
61
- // Cleanup
62
- if (fs.existsSync(filename)) fs.unlinkSync(filename);
63
- });
64
- });
65
-
66
29
  describe("API Model test", () => {
67
30
  beforeAll((done) => {
68
31
  const port = 8081;
@@ -127,55 +90,140 @@ describe("API Model test", () => {
127
90
  });
128
91
 
129
92
  describe("Database Model test", () => {
130
- it("should be empty", async () => {
131
- const blog = new Blog();
132
- blog.database.type = "sqlite";
133
- blog.database.dbname = "test";
134
- blog.setStyle(
135
- "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
136
- );
93
+ describe("Sqlite Database Adapter test", () => {
94
+ it("should be empty", async () => {
95
+ const blog = new Blog();
96
+ const sqliteAdapter = new SqliteAdapter({
97
+ dbname: "blog",
98
+ });
99
+ blog.setDatabaseAdapter(sqliteAdapter);
100
+ await blog.init();
101
+
102
+ expect(await blog.toHTML()).not.toContain("<article>");
103
+ expect(blog).toBeDefined();
104
+ });
137
105
 
138
- //const article = new Article("hello", "hello world1!");
139
- //blog.postArticle(article);
106
+ it("should load blog data from sqlite database", async () => {
107
+ const blog = new Blog();
108
+ const sqliteAdapter = new SqliteAdapter({
109
+ dbname: "blog",
110
+ });
111
+ blog.setDatabaseAdapter(sqliteAdapter);
112
+ await blog.init();
113
+
114
+ const content = generateRandomContent(200);
115
+
116
+ await new Promise((resolve) => {
117
+ const req = {
118
+ on: (event, cb) => {
119
+ if (event === "data") {
120
+ setTimeout(() => cb(`title=hello&content=${content}`), 0);
121
+ }
122
+ if (event === "end") {
123
+ setTimeout(() => cb(), 20);
124
+ }
125
+ },
126
+ off: () => {},
127
+ };
128
+ const res = {
129
+ writeHead: () => {},
130
+ end: resolve,
131
+ };
132
+ blog.postArticle(req, res);
133
+ });
134
+
135
+ expect(await blog.toHTML()).toContain(content);
136
+ expect(blog).toBeDefined();
137
+ });
138
+ });
139
+ describe("Postgres Adapter test", () => {});
140
+ describe("File Adapter test", () => {
141
+ it("should update blog title", async () => {
142
+ const title_org = "Test Blog Title";
143
+ const fileAdapter = new FileAdapter();
144
+ await fileAdapter.initialize();
145
+ await fileAdapter.updateBlogTitle(title_org);
146
+ const title = await fileAdapter.getBlogTitle();
147
+ console.log(title);
148
+ expect(title).toBe(title_org);
149
+ });
150
+ });
151
+ describe("File Model test", () => {
152
+ it("should init files and load info", async () => {
153
+ const infoFile = "test_bloginfo.json";
154
+ const articlesFile = "test_articles.txt";
140
155
 
141
- //await blog.load("blog.json");
156
+ // Ensure clean state
157
+ if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
158
+ if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
142
159
 
143
- expect(await blog.toHTML()).not.toContain("<article>");
144
- expect(blog).toBeDefined();
145
- });
160
+ await initFiles(infoFile, articlesFile);
146
161
 
147
- it("should load blog data from sqlite database", async () => {
148
- const blog = new Blog();
149
- const sqliteAdapter = new SqliteAdapter({
150
- dbname: "blog",
162
+ const info = await loadInfo(infoFile);
163
+ expect(info.title).toBe("Blog");
164
+
165
+ // Cleanup
166
+ fs.unlinkSync(infoFile);
167
+ fs.unlinkSync(articlesFile);
151
168
  });
152
- blog.setDatabaseAdapter(sqliteAdapter);
153
- await blog.init();
154
169
 
155
- const content = generateRandomContent(200);
170
+ it("should read and write to articles.txt file", async () => {
171
+ const content = generateRandomContent(200);
172
+ const article = new Article("hello", content, new Date().toISOString());
173
+ const filename = "test_articles.txt";
156
174
 
157
- await new Promise((resolve) => {
158
- const req = {
159
- on: (event, cb) => {
160
- if (event === "data") {
161
- setTimeout(() => cb(`title=hello&content=${content}`), 0);
162
- }
163
- if (event === "end") {
164
- setTimeout(() => cb(), 20);
165
- }
166
- },
167
- off: () => {},
168
- };
169
- const res = {
170
- writeHead: () => {},
171
- end: resolve,
172
- };
173
- blog.postArticle(req, res);
175
+ await appendArticle(filename, article);
176
+ const articles = await loadArticles(filename);
177
+
178
+ // Compare properties since 'articles' contains plain objects, not Article instances
179
+ expect(articles.length).toBeGreaterThan(0);
180
+ const lastArticle = articles[articles.length - 1];
181
+ expect(lastArticle.title).toBe(article.title);
182
+ expect(lastArticle.content).toBe(article.content);
183
+
184
+ // Cleanup
185
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
174
186
  });
175
187
 
176
- //await blog.load("blog.json");
188
+ it("should be empty articles.txt", async () => {
189
+ const filename = "test_articles_empty.txt";
190
+ const articles = await loadArticles(filename);
191
+ expect(articles.length).toBe(0);
192
+ // Cleanup
193
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
194
+ });
177
195
 
178
- expect(await blog.toHTML()).toContain(content);
179
- expect(blog).toBeDefined();
196
+ it("should read and write multiple articles to articles.txt file", async () => {
197
+ const filename = "test_articles.txt";
198
+
199
+ // Ensure clean state
200
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
201
+
202
+ try {
203
+ const articles_org = [];
204
+ const count = 20;
205
+ for (let i = 0; i < count; i++) {
206
+ const title = generateRandomContent(20);
207
+ const content = generateRandomContent(200);
208
+ const article = new Article(title, content, new Date().toISOString());
209
+ articles_org.push(article);
210
+
211
+ await appendArticle(filename, article);
212
+ }
213
+ const articles = await loadArticles(filename);
214
+
215
+ // Compare properties since 'articles' contains plain objects, not Article instances
216
+ expect(articles).toHaveLength(count);
217
+
218
+ for (let i = 0; i < articles.length; i++) {
219
+ expect(articles[i].title).toBe(articles_org[i].title);
220
+ expect(articles[i].content).toBe(articles_org[i].content);
221
+ expect(articles[i].createdAt).toBe(articles_org[i].createdAt);
222
+ }
223
+ } finally {
224
+ // Cleanup
225
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
226
+ }
227
+ });
180
228
  });
181
229
  });
@@ -16,10 +16,26 @@ describe("Blog Stylesheet Test", () => {
16
16
  expect(data).toContain("body");
17
17
  expect(data).toContain("nav a");
18
18
  expect(data).toContain(".datetime");
19
- expect(data).toContain("font-style: normal");
20
19
  expect(data).toContain("color: darkgray");
21
20
  });
22
21
 
22
+ it("should load the stylesheet (.css) file from public", async () => {
23
+ const filepath = path.join(__dirname, "../public/styles.min.css");
24
+
25
+ const data = await fs.promises.readFile(filepath, "utf8");
26
+ console.log(data);
27
+ expect(data).toContain("font-family:Arial");
28
+ expect(data).toContain("h1");
29
+ expect(data).toContain(".grid{");
30
+ expect(data).toContain(".grid article");
31
+ expect(data).toContain("nav a");
32
+ expect(data).toContain(".datetime");
33
+ expect(data).toContain("nav a:visited{");
34
+ expect(data).toContain("@media screen");
35
+ expect(data).toContain("#wrapper{");
36
+ expect(data).not.toContain("color:darkgray");
37
+ });
38
+
23
39
  it("should load and compile the stylesheet (.css) correctly", async () => {
24
40
  const blog = new Blog();
25
41
  blog.title = "My Blog";
@@ -38,7 +54,7 @@ describe("Blog Stylesheet Test", () => {
38
54
  expect(publicCSS).toContain("body");
39
55
  expect(publicCSS).toContain("nav a");
40
56
  expect(publicCSS).toContain(".datetime");
41
- expect(publicCSS).toContain("font-style: normal");
57
+ expect(publicCSS).toContain("font-style:normal");
42
58
  expect(publicCSS).toContain("color:");
43
59
  });
44
60
 
@@ -60,7 +76,7 @@ describe("Blog Stylesheet Test", () => {
60
76
  expect(publicCSS).toContain("body");
61
77
  expect(publicCSS).toContain("nav a");
62
78
  expect(publicCSS).toContain(".datetime");
63
- expect(publicCSS).toContain("font-style: normal");
79
+ expect(publicCSS).toContain("font-style:normal");
64
80
  expect(publicCSS).toContain("color:");
65
81
  });
66
82
  });
package/articles.txt DELETED
@@ -1,3 +0,0 @@
1
- {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:19:39.939Z"}
2
- {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:34:38.866Z"}
3
- {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:43:32.343Z"}
package/blog.db DELETED
Binary file
package/blog.json DELETED
@@ -1,110 +0,0 @@
1
- {
2
- "title": "",
3
- "articles": [
4
- {
5
- "title": "hello",
6
- "content": "hML`JfDS]ARPVAfsGdogseNVYPXTpZaJhKJkPoMTWA\\q[CkNsXYrUGyGHWTDZpByubZMf_Y\\bMELh[]afM^XMAFqvxuyWat^hqSBMIifvbTdHNOQP_rbwwlWnGvVbPi]jlTIixyAaaU_]ecJYTjr]FqLq`UY\\XoYqhmuY`rvWH]EZO`tBHZSan`mAuhnwpXgdRJtjtH[",
7
- "createdAt": "2026-01-07T12:49:43.098Z"
8
- },
9
- {
10
- "title": "hello",
11
- "content": "HgcFKFWDMl]cxiCVVrgFddJaOQCSPF^KYkROxkLTmxSc^ZssZh`ENyOXZn_DtOQe^V_vbu^KRonZ]bGcqsP]FYMmY\\YLjWknhiUksXwIUsvtpfXqViiKJ`xcmScZRXfAdHshvFlEHvPXWOfZRRIDIOxAafPoMD[ZgVlFkl\\Uw]RfubZWQ]`IHIUOKea_RTO`KiZisSA[",
12
- "createdAt": "2026-01-07T12:49:43.444Z"
13
- },
14
- {
15
- "title": "Test Title from Jest",
16
- "content": "This is the content of the test article.",
17
- "createdAt": "2026-01-07T13:10:50.239Z"
18
- },
19
- {
20
- "title": "hello",
21
- "content": "_\\jAuGsYByS]q\\QsnlqT\\]]TEDDalywtlmwmBZRlL[vtFdGqB\\[dlVx]A^[IV`KjhWU`DW`oorGwUlBU_IMCYoymIFi[`FpXrJfUmLZTRhoisxhXIcWqmk\\SPMV^]dvCuL_Pu^EAcMvBfadWFbUuSxFkjh\\KQyLnNWWsP`agQQkmU\\TAaOvYZAO\\FGtstSbaCJPXRQTO",
22
- "createdAt": "2026-01-07T13:10:57.249Z"
23
- },
24
- {
25
- "title": "hello",
26
- "content": "HRkfwNRL_r\\DF]tyVBM[AMIjfMoBbohiqUf_T^]LwiJo[duXB`mQiRmmTkbMSJxvvHccfbXsdmKMbuYmxo[JGkwfCpqXSbhKiZID[vouGhbnLZ[wjkFFgm]fOaZcxoWduI`TNvin]\\dXoGW]YtueqSYPRLmGJswvUnyPHhfLUtf`]QNfM]hcvqqZasrQZVcXYjAnJxRP",
27
- "createdAt": "2026-01-07T13:10:57.357Z"
28
- },
29
- {
30
- "title": "hello",
31
- "content": "UI\\KBodT[CgUg]L^WJ`Tuafq`rS]n\\T_eMqmBQqJVQisfABj^msS]u\\OEoJ]D^CtwE`UY[UbOICHcYPjNqNQ[LlwlJGPOoppSrkaEmDh_tm]CrrPGbBSPxgQQIZ^efcmITQQaAgT_vx[GUepg]LDagmTmihHbOIqHAa_NmRsX_EjxeE]EKhnHwFm]vFUpJTPFiDNU^BE",
32
- "createdAt": "2026-01-07T13:17:47.559Z"
33
- },
34
- {
35
- "title": "hello",
36
- "content": "aMDnSSZDIZjbfrMlOcjMB\\CPup[GfoqHkLWAi]V_Ctkt[ekYRqIaufps[]TOMcw\\ojbVpPNv`lTTH\\[jbOcJXU[_tBSCsZW\\akiWuRf^mZGGrFxAq^oAdZjHwmJBjt^_GUXnHdXfjSrifiqXdAAdo]EM`aBFpJvyVLnLOfqrqoPrAIhRwoPHRy^lRCLyaS^VXChowHCn",
37
- "createdAt": "2026-01-07T13:17:47.681Z"
38
- },
39
- {
40
- "title": "Test Title from Jest",
41
- "content": "This is the content of the test article.",
42
- "createdAt": "2026-01-07T13:18:50.764Z"
43
- },
44
- {
45
- "title": "hello",
46
- "content": "[vL^GSDHJjBq^qdTMyyMm`SGhZhvgXsyTp[kYNkiBDavKevpIkSlWQr`bJaWyH]Qa`LQMKisdHeRvTKm`dviPLkLcJCtWbGPaDnNhJtEcEQM^MmP[RYERk\\uYeXruvPmJMjdvsOxA`btQtTmtBlOvrHLANJhcYRTAXmwOjLhp_SgkhWsB_pcIUQbd^X`RIWAdqUBPVmk",
47
- "createdAt": "2026-01-07T13:18:52.387Z"
48
- },
49
- {
50
- "title": "hello",
51
- "content": "jRaJYEUFnMoEcjrWFREUuIQEoCtLoEuJSxJapEGpdPvq[GyrOqdSLY`VDlqSMTh\\BgybfwxfQQ_QqKkh\\YcEHYpA^W[ttGyGjA[TdZHn]]yoPWnFyFHJfAUkxkSM[RkQVqxAg_AGoJahaoOsNnoSGx^uM\\smWlWVDGvRcWVW\\rI_[lKnZEifvdPwkCcBRLXKGw[aNedN",
52
- "createdAt": "2026-01-07T13:18:52.971Z"
53
- },
54
- {
55
- "title": "Test Title from Jest",
56
- "content": "This is the content of the test article.",
57
- "createdAt": "2026-01-07T13:22:54.227Z"
58
- },
59
- {
60
- "title": "Test Title from Jest",
61
- "content": "This is the content of the test article.",
62
- "createdAt": "2026-01-07T13:23:08.140Z"
63
- },
64
- {
65
- "title": "hello",
66
- "content": "M_RFGy[auVjFYypv^FdPqesIlcBkg]uoeIwMNLM]TOcDWKw[CFpsPpQWxSAXAupUdVsaiGJgLgIeuJEwBMXk`VG\\OoXIxCERYs`hqGRYGTnP]ZIrOSoJk[MFAxfVjJNMyZycryoRWr\\bhCXlurxgKPFSsTYbhlqZmrqaiJyvXpFsbiLQx^LBcZi`VqZSbjc`TNF_\\Eex",
67
- "createdAt": "2026-01-07T13:26:02.437Z"
68
- },
69
- {
70
- "title": "hello",
71
- "content": "\\uRyVjar]vDZKHqRqJbEJpZbdKCWmQOGToNFQAqWdJQxSwmKTAGXywMDF`D\\nBA_eJxmFV^XtTuSGNJZTFjYvYibQIlgZdV[YXZpujmuWBgKNNBjrGZLVC[QlRZOwBdY^QPom\\b^WIIkeawsGBaafELGmfmTaIikRhCnCV_SYKEwogtqdfhkMkJslllumPyeGKqe\\cca",
72
- "createdAt": "2026-01-07T13:26:02.816Z"
73
- },
74
- {
75
- "title": "Test Title from Jest",
76
- "content": "This is the content of the test article.",
77
- "createdAt": "2026-01-07T13:26:07.866Z"
78
- },
79
- {
80
- "title": "hello",
81
- "content": "UecpUr`PFkpfKJ^V^q[T_lTr\\uP\\WFwcFtZgrdoao[japNpDycoAgWyELuIZtQroKxXSYdI]LrDTQSGqegWjgArWk[[eYXpy_OchWDywkus`GLcdwwr[qHoLINIdUoB^C]JcLfacOEJtMZOeWjRNbGcv\\XCfSAXUKIVqkPwJbaRTjlpGBxCGXxnchUhRoePYoLE]fexb",
82
- "createdAt": "2026-01-07T13:45:46.304Z"
83
- },
84
- {
85
- "title": "hello",
86
- "content": "ef[SK^DuSJrKOlieeJHPLLoGgS[ZO\\gk_dWggGZchFewEnwspxJlhCWQ\\aTEKlDj\\a\\xRj\\uJANSkNLwrnPoOxJ[h]CrLppVtoeCEWPXRpCcoNFU`YA\\RPK\\dk\\Jp^fbdmC`Hpuf[ZYExseoPRbAYAaIolQ_mkJKRfrKsthmsBtCZdeQiBhY\\XAodKo_rOeQvtfNtDQB",
87
- "createdAt": "2026-01-07T13:45:46.469Z"
88
- },
89
- {
90
- "title": "Test Title from Jest",
91
- "content": "This is the content of the test article.",
92
- "createdAt": "2026-01-07T13:45:47.311Z"
93
- },
94
- {
95
- "title": "Test Title from Jest",
96
- "content": "This is the content of the test article.",
97
- "createdAt": "2026-01-07T18:19:21.946Z"
98
- },
99
- {
100
- "title": "Test Title from Jest",
101
- "content": "This is the content of the test article.",
102
- "createdAt": "2026-01-07T18:21:43.833Z"
103
- },
104
- {
105
- "title": "Test Title from Jest",
106
- "content": "This is the content of the test article.",
107
- "createdAt": "2026-01-07T18:24:28.968Z"
108
- }
109
- ]
110
- }
package/bloginfo.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Blog"
3
- }
Binary file
Binary file