@lexho111/plainblog 0.0.5 → 0.0.6

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/Article.js CHANGED
@@ -9,6 +9,7 @@ export default class Article {
9
9
  }
10
10
 
11
11
  getContentShort() {
12
+ if (this.content.length < 400) return this.getContent();
12
13
  let contentShort = this.content.substring(0, 400);
13
14
  contentShort += "...";
14
15
  return contentShort;
package/Blog.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from "http";
2
+ import fetch from "node-fetch";
2
3
  import { promises as fs } from "fs";
3
4
  import { URLSearchParams } from "url";
4
5
  import Article from "./Article.js";
@@ -11,6 +12,33 @@ export default class Blog {
11
12
  this.filename = null;
12
13
  }
13
14
 
15
+ async fetchDataFromApi(apiUrl) {
16
+ try {
17
+ const response = await fetch(apiUrl);
18
+
19
+ if (!response.ok) {
20
+ throw new Error(`HTTP error! status: ${response.status}`);
21
+ }
22
+
23
+ const data = await response.json();
24
+ return data;
25
+ } catch (error) {
26
+ console.error("Failed to fetch data:", error);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async postDataToApi(apiUrl, postData) {
32
+ const response = await fetch(apiUrl, {
33
+ method: "POST",
34
+ body: JSON.stringify(postData),
35
+ headers: { "Content-Type": "application/json" },
36
+ });
37
+
38
+ const data = await response.json();
39
+ return data;
40
+ }
41
+
14
42
  setTitle(title) {
15
43
  this.title = title;
16
44
  }
@@ -23,8 +51,15 @@ export default class Blog {
23
51
  this.style = style;
24
52
  }
25
53
 
26
- startServer(port = 8080) {
27
- const server = http.createServer((req, res) => {
54
+ APIUrl = "";
55
+
56
+ setAPI(APIUrl) {
57
+ this.APIUrl = APIUrl;
58
+ }
59
+
60
+ async startServer(port = 8080) {
61
+ if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
62
+ const server = http.createServer(async (req, res) => {
28
63
  if (req.method === "POST") {
29
64
  let body = "";
30
65
  req.on("data", (chunk) => {
@@ -36,10 +71,16 @@ export default class Blog {
36
71
  const content = params.get("content");
37
72
 
38
73
  if (title && content) {
39
- this.addArticle(new Article(title, content));
40
- // Auto-save if a file has been loaded or saved before
41
- if (this.filename) {
42
- await this.save();
74
+ const newArticle = new Article(title, content);
75
+ this.addArticle(newArticle);
76
+
77
+ // Post the new article to the backend API
78
+ try {
79
+ const postUrl = new URL(this.APIUrl);
80
+ postUrl.pathname = "/blog/articles";
81
+ await this.postDataToApi(postUrl.toString(), newArticle);
82
+ } catch (error) {
83
+ console.error("Failed to post new article to API:", error);
43
84
  }
44
85
  }
45
86
 
@@ -48,8 +89,9 @@ export default class Blog {
48
89
  res.end();
49
90
  });
50
91
  } else {
92
+ const html = await this.toHTML();
51
93
  res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
52
- res.end(this.toHTML());
94
+ res.end(html);
53
95
  }
54
96
  });
55
97
 
@@ -59,6 +101,7 @@ export default class Blog {
59
101
  }
60
102
 
61
103
  async save(filename = this.filename) {
104
+ if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
62
105
  if (!filename) {
63
106
  console.error("Error: Filename not provided and not set previously.");
64
107
  return;
@@ -80,12 +123,30 @@ export default class Blog {
80
123
  async load(filename) {
81
124
  this.filename = filename;
82
125
  const data = await fs.readFile(filename, "utf8");
83
- const { title, articles } = JSON.parse(data);
84
- this.title = title;
85
- this.articles = articles;
126
+ const jsonData = JSON.parse(data);
127
+ this.title = jsonData.title;
128
+ this.articles = jsonData.articles.map(
129
+ (article) => new Article(article.title, article.content)
130
+ );
86
131
  }
87
132
 
88
- print() {
133
+ async loadFromAPI(apiUrl) {
134
+ const data = await this.fetchDataFromApi(apiUrl);
135
+ if (data) {
136
+ this.articles = []; // Clear existing articles before loading new ones
137
+ this.setTitle(data.title);
138
+ // Assuming the API returns an array of objects with title and content
139
+ if (data.articles && Array.isArray(data.articles)) {
140
+ for (const articleData of data.articles) {
141
+ this.addArticle(new Article(articleData.title, articleData.content));
142
+ }
143
+ this.apiDataLoaded = true; // Mark that we have successfully loaded data
144
+ }
145
+ }
146
+ }
147
+
148
+ async print() {
149
+ if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
89
150
  console.log(`# ${this.title}`);
90
151
  for (const article of this.articles) {
91
152
  console.log(`## ${article.title}`);
@@ -93,37 +154,40 @@ export default class Blog {
93
154
  }
94
155
  }
95
156
 
96
- toHTML() {
97
- let html = `
98
- <!DOCTYPE html>
99
- <html lang="de">
100
- <head>
101
- <meta charset="UTF-8">
102
- <title>${this.title}</title>
103
- <style>${this.style}</style>
104
- </head>
105
- <body>`;
106
- html += `<h1>${this.title}</h1><div style="width: 500px;">`;
107
-
108
- html += `
157
+ async toHTML() {
158
+ // If we have an API URL and haven't loaded data yet, load it now.
159
+ if (this.APIUrl && !this.apiDataLoaded) {
160
+ await this.loadFromAPI(this.APIUrl);
161
+ }
162
+
163
+ return `<!DOCTYPE html>
164
+ <html lang="de">
165
+ <head>
166
+ <meta charset="UTF-8">
167
+ <title>${this.title}</title>
168
+ <style>${this.style}</style>
169
+ </head>
170
+ <body>
171
+ <h1>${this.title}</h1>
172
+ <div style="width: 500px;">
109
173
  <form action="/" method="POST">
110
174
  <h3>Add a New Article</h3>
111
175
  <input type="text" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
112
176
  <textarea name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
113
177
  <button type="submit">Add Article</button>
114
178
  </form>
115
- <hr>`;
116
-
117
- for (const article of this.articles) {
118
- html += `
179
+ <hr>
180
+ ${this.articles
181
+ .map(
182
+ (article) => `
119
183
  <article>
120
184
  <h2>${article.title}</h2>
121
185
  <p>${article.getContentShort()}</p>
122
- </article>`;
123
- }
124
-
125
- html += `</div></body></html>`;
126
-
127
- return html;
186
+ </article>`
187
+ )
188
+ .join("")}
189
+ </div>
190
+ </body>
191
+ </html>`;
128
192
  }
129
193
  }
package/README.md CHANGED
@@ -1,21 +1,65 @@
1
+ # Plainblog
2
+
3
+ Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser. Articles are stored in a file or a database specified via an API.
4
+
5
+ ## Install
6
+
1
7
  ```
2
- import { Blog, Article } from "@lexho111/plainblog";
8
+ npm install @lexho111/plainblog
9
+ ```
10
+
11
+ ## Quick Start
3
12
 
4
- const bl og = new Blog();
13
+ ```
14
+ import { Blog } from "@lexho111/plainblog";
15
+
16
+ const blog = new Blog();
5
17
  blog.setTitle("My Blog");
6
18
  blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
7
19
 
8
- const article1 = new Article("My First Post", "Hello world!");
9
- blog.addArticle(article1);
20
+ blog.startServer(8080);
21
+ ```
22
+
23
+ Now you can start to add articles to your blog via your webbrowser on `http://localhost:8080`.
24
+
25
+ ## More Features
26
+
27
+ set an API to fetch data from an external database
28
+
29
+ ```
30
+ blog.setAPI("http://localhost:8081/blog")
31
+ ```
32
+
33
+ ### run api server with sqlite database
34
+
35
+ ```
36
+ import { Blog, storageserver} from "@lexho111/plainblog";
37
+ await storageserver("sqlite", 8081); // you can use a postgres db too
38
+ const blog = new Blog();
39
+ blog.setAPI(storageserver.getAPIURL());
40
+ blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
41
+
42
+ blog.startServer(8080);
43
+ ```
44
+
45
+ ### run api server with postgres database
46
+
47
+ ```
48
+ docker run -p 5432:5432 --name postgresdb --restart always -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=blog -v db_data:/var/lib/postgresql/data -d postgres:13
49
+ ```
50
+
51
+ ```
52
+ import { Blog, storageserver} from "@lexho111/plainblog";
53
+ await storageserver("postgres", 8081); // you can use a postgres db too
54
+ const blog = new Blog();
55
+ blog.setAPI(storageserver.getAPIURL());
56
+ blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
10
57
 
11
- const article2 = new Article(
12
- "Good Evening!",
13
- "Good Evening, Ladies and Gentleman!"
14
- );
15
- blog.addArticle(article2);
16
58
  blog.startServer(8080);
17
59
  ```
18
60
 
61
+ save data to file
62
+
19
63
  ```
20
64
  # save your blog to 'myblog.json'
21
65
  await blog.save("myblog.json");
@@ -23,3 +67,9 @@ await blog.save("myblog.json");
23
67
  # load data from 'myblog.json'
24
68
  await blog.load("myblog.json");
25
69
  ```
70
+
71
+ print your blog articles in markdown
72
+
73
+ ```
74
+ blog.print()
75
+ ```
package/api-server.js ADDED
@@ -0,0 +1,162 @@
1
+ import http from "http";
2
+ import { Sequelize, DataTypes } from "sequelize";
3
+
4
+ // This is a simple API server that provides blog data.
5
+ // This is the data that our API will serve, similar to a myblog.json file.
6
+ const blogData = {
7
+ title: "My Blog from the API",
8
+ articles: [
9
+ {
10
+ title: "First API Post",
11
+ content: "This article was loaded from our new external API server!",
12
+ },
13
+ {
14
+ title: "REST is Cool",
15
+ content:
16
+ "Using a REST API allows us to separate our frontend from our backend.",
17
+ },
18
+ ],
19
+ };
20
+
21
+ let username;
22
+ let password;
23
+ let host;
24
+ let dbport = 5432;
25
+
26
+ export function setUsername(username1) {
27
+ username = username1;
28
+ }
29
+ export function setPassword(password1) {
30
+ password = password1;
31
+ }
32
+ export function setHost(host1) {
33
+ host = host1;
34
+ }
35
+
36
+ let serverPort = 8081; // Default port
37
+
38
+ export async function start(databasetype, port = 8081) {
39
+ let sequelize;
40
+ serverPort = port; // Update the port for getAPIURL
41
+
42
+ if (databasetype === "sqlite") {
43
+ sequelize = new Sequelize({
44
+ dialect: "sqlite",
45
+ storage: "./blog.db",
46
+ logging: false,
47
+ });
48
+ } else if (databasetype === "postgres") {
49
+ if (!username || !password || !host) {
50
+ throw new Error(
51
+ "PostgreSQL credentials not set. Please use setUsername(), setPassword(), and setHost() before starting the server."
52
+ );
53
+ }
54
+ sequelize = new Sequelize(
55
+ `postgres://${username}:${password}@${host}:${dbport}/blog`,
56
+ {
57
+ logging: false,
58
+ }
59
+ ); // Connect to local Docker container
60
+ } else {
61
+ console.error(
62
+ `Invalid or no database type specified. Received: "${databasetype}". Exiting.`
63
+ );
64
+ process.exit(1);
65
+ }
66
+
67
+ // Define the Article model using the single sequelize instance
68
+ const Article = sequelize.define(
69
+ "Article",
70
+ {
71
+ title: DataTypes.STRING,
72
+ content: DataTypes.STRING,
73
+ },
74
+ {
75
+ timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
76
+ }
77
+ );
78
+
79
+ try {
80
+ // Sync all models with the database (creates tables if they don't exist)
81
+ await sequelize.sync();
82
+ console.log("Database synchronized.");
83
+ // Optionally, populate initial data if the database is empty
84
+ if ((await Article.count()) === 0) {
85
+ for (const articleData of blogData.articles) {
86
+ await Article.create(articleData);
87
+ }
88
+ console.log("Initial blog data populated.");
89
+ }
90
+
91
+ const server = http.createServer(async (req, res) => {
92
+ // Set CORS headers for all responses
93
+ res.setHeader("Access-Control-Allow-Origin", "*");
94
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
95
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
96
+
97
+ // Handle preflight CORS requests
98
+ if (req.method === "OPTIONS") {
99
+ res.writeHead(204);
100
+ res.end();
101
+ return;
102
+ }
103
+
104
+ // Health check endpoint
105
+ if (req.method === "GET" && req.url === "/health") {
106
+ res.writeHead(200, { "Content-Type": "application/json" });
107
+ res.end(JSON.stringify({ status: "ok" }));
108
+ return;
109
+ }
110
+
111
+ // GET all blog data
112
+ if (req.method === "GET" && req.url === "/blog") {
113
+ res.writeHead(200, { "Content-Type": "application/json" });
114
+ const dbArticles = await Article.findAll();
115
+ const responseData = {
116
+ title: blogData.title, // Keep the title from the original constant
117
+ articles: dbArticles,
118
+ };
119
+ res.end(JSON.stringify(responseData));
120
+ }
121
+ // POST a new article
122
+ else if (req.method === "POST" && req.url === "/blog/articles") {
123
+ let body = "";
124
+ req.on("data", (chunk) => (body += chunk.toString()));
125
+ req.on("end", async () => {
126
+ const newArticle = JSON.parse(body);
127
+ await Article.create(newArticle);
128
+ console.log("Added new article:", newArticle);
129
+ res.writeHead(201, { "Content-Type": "application/json" });
130
+ res.end(JSON.stringify(newArticle));
131
+ });
132
+ } else {
133
+ res.writeHead(404, { "Content-Type": "application/json" });
134
+ res.end(JSON.stringify({ message: "Not Found" }));
135
+ }
136
+ });
137
+
138
+ return new Promise((resolve) => {
139
+ server.listen(port, () => {
140
+ console.log(`API server running at http://localhost:${port}/`);
141
+ resolve(server);
142
+ });
143
+ });
144
+ } catch (error) {
145
+ console.error("Failed to initialize database or start server:", error);
146
+ process.exit(1); // Exit the process if there's a critical error
147
+ }
148
+ }
149
+
150
+ function getAPIURL() {
151
+ return `http://localhost:${serverPort}/blog`;
152
+ }
153
+
154
+ const storageserver = {
155
+ start,
156
+ getAPIURL,
157
+ setUsername,
158
+ setPassword,
159
+ setHost,
160
+ };
161
+
162
+ export default storageserver;
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Blog from "./Blog.js";
2
2
  import Article from "./Article.js";
3
+ import storageserver from "./api-server.js";
3
4
 
4
- export { Blog, Article };
5
+ export { Blog, Article, storageserver };
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "files": [
8
- "index.js",
9
- "Blog.js",
10
- "Article.js"
11
- ],
12
7
  "scripts": {
13
- "test": "echo \"Error: no test specified\" && exit 1"
8
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
14
9
  },
15
10
  "keywords": [
16
11
  "blog",
@@ -18,5 +13,15 @@
18
13
  "server"
19
14
  ],
20
15
  "author": "lexho111",
21
- "license": "ISC"
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "node-fetch": "^3.3.2",
19
+ "pg": "^8.16.3",
20
+ "pg-hstore": "^2.3.4",
21
+ "sequelize": "^6.37.7",
22
+ "sqlite3": "^5.1.7"
23
+ },
24
+ "devDependencies": {
25
+ "jest": "^30.2.0"
26
+ }
22
27
  }
@@ -0,0 +1,31 @@
1
+ import Article from "../Article.js";
2
+
3
+ test("get content", () => {
4
+ const article = new Article("title", "text text text");
5
+ const content = article.getContent();
6
+ expect(article.title).toContain("title");
7
+ expect(content).toContain("text text text");
8
+ });
9
+
10
+ test("get shortened content", () => {
11
+ const article = new Article("title", "text text text");
12
+ const content = article.getContentShort();
13
+ expect(article.title).toContain("title");
14
+ const text_short = "text text text";
15
+ expect(content).toContain(text_short);
16
+ expect(content).not.toContain("...");
17
+ });
18
+
19
+ test("get shortened content", () => {
20
+ let text = "text";
21
+ for (let i = 0; i < 500; i++) {
22
+ text += " ";
23
+ text += "text";
24
+ }
25
+ const text_long = text;
26
+ const article = new Article("title", text_long);
27
+ const content = article.getContentShort();
28
+ expect(article.title).toContain("title");
29
+ expect(content).toContain("text text text");
30
+ expect(content).toContain("...");
31
+ });
@@ -0,0 +1,97 @@
1
+ import Blog from "../Blog.js";
2
+ import Article from "../Article.js";
3
+ import { jest } from "@jest/globals";
4
+
5
+ describe("test blog", () => {
6
+ test("is valid html", async () => {
7
+ const myblog = new Blog();
8
+ const html = await myblog.toHTML();
9
+ expect(html).toContain("html>");
10
+ expect(html).toContain("</html>");
11
+ expect(html).toContain("<body");
12
+ expect(html).toContain("</body>");
13
+ expect(html).toContain("<div"); // container
14
+ expect(html).toContain("</div>");
15
+ });
16
+ test("creates Blog with specified title", async () => {
17
+ const titles = ["TestBlog", "Blog1234", "abcdfg", "kiwi"];
18
+ for (const title of titles) {
19
+ const myblog = new Blog();
20
+ myblog.setTitle(title);
21
+ const html = await myblog.toHTML();
22
+ expect(html).toContain(title);
23
+ }
24
+ });
25
+ test("empty blog without any article", async () => {
26
+ const myblog = new Blog();
27
+ const html = await myblog.toHTML();
28
+ expect(html).not.toContain("<article");
29
+ });
30
+ test("add article", async () => {
31
+ const myblog = new Blog();
32
+ const article = new Article("", "");
33
+ myblog.addArticle(article);
34
+ const html = await myblog.toHTML();
35
+ expect(html).toContain("<article");
36
+ expect(myblog.articles.length).toBe(1);
37
+ });
38
+ test("add articles", async () => {
39
+ const myblog = new Blog();
40
+ expect(myblog.articles.length).toBe(0);
41
+ const size = 10;
42
+ for (let i = 1; i <= size; i++) {
43
+ const article = new Article("", "");
44
+ myblog.addArticle(article);
45
+ expect(myblog.articles.length).toBe(i);
46
+ }
47
+ const html = await myblog.toHTML();
48
+ expect(html).toContain("<article");
49
+ expect(myblog.articles.length).toBe(size);
50
+ });
51
+ test("set style", async () => {
52
+ const myblog = new Blog();
53
+ const styles = [
54
+ "body { font-family: Courier; }",
55
+ "body { background-color: black; color: white; }",
56
+ "body { font-size: 1.2em; }",
57
+ ];
58
+ for (const style of styles) {
59
+ myblog.setStyle(style);
60
+ const html = await myblog.toHTML();
61
+ expect(html).toContain(style);
62
+ }
63
+ });
64
+ test("print method logs title and articles to console", () => {
65
+ // Arrange: Set up the blog with a title and articles
66
+ const myblog = new Blog();
67
+ myblog.setTitle("My Test Blog");
68
+ myblog.addArticle(new Article("Article 1", "Content 1"));
69
+
70
+ // Spy on console.log to capture its output without printing to the test runner
71
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {});
72
+
73
+ // Act: Call the method we want to test
74
+ myblog.print();
75
+
76
+ // Assert: Check if console.log was called with the correct strings
77
+ expect(consoleSpy).toHaveBeenCalledWith("# My Test Blog");
78
+ expect(consoleSpy).toHaveBeenCalledWith("## Article 1");
79
+ expect(consoleSpy).toHaveBeenCalledWith("Content 1");
80
+
81
+ // Clean up the spy to restore the original console.log
82
+ consoleSpy.mockRestore();
83
+ });
84
+ });
85
+
86
+ /*
87
+ test Blog.js
88
+ ok - new Blog()
89
+ ok - set title
90
+ ok - set style
91
+ ok - add article
92
+ - set api
93
+ - fetch/post
94
+ - save/load
95
+ ok - print
96
+ ok - toHTML
97
+ */
@@ -0,0 +1,27 @@
1
+ import { start as startAPIServer } from "../api-server.js";
2
+ import fetch from "node-fetch";
3
+
4
+ describe("API Server Health Check", () => {
5
+ let server;
6
+ const PORT = 8085;
7
+ const apiBaseUrl = `http://localhost:${PORT}`;
8
+
9
+ beforeAll(async () => {
10
+ // how about to use in-memory database?
11
+ server = await startAPIServer("sqlite", PORT);
12
+ });
13
+
14
+ afterAll((done) => {
15
+ server.close(done);
16
+ });
17
+
18
+ test("should respond with 200 OK on the /health endpoint", async () => {
19
+ const response = await fetch(`${apiBaseUrl}/health`);
20
+
21
+ // Check for a successful status code
22
+ expect(response.status).toBe(200);
23
+
24
+ const body = await response.json();
25
+ expect(body).toEqual({ status: "ok" });
26
+ });
27
+ });