Compare commits

..

136 Commits

Author SHA1 Message Date
Nullifier
9be261d415 Japanese locale typo fixed 2026-01-18 09:41:50 +00:00
Nullifier
9398919711 Japanese Locale - By Nullifier 2026-01-17 21:49:42 +00:00
545a848479 Stupid spelling error 2026-01-17 14:52:30 +00:00
54ffac931d Added english Australian, apparently there's no differences with GB and
AU
2026-01-17 12:11:04 +00:00
18b842e48c Merge pull request 'add(locale): sv-SE' (#3) from pickzelle/blogger-nodejs:master into master
Reviewed-on: #3
2026-01-16 23:23:52 +01:00
Pickzelle
c7bc64e59e add(locale): sv-SE 2026-01-16 23:19:02 +01:00
9383bd8058 Template .jsonc for locales 2026-01-16 21:25:47 +00:00
d554fce402 Added translated_by key to the locales 2026-01-16 21:15:11 +00:00
d17fcf2dd2 Merge pull request 'fix: these don't need to be executable' (#2) from javalsai/blogger-nodejs:fix-permissions into master
Reviewed-on: #2
2026-01-16 22:12:25 +01:00
a1fa0e3dbc Merge pull request 'add(locale): es-ES' (#1) from javalsai/blogger-nodejs:locale-es into master
Reviewed-on: #1
2026-01-16 22:12:17 +01:00
f44a15ed41 fix: these don't need to be executable 2026-01-16 21:12:58 +01:00
ab1fa5e69a add(locale): es-ES 2026-01-16 21:10:48 +01:00
be75382ead Add start up checks 2025-12-19 17:31:28 +00:00
3d58c5b244 Some minor changes to data handling error messages
and fixed a issue occuring in the forms routs that used the old
parameters for data.getdata
2025-11-30 17:04:15 +00:00
54b6f018cf Updated the data reading to have a data request limit, also updated docs
on new configuration options
2025-11-27 11:44:27 +00:00
7d38752f34 Changed how comments are stored and how data is retrieved 2025-11-27 11:34:12 +00:00
ef8711b0e1 Added a pipe | between the edit and RSS/ATOM buttons to make them more
seperated
2025-11-05 10:47:11 +00:00
c3de930b50 Split the page-header and site-header into seperate sections 2025-11-05 10:34:41 +00:00
1d1e4d863e Untracked robots.txt 2025-10-25 11:32:28 +01:00
3b47701c18 Made some divs consistent, the header was a div type with id='header'
and the footer was a footer type with no id, so now they are both both.
Also added some margins at the top and bottom of the page in CSS
2025-10-25 11:31:08 +01:00
deadvey
ddf9fcae13 Added icons for RSS and ATOM to make it look nicer 2025-10-24 13:59:41 +01:00
deadvey
9f0eb13bb4 BAD BUG! 2025-10-24 13:10:53 +01:00
deadvey
2d33ce79a8 Fixed a bug where comments could be submitted without any content
and where the hitcount was incremented before the program checked if
the post existed
2025-10-24 13:05:31 +01:00
deadvey
4ad7352fcc Updated some EJS and CSS to make it look a bit nicer (I am hopeless at
UX)
2025-10-24 12:48:54 +01:00
deadvey
f8f05221b2 removed custom.css from tracking 2025-10-24 12:29:57 +01:00
deadvey
35163b5584 Fixed an issue with an incomplete example-config.json that made the
server not work at all.
Also added a default.css file
2025-10-24 12:28:22 +01:00
66423cb3c0 There is now a Makefile because I learnt make syntax 2025-10-19 21:19:13 +01:00
cb7dcde7c5 Made tags case insensitive 2025-10-09 15:13:49 +01:00
553f126f2a Changed how comments are ided and classed 2025-10-09 14:43:48 +01:00
6f9e7aee13 small change in comment ids 2025-10-09 14:38:37 +01:00
23add8897b divs to spans because correct html 2025-10-09 14:30:48 +01:00
84a34d94f3 removed newlines from comments ejs 2025-10-09 14:28:51 +01:00
783f386dfe tiny fix 2025-10-09 14:19:16 +01:00
e63fb4a27a updated ejs 2025-10-09 14:18:03 +01:00
2ada1d970f Created a per-post hitcount as well a writedata() function that can
write to a particular index or to a whole data type
2025-10-02 13:34:55 +01:00
17919e3078 README 2025-10-02 11:13:39 +01:00
179a4f83bc Bug fix, most recent post would not show in the post's permalink due to
an indexing bug, pretty simple fix, I'm just a moron.
2025-10-02 10:47:38 +01:00
d9d45ff6ea Tracking /webroot, contains custom.css and robots.txt, might have to add
favicon.ico too.
2025-10-02 10:43:44 +01:00
87fd97730e Escape potentially dangerous input in the search field 2025-10-01 17:45:31 +01:00
acca51da20 README 2025-10-01 10:51:07 +01:00
788672cbea Bug fix with the search page's EJS 2025-10-01 10:44:34 +01:00
521dbccc7e Basic search functionality on the frontpage, I want to add support for
more advanced searches like using boolean operators, but right now it's
pretty basic.
2025-10-01 10:40:36 +01:00
8ad8f01043 Added basic search functionality (no frontend for it yet) 2025-09-30 23:15:33 +01:00
d27330a3db README update
I removed and added some stuff to the todo list
2025-09-24 23:41:29 +01:00
996bf0018b bug fix
numbers should be parsed

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 21:21:48 +01:00
45af80f747 Updated how comments are searched for
commentID's aren't neccesarily = to the index, so instead of using it as an index,  I just use a for loop to find the matching comment.
I also added another form at the bottom of the timeline to trick bots

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 21:12:21 +01:00
23744f4000 Bug fix and Document fix
Removed the string object from config.json as it's now all in the locale.
and I fixed data.getdata() to return an error code if the index is out of bounds, it now returns a 1.

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:57:47 +01:00
9305559660 Documentation fix
String -> Boolean

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:39:42 +01:00
35e6b94ba1 Support for uncached data loading
So you don't have to restart the server, you can add "cache_data": false option to config.json to not cache data.
Documented in CONFIG.md
I added a require_module function that either does or does not cache the data based on this configuration option.

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:37:35 +01:00
3f173fc2e3 bug fix
removed data/data.json from tracking

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:20:48 +01:00
b44762ba7c bug fix
Hitcount updates visually, previously it just showed 1 on the frontend as the value wasn't actually  being retrieved from the data.getdata()

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:18:05 +01:00
1ecc223433 small changes
Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 20:07:49 +01:00
22a7983737 data.getdata() is fully implemented
I think it all works now...

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 19:31:32 +01:00
e597fd78f7 Comment replies on comment page
There is now a reply form on the comment pages to reply to that comment
Syntax:
>> postID-commentID

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 17:33:25 +01:00
ef7178cc3f Comment submission works
I fixed the comment submission to use the new way of storing comments and their counter.
I also fixed the AI-consent field in en-US because I accidently had · instead of spaces (from when I copy pasted from vim)

Signed-off-by: deadvey <deadvey@deadvey.com>
2025-09-24 17:20:20 +01:00
max
bfaf957ae2 removed func.get_comment()
It is no longer needed

Signed-off-by: max <deadvey@localhost.localdomain>
2025-09-24 17:06:55 +01:00
max
e6476dcd4e Everything uses data.getdata()
data.getdata() excepts two parameters, if desired, the second will be the index, however if you don't specify anything, the whole array will be returned
Also, comments now have a composite key of postID-commentID, with each post's comments having their own set starting at 0, this makes it easier to index and find a specific comment, and making the getcomment() function unessesary
2025-09-24 17:06:13 +01:00
max
93c5f13750 Clean up changes
Seperated all routes into seperate files for neatness, and I've made the comments.json store only the comments, comments_counter is in the new data.json file which also stores the hitcount.

Signed-off-by: max <deadvey@localhost.localdomain>
2025-09-24 10:02:28 +01:00
max
0541b704db ExpressJS routes in different files
ExpressJS routes for the syndication stuff is now in a seperate file, will now do the other routes.

Signed-off-by: max <deadvey@localhost.localdomain>
2025-09-23 17:38:20 +01:00
max
5e4eb38763 started implementing data functions for easy requests to data 2025-09-23 15:01:28 +01:00
09967a0be9 Automatically removes leading and trailing spaces from tags because tags
are often entered with a space after each comma.
This uses .trim() in func.render_tags() to remove when rendering as well
as .map(str => str.trim()) when a post or post edit is submitted so they
are also stored without spaces.
2025-09-07 22:11:58 +01:00
e3e5469e1a Made it so if an invalid user or post is loaded, a proper error message is
shown instead of just a nodeJS error message
2025-09-06 23:54:49 +01:00
c8af978259 docs and also I improved the readability/user friendliness of the EJS 2025-09-03 22:28:50 +01:00
99e07389d0 * more proffesional language in rejecting AI
* footer uses locales
2025-08-27 19:00:49 +01:00
532010f873 bug fix for site_admin config field 2025-08-27 18:53:29 +01:00
5917789c5b add webadmin as a field in the config 2025-08-27 18:51:59 +01:00
4f0941262e Withdraw consent for AI scraping 2025-08-27 18:49:33 +01:00
27b9ee6437 * added "quotes" to the locales
* made all the ejs pages use "postID" as the variable for post indexes
* split up en-GB and en-US
2025-08-27 18:30:26 +01:00
c73ce69f93 bug fix: postID -> post_index 2025-08-27 18:19:12 +01:00
5bd0429ae2 timeline, userpage and tag page have an edit post option. 2025-08-27 17:58:48 +01:00
6e583d1410 removed webroot dir 2025-08-27 16:41:39 +01:00
cf784a1a99 removed webroot dir (in .gitignore) 2025-08-27 16:40:53 +01:00
b3ea048244 bug fix RSS and ATOM using func.render_md instead of showdown 2025-08-27 16:36:21 +01:00
9b5d3f3f73 Fixed issue relating to showdownjs not escaping html tags by porting to
markdown-it, also introduced a new function: func.render_md
2025-08-27 15:09:57 +01:00
5f07db1e15 tables in CONFIG.md 2025-08-09 21:10:23 +01:00
d298717519 Testing markdown tables 2025-08-09 21:05:58 +01:00
0b25fb221b Documentation 2025-08-09 20:53:57 +01:00
f85c4aa893 Minor change to /index/pages EJS 2025-08-09 16:59:57 +01:00
6fc1f85e18 Added locale (english only at the moment) and modifed the EJS so I think
every string is customisable (via the /locales/selected locale)
2025-08-09 16:57:31 +01:00
8418318d80 Added fake form at the top of html in attempt to mitigate spam, probably
wont work that well but might as well try
2025-08-09 13:18:44 +01:00
44a060508b Hitcount is now created by init.initialise() 2025-08-09 13:00:07 +01:00
49c7fc7cdf init.initialise() checks for correct path of config.json (bug fix) 2025-08-09 12:51:43 +01:00
144c276bc9 Fixed init.initialise() checking for .js files as opposed to .json files 2025-08-09 12:48:25 +01:00
df4bc99d9a Initialise() imports fs (bug fix) 2025-08-09 12:44:39 +01:00
3e2a63bfd7 Export the init.initialise() function (bug fix) 2025-08-09 12:42:18 +01:00
bced9c7c0e CONFIG.md documentation and also fixed a bug where when ATOM files are
loaded the config.rss boolean is actually checked as opposed to
config.atom, fixed by also adding string.atom_disabled to config.json :)
2025-08-02 03:00:20 +01:00
f723e37732 docs and that 2025-08-02 02:48:22 +01:00
8b9ddcf048 Added page indexes for comments, posts, users and pages overall, should
add one for tags but it might be inefficient as I don't store all tags
in an array or anything...
2025-08-02 00:51:33 +01:00
5f2aba0c2b Site wide header (currently only used for a link to / but will add
index's for stuff later)
2025-08-01 23:27:28 +01:00
cdfc5f2c30 Post doesn't exist page 2025-08-01 23:21:43 +01:00
88b198365d user, post and comment objects contain their ID's now. 2025-08-01 12:34:29 +01:00
b683b658f7 Comments now have their own pages, at /comment/commentID, these are
linked to when someone replies to another comment (>> id), I also fixed
a bug in comment submission where the counter was not incrementing
2025-07-31 03:58:28 +01:00
0cc319a702 Added user specific RSS and ATOM feeds and updated the EJS templates to
add them by default to the user's header section
2025-07-30 01:28:23 +01:00
72316094e4 Updated the readme to have a better feature section 2025-07-30 01:12:29 +01:00
47877e71d4 atom functionality is added and I fixed an issue in the RSS that is
caused by the deleted posts not having valid data.
2025-07-30 01:09:51 +01:00
0c43c7315c editing user redirects to user's page and began to implment ATOM 2025-07-30 00:43:21 +01:00
39eba8fcda posts are marked as deleted to preserve array structure 2025-07-28 15:11:26 +01:00
38d82f9e1a Readme formatting stuff 2025-07-23 02:55:08 +01:00
bb62ccf25f Readme 2025-07-23 02:54:24 +01:00
40e0cc80a3 re-added markdown support under EJS 2025-07-23 02:49:05 +01:00
306adf3943 rss in ejs 2025-07-23 02:38:03 +01:00
ff1d34a7ab put package.json in / again 2025-07-23 02:22:13 +01:00
797d894621 lots of fixes and more EJS 2025-07-23 02:20:38 +01:00
929151a16d Update CONFIG.md
removed stuff about format indicators
2025-07-22 23:40:19 +02:00
ce93a1991b nfs removal 2025-07-22 00:18:23 +01:00
cc131798a3 ejs stuff 2025-07-22 00:17:00 +01:00
7eeafddae4 bloody markdown formatting >:( 2025-07-21 00:09:51 +01:00
fc3a68e476 formatting 2025-07-21 00:03:20 +01:00
590d675075 config docs 2025-07-20 23:59:30 +01:00
d54b682267 small readme edit 2025-07-21 00:06:30 +02:00
2dfed40665 CONFIG.md 2025-07-20 23:05:53 +01:00
3e745e6842 json 2025-07-20 22:47:07 +01:00
74f2e4eaec ever growing todo list 2025-07-15 03:14:46 +01:00
636a460918 untrack hitcount.txt 2025-07-15 02:11:26 +01:00
b5ab7af515 delete account 2025-07-15 02:10:26 +01:00
9a1bc97b99 format indiactors 2025-07-15 01:29:38 +01:00
09ab903815 Sign up link 2025-07-15 01:28:30 +01:00
0b70624e05 comments 2025-07-13 02:52:12 +01:00
23685d02c0 fixed user importing error 2025-07-13 02:49:55 +01:00
3efe4b7836 headerLevelStart (for showdownjs) and some comments 2025-07-13 02:48:15 +01:00
4fb12c54f8 did some commenting (I barely commented anything) 2025-07-13 02:01:09 +01:00
3a821b5eba don't freak if posts.post = [] 2025-07-13 00:51:01 +01:00
d75a74b7a8 fixed copy error bug 2025-07-13 00:41:59 +01:00
1ea18208f0 removed dependency check 2025-07-13 00:34:43 +01:00
ebcc61a9e7 argument error 2025-07-13 00:32:45 +01:00
17b7ddf133 dependency check 2025-07-13 00:30:13 +01:00
6a3b2da013 untracked config file 2025-07-13 00:26:43 +01:00
932dfbc00b untracked config file 2025-07-13 00:25:46 +01:00
60fc1b90ca exit after initialisation 2025-07-13 00:25:19 +01:00
f2f244eb9d better first time xp 2025-07-13 00:21:53 +01:00
bcaa3487dd commenting 2025-07-12 22:28:36 +01:00
b241a80963 dependencies 2025-07-11 12:59:53 +01:00
b538d495db better markdown 2025-07-11 12:09:09 +01:00
78b836bef5 markdown support 2025-07-11 02:58:13 +01:00
b2c649d001 sanitise input (can't believe I just remembered this) 2025-07-11 02:11:25 +01:00
68068adfa3 hitcount 2025-07-10 20:32:30 +01:00
63 changed files with 2303 additions and 386 deletions

13
.gitignore vendored Executable file → Normal file
View File

@@ -1,6 +1,13 @@
package.json
node_modules
package-lock.json
posts.js
users.js
posts.json
comments.json
users.json
config.json
data.json
hitcount.txt
*.swp
data
images/*
webroot/custom.css
webroot/robots.txt

41
Makefile Normal file
View File

@@ -0,0 +1,41 @@
DATA_DIR=data
WEBROOT_DIR=webroot
all: config css users posts comments data
clean:
rm -rf data
rm -f webroot/custom.css
rm -f config.json
# config file
config: config.json
config.json:
cp example-config.json config.json
echo '!!!PLEASE MODIFY config.json ACCORDING TO YOUR NEEDS!!!'
# custom.css
css: $(WEBROOT_DIR)/custom.css
$(WEBROOT_DIR)/custom.css:
mkdir -p webroot
echo '* { font-family: sans-serif; }' > $(WEBROOT_DIR)/custom.css
# users.json
users: $(DATA_DIR)/users.json
$(DATA_DIR)/users.json:
mkdir -p data
echo '[]' > $(DATA_DIR)/users.json
# posts.json
posts: $(DATA_DIR)/posts.json
$(DATA_DIR)/posts.json:
mkdir -p data
echo '[]' > $(DATA_DIR)/posts.json
# comments.json
comments: $(DATA_DIR)/comments.json
$(DATA_DIR)/comments.json:
mkdir -p data
echo '[]' > $(DATA_DIR)/comments.json
# data.json
data: $(DATA_DIR)/data.json
$(DATA_DIR)/data.json:
mkdir -p data
echo '{"hitcount": 0}' > $(DATA_DIR)/data.json

View File

@@ -1,16 +1,54 @@
This is a blogging site written in nodejs, all pages are served directly by the nodejs backend.<br/>
In action on my website: [deadvey.com](https://deadvey.com)<br/>
This software aims to provide a lot of power to the web admin who is running the blog site.<br/>
Customisation is unlimited with a bit of knowledge of EJS and CSS, you can edit the entire formatting of the pages, making the site truly yours!<br/>
This software also aims to be compatible with text based browsers and as a result contains no client side Javascript, if you're looking for a more<br/>
beautiful and featureful blogging frontend, this isn't for you.<br/>
The AI robots.txt file is from [ai.robots.txt (MIT)](https://github.com/ai-robots-txt/ai.robots.txt)<br/>
# features
* post creation via the web frontend (no need to remote to your server to make a post)
> [!CAUTION]
> This software is not finished yet, so it's very buggy and probably really insecure<br/>
> use at your own risk!<br/>
See the software in action: [deadvey.com](https://deadvey.com)<br/>
# Installation and Running:
Read the [installation guide](/docs/INSTALLATION.md)
# Confiuration:
Read the [configuation guide](docs/CONFIG.md) for configuration help (in config.json)
# Features:
* post creation, modification and deletion via frontend
* user creation, modification and deletion via frontend
* multi user
* powerful customisation
* rss
* timeline, user page, post page and tag specific page
* edit/delete posts
* powerful customisation via EJS
* Configuration via config.json
* site wide and user specific rss, atom
* Markdown syntax in posts
* Commenting on posts and replying to other comments
* site wide custom CSS and strings
* Search functionality
* Page indexes
# Bugs:
* probably scales like shit
* probably insecure as hell
# planned features
* atom
* federation
* sign up
# Planned features/todo list:
* federation (looks tricky)
* inline comments and docs
* clean up code a bit
* /postID and /userID pages
* Make EJS modification more user friendly (half done)
* API for returning posts, users, comments, tags other?...
* Moderation tools including a keyword blacklist
* Request account function? Not sure how this should be implemented.
* optional SQL
* initialisation has prompts for setup process.
# Docs:
See [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md)
# Customisation:
Customisation of settings can be done via the config.json file (use example-config.json as an example) and see [the configuration guide](docs/CONFIG.md)<br/>
Additionaly, more complex configuration of the precise template of the whole site, can be done via [EJS](https://ejs.co/) (in /views) (see [the list of things variables and functions available in EJS](docs/EJS.md) (you will need to understand EJS syntax and JavaScript, to customise this (why did I use EJS? well I originally had this weird system of format indicators with percent (%) signs and stuff (like in unix's date (`date`)) but then I was told EJS is better and it sure is, though it is a bit harder to understand but MUCH more powerful!))
Also, if you want to change any of the strings on the website, please modify or create a new, customised locale in /locales

246
app.js
View File

@@ -1,246 +0,0 @@
const express = require('express');
const crypto = require('crypto'); // For encrypting passwords
const { fromUnixTime, format, getUnixTime } = require("date-fns")
const fs = require('fs');
const users = require('./users.js');
const posts = require('./posts.js');
const config = require('./config.js');
const app = express();
const port = 8080;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static(config.root_path));
function get_userID(username) {
for (let i = 0; i < users.users.length; i++) {
if (users.users[i]['username'] == username) {
return i
}
}
return -1
}
function unix_time_to_date_format(unix_time) {
date = fromUnixTime(unix_time)
formatted_date = format(date, config.date_format)
return formatted_date
}
function unix_time_to_rss_date(unix_time) {
date = fromUnixTime(unix_time)
formatted_date = format(date, "EEE, dd MMM yyyy HH:mm:ss")
return `${formatted_date} ${config.time_zone}`
}
function hyperlink_tags(tags) {
string = ""
for (let tag_index = 0; tag_index < tags.length; tag_index++) {
string += `<a href="/tag/${tags[tag_index]}">${tags[tag_index]}</a>`
if (tag_index < tags.length - 1) {
string += ", ";
}
}
return string
}
function replace_format_indicators(input_string, post_index=0, tag_name="tag") {
post_object = posts.posts[post_index]
output_string = input_string
.replaceAll("%%", "&#37;")
.replaceAll("%A", (post_object["tags"]))
.replaceAll("%B", (hyperlink_tags(post_object["tags"])))
.replaceAll("%C", post_object["content"].replaceAll("\n","<br/>"))
.replaceAll("%D", unix_time_to_date_format(post_object["pubdate"]))
.replaceAll("%E", unix_time_to_date_format(post_object["editdate"]))
.replaceAll("%F", users.users[post_object["userID"]]['prettyname'])
.replaceAll("%G", tag_name)
.replaceAll("%I", users.users[post_object['userID']]['description'])
.replaceAll("%L", `/post/${post_index}`)
.replaceAll("%N", users.users[post_object["userID"]]['username'])
.replaceAll("%P", "/post")
.replaceAll("%O", `/edit/${post_index}`)
.replaceAll("%R", "/rss")
.replaceAll("%S", config.seperator)
.replaceAll("%T", post_object["title"])
.replaceAll("%U", `/user/${users.users[post_object["userID"]]['username']}`)
.replaceAll("%Y", config.site_name)
.replaceAll("%W", config.site_description)
return output_string
}
app.get(config.rss_path, (req,res) => {
if (config.rss == false) {
res.send("Sorry, RSS is disabled!")
}
else {
let rss_content = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${config.site_name}</title>
<link>${config.site_url}</link>
<description>${config.site_description}</description>
`
for (let i = posts.posts.length-1; i >= 0; i--) {
rss_content += `
<item>
<title>${posts.posts[i]["title"]}</title>
<link>${config.site_url}/post/${i}.${config.file_extension}</link>
<description><![CDATA[${posts.posts[i]["content"].replaceAll("\n","<br/>")}]]></description>
<guid isPermaLink="true">${config.site_url}/post/${i}</guid>
<pubDate>${unix_time_to_rss_date(posts.posts[i]['pubdate'])}</pubDate>`
for (let j = 0; j < posts.posts[i]['tags'].length; j++) {
rss_content += `<category><![CDATA[${posts.posts[i]['tags'][j]}]]></category>`
};
rss_content += "</item>"
}
rss_content += `
</channel>
</rss>`
res.setHeader('content-type', 'application/rss+xml');
res.send(rss_content)
};
});
app.get("/", (req,res) => {
header_div = config.timeline_header
header_div = replace_format_indicators(header_div);
posts_div = "";
counter = posts.posts.length - 1;
while ((counter >= 0) && (counter > (posts.posts.length - (config.timeline_length + 1))))
{
let post = config.timeline_post_format;
posts_div += replace_format_indicators(post, counter);
counter -= 1;
}
res.send(`<html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body><div id="header">${header_div}</div><div id="posts">${posts_div}</div></body></html>`);
});
app.get("/post", (req,res) => {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><form action="/submit_post" method="POST" onsubmit="sha512password()">
<label>Username: </label><input required name="username"><br/>
<label>Password: </label><input type="password" required id="password" name="password"><br/>
<label>Title: </label><input required name="title"><br/>
<label>Content: </label><textarea required name="content"></textarea><br/>
<label>Tags (comma seperated): </label><input name="tags"><br/>
<input type="submit" value="Submit">
</form></html>`);
});
app.get("/edit/:post_id", (req,res) => {
const post_id = req.params.post_id
const post = posts.posts[post_id]
const user = users.users[post['userID']]
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head>
<form action="/submit_edit" method="POST" onsubmit="sha512password()">
<input name="userID" type="hidden" value="${post['userID']}">
<input name="postID" type="hidden" value="${post_id}">
<label>${user.prettyname}'s Password: </label><input type="password" required id="password" name="password"><br/>
<label>Title: </label><input value="${post['title']}" required name="title"><br/>
<label>Content: </label>
<textarea required name="content">${post['content']
.replaceAll('"', "&#34;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\\", "&#92;")}</textarea><br/>
<label>Tags (comma seperated): </label><input value="${post['tags']}" name="tags"><br/>
<label>Delete forever (no undo): </label><input name="delete" type="checkbox"><br/>
<input type="submit" value="Submit">
</form></html>`);
});
app.get("/user/:username", (req, res) => {
header_div = config.user_page_header
header_div = replace_format_indicators(header_div)
posts_div = "";
for (let post_index = posts.posts.length-1; post_index >= 0; post_index--) {
if (users.users[posts.posts[post_index]["userID"]]["username"] == req.params.username) {
let post = config.user_post_format;
posts_div += replace_format_indicators(post, post_index);
}
}
res.send(`<html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body><div id="header">${header_div}</div><div id="posts">${posts_div}</div></body></html>`);
});
app.get("/post/:post_index", (req, res) => {
post_div = "";
let post = config.post_page_format;
post_div += replace_format_indicators(post, req.params.post_index);
res.send(`<html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body><div id="posts">${post_div}</div></body></html>`);
});
app.get("/tag/:tag", (req,res) => {
const tag = req.params.tag
let header_div = config.tag_page_header
header_div = replace_format_indicators(header_div,0,tag)
let page_content = ""
for (let i = posts.posts.length-1; i >= 0; i--) {
if (posts.posts[i]['tags'].includes(tag)) {
let post = config.tag_post_format;
page_content += replace_format_indicators(post, i);
};
};
res.send(`<html><style>${config.css}</style><body><div id="header">${header_div}</div><div id="posts">${page_content}</div></body></html>`);
});
app.post("/submit_edit", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const postID = req.body.postID
const userID = req.body.userID
const title = req.body.title
const content = req.body.content
const tags = req.body.tags.split(',');
const delete_bool = req.body.delete
const unix_timestamp = getUnixTime(new Date())
console.log(users.users[userID]['prettyname'], "is editting the post titled:", title);
if (users.users[userID]['hash'] == password) { // password matches
let post = posts.posts[postID]
post['title'] = title
post['content'] = content
post['tags'] = tags
post['editdate'] = unix_timestamp
if (typeof delete_bool != "undefined") {
console.log("Deleting post!")
posts.posts.splice(postID,1)
}
fs.writeFileSync(`${__dirname}/posts.js`, `export const posts = ${JSON.stringify(posts.posts)}`, 'utf-8');
res.redirect(302, "/");
}
else {
res.send(`Invalid Password for user`,users.users[userID]['prettyname']);
}
});
app.post("/submit_post", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const username = req.body.username
const title = req.body.title
const content = req.body.content
const tags = req.body.tags.split(',');
const unix_timestamp = getUnixTime(new Date())
console.log(username, "is submitting a post titled:", title);
if (get_userID(username) == -1) {
res.send("User does not exist")
}
else if (users.users[get_userID(username)]['hash'] == password) { // Password matches
posts.posts.push({
"userID": get_userID(username),
"title": title,
"content": content,
"pubdate": unix_timestamp,
"editdate": unix_timestamp,
"tags": tags,
})
fs.writeFileSync(`${__dirname}/posts.js`, `export const posts = ${JSON.stringify(posts.posts)}`, 'utf-8');
res.redirect(302, "/");
}
else {
res.send(`Invalid Password for user`,username);
}
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port} in ${config.root_path}`);
});

125
config.js
View File

@@ -1,125 +0,0 @@
export const seperator = "<hr/>"
export const site_name = "Deadvey's Blog"
export const site_url = "https://deadvey.com"
export const site_description = "Films, tech, random shit"
export const timeline_length = 20
export const charset = "UTF-8" // Don't change unless you know why
// Anything in this directory will be in the webroot, so put favicon.ico and anything else here.
export const root_path = "/var/www/deadvey.com/blog"
//export const federation = true
//export const fediverse_url = "deadvey.com"
export const rss = true
export const rss_path = "/rss"
// https://date-fns.org/v4.1.0/docs/format
export const date_format = "yyyy-MM-dd"
export const time_zone = "+0000"
//// Format /////
// The syntax for this is pretty simple
// %% - A literal %
// %A - List of tags
// %B - List of tags, each one with a hyperlink to that tag page
// %C - Post content
// %D - Published date in the format specified by date_format
// %E - Edited date in the format specified by date_format
// %F - Pretty name
// %G - Tag name (used for the tag page only)
// %I - User description
// %L - URL Permanent link to the post
// %N - the username of the user (poster)
// %P - URL to create a new post
// %O - URL to edit this post
// %R - Site wide RSS feed
// %S - post seperator as defined by post_seperator
// %T - Title
// %U - URL the the user (poster)
// %Y - Site Name as defined by site_name
// %W - Site Description as defined by site_description
export const timeline_header = `<h1>%Y</h1>
<h2>%W</h2>
<a href="%P">Create Post</a><br/>
<a href="%R">RSS Feed</a><br/>
%S`
export const user_page_header = `<h1>%F's posts:</h1>
%I
%S`
export const tag_page_header = `<h1>Posts tagged: %G</h1>%S`
// ---------------------------------------------
export const user_post_format = `<h2>%T</h2>
<p>%C</p>
<i>%B</i><br/>
<a href="%L">Permalink</a><br/>
%S`
export const post_page_format = `<h1>%T</h1>
<p>%C</p>
<i>%B</i><br/>
<i>By <a href="%U">%N</a></i><br/>
<a href="%O">Edit Post</a><br/>
<i>Posted: %D</i><br/>
<i>Edited: %E</i>`
export const timeline_post_format = `<h3>%T</h3>
<p>%C</p>
<a href="%L">Permalink</a><br/>
<i>By <a href="%U">%N</a></i>
%S`
export const tag_post_format = `<h3>%T</h3>
<p>%C</p>
<i>%B</i><br/>
<a href="%L">Permalink</a><br/>
<i>By <a href="%U">%N</a></i>
%S`
/// Custom CSS to be applied to every page
export const css = `
@media (prefers-color-scheme: light) {
body {
background: #ebdbb2;
color: #282828;
}
code {
background: #bdae93;
}
a {
color: #076678;
text-decoration: none;
}
a:visited {
color: #8f3f71;
}
input,textarea,button {
background: #ebdbb2;
color: #282828;
border: 1px solid #282828;
}
}
@media (prefers-color-scheme: dark) {
body {
background: #282828;
color: #ebdbb2;
}
code {
background: #665c54;
box-decoration-break: clone;
display: block;
white-space: pre;
max-width: 50%;
min-width: 100px;
}
a {
color: #83a598;
text-decoration: none;
}
a:visited {
color: #d3869b;
}
input,textarea,button {
background: #282828;
color: #ebdbb2;
border: 1px solid #ebdbb2;
}
}
`

4
docs/CLASSES_AND_IDS.md Normal file
View File

@@ -0,0 +1,4 @@
Post:
![images/post-css.png](An image showing the css id's assosciated with each part of a post)

52
docs/CONFIG.md Normal file
View File

@@ -0,0 +1,52 @@
# Configuration Documentation
## Introduction
The configuration file is stored in a file called config.json, for an example, copy example-config.json to config.json (`cp example-config.json config.json`) and modify from there.<br/>
Currently all values in example-config.json are required, however I plan to add support for default values in the case of no value being set.<br/>
All options show an example configuartion value and the variable type + an explaination below it.<br/>
## Technical configuration
| name | example value | variable type | explanation |
|-----------------|----------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| site_url | "https://example.com" | String | This value defines the url of your site, this used for the RSS feed to link back to post. |
| port | 8080 | Integer | This value defines the port that you run the blog on. Don't change this value if you don't know what that means. |
| allow_signup | true | Boolean | Defines weather new people should be allowed to signup. |
| timeline_length | 20 | Integer | How many posts will be shown on the timeline (home page). |
| enable_hitcount | true | Boolean | Enabling the hitcount (a number that represents the amount of front page loads (stored in hitcount.txt)) can slightly slow down loading of the front page. |
| charset | "UTF-8" | String | This is the value in the <meta charset=""> tag in the html of all pages, you should not change this unless you know why. |
| root_path | "/path/to/root/of/website" | String | Anything in this directory will be in the webroot, so put favicon.ico and anything else here. |
| data_storage | "json" | String | JSON is currently the only supported format, but SQL is going to be added/is a work in progress |
| cache_data | true | Boolean | Not caching data means you can edit the posts, users, comments etc, maunally and not have to restart the server, however, for large instances this is not reccomended as it takes longer to load the required data. Note: config.json always needs a restart |
| request_data_limit | 20 | Integer | The maximum number of objects to return (latest), so if set to 20, then only the 20 most recent posts will ever show |
| root_path | '/var/www/blog_root' | String | Relative or Absolute path to the root directory of your static content, holds files such as favicon.ico, custom.css and robots.txt |
| locale | 'en_GB' | String | The locale to use which determines the language used and minor cultural differences |
## Basic Customisation
| name | example value | variable type | explanation |
|-----------------|----------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|locale|"en-US"|String|Your locale, see [/locales](/locales) for a list of all locales (you can open a PR for a new translation too)|
|seperator|"\<hr/\>"|String|By default, this will go inbetween posts and generally to seperate out content on pages.|
|site_name|"Pete's Blogging Site!"|String|It's the name of your blog site, a human readable string.|
|site_description|"Read my blogs!"|String|This is what %W represents; it's the description of your instance, a human readable string.|
|default_commenter_username|"Anon"|String|Default commenter username if no username is inputted in comment submission.|
## Syndication
| name | example value | variable type | explanation |
|------|---------------|---------------|-------------------------------|
| rss | true | Boolean | Enable or Disable RSS feeds. |
| atom | true | Boolean | Enable or Disable ATOM feeds. |
## Dates
Read more at [date-fns](https://date-fns.org/v4.1.0/docs/format)<br/>
| name | example value | variable type | explanation |
|------|---------------|---------------|-------------------------------|
|date_format|"yyyy-MM-dd"|String|The format of date's on the website.|
|time_zone|"+0000"|String|Your offset from UTC|
## Advanced Customisation
* /views/* files are EJS files (used for formatting HTML) and can be editted to your liking, you might want to read [the EJS docs](https://ejs.co/#docs) for help.
* "css": "body { background: red; }"<br/>
String. Custom CSS to be applied to all pages, if you want more complex css, you can edit custom.css.<br/>
You can also edit the custom.css file in the webroot, as by default this is linked in the global header.
* You can create a file called custom.css in the webroot and that will be loaded as a style onto every page.
## Custom Strings
* You can edit all the strings on the site in /locales/\<your-locale>.json

1
docs/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
Just open a PR or something, if it's good I'll pull

4
docs/DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,4 @@
All documentation is under construction as the program is also under construction and so is constantly changing and is also a mess and I'm shit at documentation
- [EJS Variables and functions](EJS.md)
- [configuring config.json](CONFIG.md)
- [Contributing](CONTRIBUTING.md)

27
docs/EJS.md Normal file
View File

@@ -0,0 +1,27 @@
This is not a guide on how EJS works, look at [EJS's website](https://ejs.co/) for that!<br/>
This is just a list of functions and pieces of information provided to each EJS file.<br/>
<br/>
# syndication/global_rss:
- All config.json data
- All posts from posts.json
- showdown.JS's converter functions
- All functions in functions.js
# syndication/user_rss:
- All config.json data
- All posts from posts.json
- showdown.JS's converter functions
- All functions in functions.js
- the userID of the user in question (integer)
# syndication/global_atom:
- All config.json data
- All posts from posts.json
- showdown.JS's converter functions
- All functions in functions.js
- getUnixTime function from date-fns
# syndication/user_atom:
- All config.json data
- All posts from posts.json
- showdown.JS's converter functions
- All functions in functions.js
- the userID of the user in question (integer)
- getUnixTime function from date-fns

10
docs/INSTALLATION.md Normal file
View File

@@ -0,0 +1,10 @@
# Installation
This program is currently just ran manually.<br/>
All you need to do is clone the git repository:<br/>
```git clone https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs.git```<br/>
Then run the Makefile:<br/>
```make```
Then you should modify config.json in / to suit your needs.<br/>
# Running
I would reccomend running the program in tmux so it does not stop running when you close the terminal window.<br/>
• There is currently no init system support. I might add this later (or you could open a PR).

BIN
docs/images/post-css.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

26
example-config.json Normal file
View File

@@ -0,0 +1,26 @@
{
"site_admin": "your name",
"seperator": "<hr/>",
"site_name": "My Blog",
"site_url": "https://example.com",
"locale": "en-US",
"port": 8080,
"data_storage": "json",
"cache_data": false,
"allow_signup": true,
"site_description": "Read my blogs!",
"request_data_limit": 20,
"enable_hitcount": true,
"charset": "UTF-8",
"root_path": "../webroot/",
"edit_account_base_url": "/edit_account",
"new_post_url": "/post",
"signup_url": "/signup",
"edit_post_base_url": "/edit",
"default_commenter_username": "Anon",
"rss": true,
"atom": true,
"date_format": "yyyy-MM-dd",
"time_zone": "+0000",
"css": ""
}

44
locales/en-AU.json Normal file
View File

@@ -0,0 +1,44 @@
{
"quotes": "“”‘’",
"password": "Password",
"username": "Username",
"prettyname": "Prettyname",
"description": "Description (social links, what you write about etc), supports markdown",
"title": "Title",
"post_content": "Post Content, supports markdown",
"tags": "Tags (comma seperated)",
"delete_account_confirmation": "Delete my account - (I agree that my account and all of my posts will be permanently deleted instantly)",
"signup_agreement": "I agree to not post illegal or hateful content",
"comment": "Comment",
"submit": "Submit",
"site_ran_by": "Site is ran by",
"signups_unavailable": "Sorry, this server does not allow signups",
"user_exists": "Sorry, this user already exists, try a different username",
"user_doesnt_exist": "Sorry, this user does not exist",
"comment_doesnt_exist": "This comment doesn't exist, this could be because the post it was attached to was deleted",
"post_doesnt_exist": "This post doesn't exist or was deleted",
"incorrect_password": "Incorrect Password",
"rss_disabled": "Sorry, RSS is disabled",
"atom_disabled": "Sorry, ATOM is disabled",
"AI_consent": "The content on this website may not be copied, scraped, or used to train AI models or large language models (LLMs) without prior written consent.",
"rss_feed": "RSS Feed",
"atom_feed": "ATOM Feed",
"no_tags": "No Tags",
"new_post": "New Post",
"edit_post": "Edit Post",
"sign_up": "Sign Up",
"edit_account": "Edit Account",
"permalink": "Permalink",
"written_by": "Written by",
"published": "Published",
"last_modified": "Last Modified",
"hitcount": "Hitcount",
"posts_tagged": "Posts Tagged",
"home_page": "Home Page",
"site_index": "Site Index",
"reply": "reply",
"attribution": "Powered by blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Source Code</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>license (WTFPL)</a>",
"translated_by": "DeaDvey"
}

44
locales/en-GB.json Normal file
View File

@@ -0,0 +1,44 @@
{
"quotes": "“”‘’",
"password": "Password",
"username": "Username",
"prettyname": "Prettyname",
"description": "Description (social links, what you write about etc), supports markdown",
"title": "Title",
"post_content": "Post Content, supports markdown",
"tags": "Tags (comma seperated)",
"delete_account_confirmation": "Delete my account - (I agree that my account and all of my posts will be permanently deleted instantly)",
"signup_agreement": "I agree to not post illegal or hateful content",
"comment": "Comment",
"submit": "Submit",
"site_ran_by": "Site is ran by",
"signups_unavailable": "Sorry, this server does not allow signups",
"user_exists": "Sorry, this user already exists, try a different username",
"user_doesnt_exist": "Sorry, this user does not exist",
"comment_doesnt_exist": "This comment doesn't exist, this could be because the post it was attached to was deleted",
"post_doesnt_exist": "This post doesn't exist or was deleted",
"incorrect_password": "Incorrect Password",
"rss_disabled": "Sorry, RSS is disabled",
"atom_disabled": "Sorry, ATOM is disabled",
"AI_consent": "The content on this website may not be copied, scraped, or used to train AI models or large language models (LLMs) without prior written consent.",
"rss_feed": "RSS Feed",
"atom_feed": "ATOM Feed",
"no_tags": "No Tags",
"new_post": "New Post",
"edit_post": "Edit Post",
"sign_up": "Sign Up",
"edit_account": "Edit Account",
"permalink": "Permalink",
"written_by": "Written by",
"published": "Published",
"last_modified": "Last Modified",
"hitcount": "Hitcount",
"posts_tagged": "Posts Tagged",
"home_page": "Home Page",
"site_index": "Site Index",
"reply": "reply",
"attribution": "Powered by blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Source Code</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>license (WTFPL)</a>",
"translated_by": "DeaDvey"
}

44
locales/en-US.json Normal file
View File

@@ -0,0 +1,44 @@
{
"quotes": "“”‘’",
"password": "Password",
"username": "Username",
"prettyname": "Prettyname",
"description": "Description (social links, what you write about etc), supports markdown",
"title": "Title",
"post_content": "Post Content, supports markdown",
"tags": "Tags (comma seperated)",
"delete_account_confirmation": "Delete my account - (I agree that my account and all of my posts will be permanently deleted instantly)",
"signup_agreement": "I agree to not post illegal or hateful content",
"comment": "Comment",
"submit": "Submit",
"site_ran_by": "Site is ran by",
"signups_unavailable": "Sorry, this server does not allow signups",
"user_exists": "Sorry, this user already exists, try a different username",
"user_doesnt_exist": "Sorry, this user does not exist",
"comment_doesnt_exist": "This comment doesn't exist, this could be because the post it was attached to was deleted",
"post_doesnt_exist": "This post doesn't exist or was deleted",
"incorrect_password": "Incorrect Password",
"rss_disabled": "Sorry, RSS is disabled",
"atom_disabled": "Sorry, ATOM is disabled",
"AI_consent": "The content on this website may not be copied, scraped, or used to train AI models or large language models (LLMs) without prior written consent.",
"rss_feed": "RSS Feed",
"atom_feed": "ATOM Feed",
"no_tags": "No Tags",
"new_post": "New Post",
"edit_post": "Edit Post",
"sign_up": "Sign Up",
"edit_account": "Edit Account",
"permalink": "Permalink",
"written_by": "Written by",
"published": "Published",
"last_modified": "Last Modified",
"hitcount": "Hitcount",
"posts_tagged": "Posts Tagged",
"home_page": "Home Page",
"site_index": "Site Index",
"reply": "reply",
"attribution": "Powered by blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Source Code</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>license (WTFPL)</a>",
"translated_by": "DeaDvey"
}

43
locales/es-ES.json Normal file
View File

@@ -0,0 +1,43 @@
{
"quotes": "«»‹›",
"password": "Contraseña",
"username": "Nombre de Usuario",
"prettyname": "Nombre Bonito",
"description": "Descripción (enlaces de redes, sobre que escribes, etc), soporta markdown",
"title": "Título",
"post_content": "Contenido de la Publicación, soporta markdown",
"tags": "Etiquetas (separados por coma)",
"delete_account_confirmation": "Eliminar mi cuenta - (Estoy de acuerdo con que todas mis publicaciones serán permanentemente eliminadas al instante)",
"signup_agreement": "Acepto no publicar contenido ilegal o de odio",
"comment": "Comentar",
"submit": "Enviar",
"site_ran_by": "El sitio es llevado por",
"signups_unavailable": "Lo siento, este servidor no permite registrarse",
"user_exists": "Lo siento, este usuario ya existe, prueba otro diferente",
"user_doesnt_exist": "Lo siento, este usuario no existe",
"comment_doesnt_exist": "Este comentario no existe, esto puede ser porque la publicación en la que estaba adjunto se ha eliminado",
"post_doesnt_exist": "Esta publicación no existe o se ha aliminado",
"incorrect_password": "Contraseña Incorrecta",
"rss_disabled": "Lo siento, RSS está desactivado",
"atom_disabled": "Lo siento, ATOM está desactivado",
"AI_consent": "El contenido de este sitio no debe ser copiado, raspado, o usado para entrenar modelos de IA o de lenguaje (LLMs) sin consentimiento previo.",
"rss_feed": "Feed RSS",
"atom_feed": "Feed ATOM",
"no_tags": "Sin Etiquetas",
"new_post": "Nueva Publicación",
"edit_post": "Editar Publicación",
"sign_up": "Registrarse",
"edit_account": "Editar Cuenta",
"permalink": "Enlace Permanente",
"written_by": "Escrito por",
"published": "Publicado",
"last_modified": "Última Modificación",
"hitcount": "Visitas",
"post_tagged": "Publicaciones Etiquetadas",
"home_page": "Página Principal",
"site_index": "Índice del Sitio",
"reply": "Responder",
"attribution": "Empujado por blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Código Fuente</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>licencia (WTFPL)</a>",
"translated_by": "Javalsai"
}

44
locales/ja_JP.json Normal file
View File

@@ -0,0 +1,44 @@
{
"quotes": "“”‘’",
"password": "パスワード",
"username": "ユーザー名",
"prettyname": "きれいな名前",
"description": "説明 (例えばSNSのリンクや何を書くなど)、 マークダウンをスポートする",
"title": "題名",
"post_content": "投稿の内容、マークダウンをスポートする",
"tags": "タグ (カンマで区切られています)",
"delete_account_confirmation": "アカウントを削除する - (アカウントと投稿の全部をいつまでも削除するに賛成します。)",
"signup_agreement": "違法なコンテントと憎らしいコンテントをポストしないに賛成します。",
"comment": "コメント",
"submit": "提出する",
"site_ran_by": "アドミン:",
"signups_unavailable": "申し訳ございませんでもこのサーバーはサインアップ",
"user_exists": "申し訳ございませんでもこのユーザー名をつかえります。別のユーザー名を入ります。",
"user_doesnt_exist": "申し訳ございませんでもこのアカウントがいません。",
"comment_doesnt_exist": "このコメントがない、これから投稿を削除したかもしれない",
"post_doesnt_exist": "この投稿がないか又は削除しました。",
"incorrect_password": "パスワードが違う",
"rss_disabled": "申し訳ございませんでもRSSが使用不可能なります。",
"atom_disabled": "申し訳ございませんでもATOMが使用不可能なります。",
"AI_consent": "書面による同意がないとこのホームページの内容はコピーするか又はスクレイピングするか又はAIモデルか大規模言語モデルLLM)を仕込むことが禁断します。",
"rss_feed": "RSSのフィード",
"atom_feed": "ATOMのフィード",
"no_tags": "タグがない",
"new_post": "新しい投稿",
"edit_post": "投稿をエディットする",
"sign_up": "サインアップ",
"edit_account": "アカウントをエディットする",
"permalink": "恒久リンク",
"written_by": "作家は",
"published": "発行の日付",
"last_modified": "全変更",
"hitcount": "ヒット数",
"posts_tagged": "投稿をタグするの数",
"home_page": "ホーム",
"site_index": "ホームページの索引",
"reply": "返事",
"attribution": "blogger-nodejsで作成されています: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>ソースコード</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>ライセンス (WTFPL)</a>",
"translated_by": "Nullifier"
}

44
locales/sv-SE.json Normal file
View File

@@ -0,0 +1,44 @@
{
"quotes": "“”‘’",
"password": "Lösenord",
"username": "Användarnamn",
"prettyname": "Vackert namn",
"description": "Beskrivning (sociala länkar, vad du skriver om m.m.), stöder markdown",
"title": "Titel",
"post_content": "Inläggsinnehåll, stöder markdown",
"tags": "Taggar (komma separerade)",
"delete_account_confirmation": "Radera mitt konto - (Jag förstår och accepterar att mitt konto och alla mina inlägg kommer att raderas permanent omedelbart)",
"signup_agreement": "Jag samtycker till att inte publicera olagligt eller hatiskt innehåll",
"comment": "Kommentera",
"submit": "Skicka",
"site_ran_by": "Webbplatsen drivs av",
"signups_unavailable": "Tyvärr, denna server tillåter inte registreringar",
"user_exists": "Tyvärr, den här användaren finns redan. Försök med ett annat användarnamn",
"user_doesnt_exist": "Tyvärr, den här användaren finns inte",
"comment_doesnt_exist": "Denna kommentar finns inte, vilket kan bero på att inlägget den var kopplad till har raderats",
"post_doesnt_exist": "Det här inlägget finns inte eller har raderats",
"incorrect_password": "Felaktigt Lösenord",
"rss_disabled": "Tyvärr, RSS är inaktiverat",
"atom_disabled": "Tyvärr, ATOM är inaktiverat",
"AI_consent": "Innehållet på denna webbplats får inte kopieras, skrapas eller användas för att träna AI-modeller eller stora språkmodeller (LLM) utan skriftligt samtycke.",
"rss_feed": "RSS-flöde",
"atom_feed": "ATOM-flöde",
"no_tags": "Inga taggar",
"new_post": "Nytt inlägg",
"edit_post": "Redigera inlägg",
"sign_up": "Registrera",
"edit_account": "Redigera konto",
"permalink": "Permalänk",
"written_by": "Skriven av",
"published": "Publicerad",
"last_modified": "Senast ändrad",
"hitcount": "Besökare",
"posts_tagged": "Taggade inlägg",
"home_page": "Startsida",
"site_index": "Webbplatsindex",
"reply": "Svara",
"attribution": "Drivs av blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Källkod</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>licens (WTFPL)</a>",
"translated_by": "pickzelle"
}

61
locales/template.jsonc Normal file
View File

@@ -0,0 +1,61 @@
{
"quotes": "“”‘’", // Single and Double quotes, according to https://github.com/markdown-it/markdown-it format
// Placeholders in form inputs!
"password": "Password",
"username": "Username",
"prettyname": "Prettyname",
"description": "Description (social links, what you write about etc), supports markdown", // Should explain what can be entered into the user description/bio
"title": "Title", // Post title
"post_content": "Post Content, supports markdown",
"tags": "Tags (comma seperated)", // An input field that allows you to enter a comma seperated list of tags like: 'sus,test,haha'
"delete_account_confirmation": "Delete my account - (I agree that my account and all of my posts will be permanently deleted instantly)", // Should make it clear that all user data and posts will be deleted
"signup_agreement": "I agree to not post illegal or hateful content", // Should make it clear that you cannot post illegal or hateful content
"comment": "Comment",
"submit": "Sumbit",
"site_ran_by": "Site is ran by", // eg 'Site is ran by Bob', it shows up in the footer of each page
// Error messages, should just apologise and make it clear the error
"signups_unavailable": "Sorry, this server does not allow signups",
"user_exists": "Sorry, this user already exists, try a different username",
"user_doesnt_exist": "Sorry, this user does not exist",
"comment_doesnt_exist": "This comment doesn't exist, this could be because the post it was attached to was deleted",
"post_doesnt_exist": "This post doesn't exist or was deleted",
"incorrect_password": "Incorrect Password",
"rss_disabled": "Sorry, RSS is disabled",
"atom_disabled": "Sorry, ATOM is disabled",
// Disclaimer, not legally binding
"AI_consent": "The content on this website may not be copied, scraped, or used to train AI models or large language models (LLMs) without prior written consent.",
// Hyperlinks to pages and plain text that shows up on the website
"rss_feed": "RSS Feed",
"atom_feed": "ATOM Feed",
"no_tags": "No Tags",
"new_post": "New Post",
"edit_post": "Edit Post",
"sign_up": "Sign Up",
"edit_account": "Edit Account",
"permalink": "Permalink",
"written_by": "Written by", // A post is written/authored by x person
"published": "Published", // Published on this date
"last_modified": "Last Modified", // Last modified on this date
"hitcount": "Hitcount", // The number of views/hits/visits to a page, eg: 'hitcount: 53'
"posts_tagged": "Posts Tagged",
"home_page": "Home Page", // The main or default page, ie index.html
"site_index": "Site Index", // Or 'site map'
"reply": "reply", // Reply to a comment
// Attribution for the source code, don't change the URLs obviously, just the text within them.
"attribution": "Powered by blogger-nodejs: <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs'>Source Code</a>, <a href='https://git.javalsai.tuxcord.net/deadvey/blogger-nodejs/raw/branch/master/LICENSE'>license (WTFPL)</a>",
// Comma seperated list of people who contributed to this translation
"translated_by": "DeaDvey"
// TODO
// indexes locales
// Password again
// site_admin?
// Should colons be part of the translations?
}

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "blogger-nodejs",
"version": "0.0.5",
"description": "Simple web logging backend",
"author": "DeaDvey",
"license": "WTFPL",
"dependencies": {
"date-fns": "^4.1.0",
"ejs": "^3.1.10",
"express": "^5.2.1",
"express-router": "^0.0.1",
"markdown-it": "^14.1.0",
"mysql": "^2.18.1",
"package.json": "^2.0.1"
}
}

171
src/data.js Normal file
View File

@@ -0,0 +1,171 @@
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const func = require('./functions.js')
const config = require("../config.json")
const fs = require("fs")
// Literally just +1 to the hitcount
export function increment_hitcount(postID = -1) { // -1 Means it will increment the timeline hitcount
if (config.data_storage == 'json') {
if (postID == -1) {
let hitcount = getdata('hitcount');
hitcount += 1
writedata('hitcount', hitcount);
}
else {
let post = getdata('posts','id', postID);
if (post == 1) // Does not exist
{
return 1
}
else if (typeof post.hitcount != 'undefined') {
post.hitcount += 1;
writedata('posts', post, postID)
return 0
}
else {
post.hitcount = 1;
writedata('posts', post, postID)
return 0
}
return 1
}
}
};
export function searchdata(term, type) { // Searches users and posts for any matches
let search_results = {"posts": [], "users": []};
// Search users
if (type.includes('post')) {
let list = getdata('posts');
list.forEach((element,index) => {
if (typeof element.deleted == 'undefined' || element.deleted == false) {
if (element.content.includes(term)) {
search_results.posts.push(element)
}
else if (element.title.includes(term)) {
search_results.posts.push(element)
}
else if (element.tags.toString().includes(term)) {
search_results.posts.push(element)
};
};
});
}
if (type.includes('user')) {
let list = getdata('users');
list.forEach((element,index) => {
if (typeof element.deleted == 'undefined' || element.deleted == false) {
if (element.username.includes(term)) {
search_results.users.push(element)
}
else if (element.prettyname.includes(term)) {
search_results.users.push(element)
}
else if (element.description.includes(term)) {
search_results.users.push(element)
};
};
});
}
return search_results;
};
export function getdata(table_name, key=-1, value=-1) {
let result = undefined
switch (config["data_storage"]) {
case 'json':
switch (table_name) {
case 'users':
case 'posts':
case 'comments':
result = func.require_module(`../data/${table_name}.json`)
if (key != -1) {
if (key == 'id')
{ // id is the index
if (value < result.length && value >= 0)
{
return result[value]
}
else
{
console.log("No object of this ID exists for the selected table")
return 1
}
}
return result[func.find_key_value_pair(result, key, value)]
return -1 // This index doesn't exist
}
return result.slice(- config['data_request_limit'])
break;
case 'hitcount':
result = func.require_module('../data/data.json') // This file is actually called data.json
return result["hitcount"]
break;
default:
console.log("Error, invalid requested")
return -1
break;
}
break;
// NOT YET WORKING!
case 'mysql':
const mysql = require('mysql');
let con = mysql.createConnection({
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.database,
});
con.connect(function(err) {
if (err) throw err;
if (data == "posts" || data == 'users' || data == 'comments') {
con.query(`SELECT * FROM ${data}`, function (err, result, fields) {
if (err) throw err;
result = Object.values(JSON.parse(JSON.stringify(result)))
console.log(result)
return result;
});
}
else if (data == 'hitcount') {
con.query(`SELECT paramValue FROM params WHERE paramName = '${data}'`, function (err, result, fields) {
if (err) throw err;
result = Object.values(JSON.parse(JSON.stringify(result)))
console.log(result)
return result;
});
}
});
}
}
export function writedata(data, data_to_write, index=-1) {
if (config["data_storage"] == "json") {
if (data == "posts" || data == 'users' || data == 'comments') {
if (index == -1) {
fs.writeFileSync(`../data/${data}.json`, JSON.stringify(data_to_write), 'utf-8')
return 0
}
else if (index >= 0) {
let result = getdata(data);
result[index] = data_to_write;
fs.writeFileSync(`../data/${data}.json`, JSON.stringify(result), 'utf-8')
return 0
}
return 1
}
else if (data == "hitcount") {
let other_data = func.require_module('../data/data.json') // This file is actually called data.json
other_data.hitcount = data_to_write
fs.writeFileSync('../data/data.json', JSON.stringify(other_data), 'utf-8')
}
else {
console.log("Error, invalid requested")
return 1
}
}
}

148
src/functions.js Normal file
View File

@@ -0,0 +1,148 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url)
const config = require("../config.json")
const fs = require('fs')
const locale = require(`../locales/${config.locale}.json`)
// This function requires a module without caching it
// So the server doesn't need to be restarted, though this can slow it down a bit.
// https://stackoverflow.com/a/16060619
export function require_module(module)
{
if (config.cache_data == false)
{
delete require.cache[require.resolve(module)];
return require(module);
}
else
{
return require(module);
}
}
// The configuration defines a date format using the date-fns (a datetime library) syntax
// eg "yyyy-MM-dd"
// this converts unix time (an integer) into a string that is formatted according to config.js
// uses date-fns's fromUnixTime() and format() functions
// returns the formatted date (string)
export function unix_time_to_date_format(unix_time)
{
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
let date = fromUnixTime(unix_time)
let formatted_date = format(date, config.date_format)
return formatted_date
}
// This is similar to the above function, however, instead of formatting to the users
// configuration, it formats to RFC-822 which is the date format used by RSS feeds
// eg "Mon, 23 May 2025 18:59:59 +0100"
// accepts unix time (int)
// returns the formatted date (string)
export function unix_time_to_rss_date(unix_time)
{
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
let date = fromUnixTime(unix_time)
let formatted_date = format(date, "EEE, dd MMM yyyy HH:mm:ss")
return `${formatted_date} ${config.time_zone}`
}
// And again with atom's date format
export function unix_time_to_atom_date(unix_time)
{
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
let date = fromUnixTime(unix_time)
let formatted_date = format(date, "yyyy-MM-dd'T'HH:mm:ss'Z'")
return `${formatted_date}`
}
// This function accepts a list of strings eg ["string1","string2,"string3"] (any length)
// then returns a string of them each pointing to a seperate url
// eg "<a href="/tag/string1">string1</a>, <a href="/tag/string2">string2</a>, <a href="/tag/string3">string3</a>"
// this is so you can have a list of tags that each point to their individual tag page
// returns: string
export function render_tags(tags)
{
let string = "" // Initialises the string
if (tags.length == 1 && tags[0] == "")
{
string = ''; // If there are no tags, output nothing
}
else
{
for (let tag_index = 0; tag_index < tags.length; tag_index++)
{ // Loop over each tag
string += `<a href="/tag/${tags[tag_index].trim()}">#${tags[tag_index].trim()}</a> ` // Adds the tag to the string as a HTML href
}
}
return string
}
// The users are stored as a list of objects [ user_object, user_object, user_object ]
// So you cannot easily find the userID (position in list) from the username
// This function returns the username for a given userID by looping over every user
// if the user is present, it returns the index of the user (integer)
// if the user is not present it returns -1
export function get_userID(username)
{
const users = require_module("../data/users.json")
for (let i = 0; i < users.length; i++)
{ // Loop over every user
if (users[i]['username'] == username)
{
return i // If the username matches then return the index of that user
}
}
return -1 // If user is not present, return -1
}
// This escapes some potentially dangerous HTML characters with their HTML entities
// https://www.freeformatter.com/html-entities.html
// accepts a string
// returns a string with some character replaced by their entities
export function escape_input(input)
{
let output = input
.replaceAll("&", "&amp;") // This must be first
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\\", "&#92;")
.replaceAll('"', "&#34;")
.replaceAll("'", "&#39;")
.replaceAll("/", "&#47;")
.replaceAll("%", "&#37;")
return output
}
// Render comment content by replacing the >> int with a url link to that comment
// Syntax: ">> postID-commentID"
export function render_comment(comment_content)
{
return comment_content
.replaceAll(/>> ([0-9]*)-([0-9]*)/g, "<a href='/comment/$1-$2'>>> $1-$2</a>")
.replaceAll(/>>([0-9]*)-([0-9]*)/g, "<a href='/comment/$1-$2'>>>$1-$2</a>")
.replaceAll(/&gt;&gt; ([0-9]*)-([0-9]*)/g, "<a href='/comment/$1-$2'>>> $1-$2</a>")
.replaceAll(/&gt;&gt;([0-9]*)-([0-9]*)/g, "<a href='/comment/$1-$2'>>>$1-$2</a>")
.replaceAll("\n", "<br/>")
};
// Renders a string into markdown using markdown-it library
export function render_md(content)
{
const markdownit = require("markdown-it")
const md = markdownit
({ // this is just defining some options for markdown-it, should I add this to config.json?
html: false,
xhtmlOut: false,
breaks: true,
linkify: false,
typographer: true,
quotes: locale.quotes,
})
return md.render(content)
};
export function find_key_value_pair(data_array, key, value) {
for (let i = 0; i < data_array.length; i++) {
if (data_array[i][key] == value) {
return i
}
}
return -1
};

229
src/routes/form_actions.js Normal file
View File

@@ -0,0 +1,229 @@
const express = require('express');
const config = require('../../config')
const data = require('../data')
const func = require('../functions')
let users = require('../../data/users.json');
let posts = require('../../data/posts.json');
let comments = require('../../data/comments.json');
let other_data = require('../../data/data.json');
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
const fs = require('fs')
const crypto = require('crypto')
const router = express.Router();
////////////////////// Form actions /////////////////////////
router.post("/submit_comment", (req,res) => {
const unix_timestamp = getUnixTime(new Date())
const postID = parseInt(req.body.post_index)
const content = func.escape_input(req.body.content)
let name = func.escape_input(req.body.name)
// Give the user the default username if they left that bit blank
if (name == "" || typeof name == 'undefined') {
name = config.default_commenter_username
}
// Check there is actually content in the comment
if (content != '' && typeof content != 'undefined') {
let comments = data.getdata('comments')
new_comment = {
"name": name,
"content": content,
"id": comments[postID]['comments'].length,
"pubdate": unix_timestamp,
};
comments[postID]['comments'].push(new_comment);
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`, 'utf-8');
}
res.redirect(301,`/post/${req.body.post_index}`)
}); // /submit_comment
router.post("/submit_post", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const username = func.escape_input(req.body.username)
const title = func.escape_input(req.body.title)
const content = req.body.content
const tags = func.escape_input(req.body.tags).split(',').map(str => str.trim());
const unix_timestamp = getUnixTime(new Date())
if (func.get_userID(username) == -1) {
res.render("partials/message", {
message: locale.user_doesnt_exit,
config,
})
}
else if (users[func.get_userID(username)]['hash'] == password) { // Password matches
console.log(username, "is submitting a post titled:", title);
id = posts.length
posts.push({
"id": id,
"userID": func.get_userID(username),
"title": title,
"content": content,
"pubdate": unix_timestamp,
"editdate": unix_timestamp,
"tags": tags,
})
fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
comments.push({'id': id, 'comments': []})
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`)
res.redirect(302, "/");
}
else {
res.render("partials/message", {
message: locale.incorrect_password,
config,
})
}
}); // /submit_post
router.post("/submit_signup", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const username = func.escape_input(req.body.username)
const prettyname = func.escape_input(req.body.prettyname)
const description = req.body.description
// Check that signups are allowed
if (config.allow_signup == true) {
// func.get_userID will return -1 if the user does not exist
// so this checks that the user does not exist
if (func.get_userID(username) == -1) {
users.push({
"id": users.length,
"username": username,
"prettyname": prettyname,
"hash": password,
"description": description,
})
fs.writeFileSync(`../data/users.json`, `${JSON.stringify(users)}`, 'utf-8');
res.redirect(301, `/user/${username}`)
}
// if the user does exist then
else {
res.render("partials/message", {
message: locale.user_exists,
config,
})
}
}
else if (config.allow_signup == false) {
res.render("partials/message", {
message: locale.signups_unavailable,
config,
})
}
// If allow_signup is undefined or not a boolean, error
else {
res.redirect(301,"/")
console.log("Error, invalid value for allow_signup (bool)")
}
}); // /submit_signup
router.post("/submit_edit_user", (req,res) => {
// Get the form info
const password = crypto.createHash("sha512").update(req.body.password).digest("hex");
const userID = func.escape_input(req.body.userID)
const description = req.body.description
const prettyname = func.escape_input(req.body.prettyname)
const delete_bool = req.body.delete
if (userID >= 0) { // The user exists
if (password == users[userID]['hash']) { // password matches
console.log(userID, " (userID) is modifying their account")
users[userID]["prettyname"] = prettyname;
users[userID]["description"] = description;
if (delete_bool == true) {
// Delete the user
users[userID] = {"id": userID,"deleted": true}
// Delete all their posts
for (let postid = 0; postid < posts.length; postid++) { // loop over all posts
if (posts[postid]['userID'] == userID) { // if userID matches
posts[postid] = {"id": postid, "deleted": true} // delete the post
comments[postid] = [] // the comments for this post should also be deleted
}
};
}
// Write these changes
fs.writeFileSync(`../data/users.json`, `${JSON.stringify(users)}`, 'utf-8');
fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`, 'utf-8');
res.redirect(301,`/user/${users[userID]["username"]}`)
}
else { // password does not match
res.render("partials/message", {
message: locale.incorrect_password,
config
}
)
};
}
else {
res.render("partials/message", {
message: locale.user_doesnt_exist,
config,
})
}
}); // /submit_delete_account
router.post("/submit_edit_post", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const postID = req.body.postID
const userID = req.body.userID
const title = func.escape_input(req.body.title)
const content = req.body.content
const tags = func.escape_input(req.body.tags).split(",").map(str => str.trim());
const delete_bool = req.body.delete
const unix_timestamp = getUnixTime(new Date())
console.log(users[userID]['prettyname'], "is editting the post titled:", title);
if (users[userID]['hash'] == password) { // password matches
let post = posts[postID]
post['title'] = title
post['content'] = content
post['tags'] = tags
post['editdate'] = unix_timestamp
if (typeof delete_bool != "undefined") {
console.log("Deleting post!")
posts[postID] = {"id": post["id"], "deleted": true}
comments[postID] = [];
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`, 'utf-8');
}
fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
res.redirect(302, "/");
}
else {
res.render("partials/message", {
message: locale.incorrect_password,
config,
})
}
}); // /submit_edit
router.get('/search', (req, res) => {
const search_term = func.escape_input(req.query.q); // 'q' is the parameter name
let search_type = req.query.type; // eg 'post', 'user'
if (typeof search_type == 'string') { // Make the search_term an array
search_type = [ search_type ]
}
if (typeof search_type == 'undefined') { // Default to all of the types
search_type = ['user', 'post'];
}
console.log('searching for: ', search_term);
const search_results = data.searchdata(search_term, search_type); // data.searchdata returns an array of search results
res.render('pages/search', {
config,
locale,
search_results,
search_term,
search_type,
})
}); // /search
module.exports = router;

58
src/routes/forms.js Normal file
View File

@@ -0,0 +1,58 @@
const express = require('express');
const config = require('../../config')
const data = require('../data')
const func = require('../functions')
const router = express.Router();
///////////////////// Form pages ////////////////////////////
router.get(config.new_post_url, (req,res) => {
res.render("forms/new_post", {
config,
locale,
});
}); // /post
router.get(config.signup_url, (req,res) => {
// if the server does allow signup
if (config.allow_signup == true) {
// Send the page for signing up to the server
res.render("forms/signup", {
config,
locale,
});
}
// if the server does not allow signup
else if (config.allow_signup == false) {
res.render("partials/message", {
message: locale.signups_unavailable,
config,
})
}
// If allow_signup is undefined or not a boolean, error
else {
res.redirect(301,"/")
console.log("Error, invalid value for allow_signup (bool)")
}
}); // /signup
router.get(`${config.edit_account_base_url}/:user_id`, (req,res) => {
const userID = parseInt(req.params.user_id);
res.render("forms/edit_account", {
config,
locale,
user: data.getdata('users', 'id', userID),
userID
});
}); // /delete_account
router.get(`${config.edit_post_base_url}/:post_id`, (req,res) => {
const postID = req.params.post_id
const post = data.getdata('posts','id', postID)
const user = data.getdata('users', 'id', post.userID)
res.render("forms/edit_post", {
config,
locale,
post,
postID,
user,
});
}); // /edit/:post_id
module.exports = router;

37
src/routes/indexes.js Normal file
View File

@@ -0,0 +1,37 @@
const express = require('express');
const config = require('../../config')
const data = require('../data')
const func = require('../functions')
const router = express.Router();
///////////////////// Page index's ///////////////////////
router.get("/index/pages", (req,res) => {
res.render("indexes/all_pages", {
config,
posts: data.getdata('posts'),
users: data.getdata('users'),
comments: data.getdata('comments'),
});
}); // /index/pages
router.get("/index/posts", (req,res) => {
res.render("indexes/posts", {
config,
posts: data.getdata('posts'),
});
}); // /index/posts
router.get("/index/users", (req,res) => {
res.render("indexes/users", {
config,
users: data.getdata('users'),
});
}); // /index/users
router.get("/index/comments", (req,res) => {
res.render("indexes/comments", {
config,
comments: data.getdata('comments'),
});
}); // /index/comments
module.exports = router;

View File

@@ -0,0 +1,151 @@
const express = require('express');
const config = require('../../config')
const data = require('../data')
const func = require('../functions')
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
const router = express.Router();
///////////////////// Standard Pages //////////////////////
// Timeline
router.get("/", (req,res) => {
// Increment the hitcount
if (config.enable_hitcount) {
data.increment_hitcount()
}
res.render("pages/timeline",
{
config,
locale,
posts: data.getdata("posts"),
users: data.getdata("users"),
comments: data.getdata("comments"),
hitcount: data.getdata("hitcount"),
fromUnixTime,
format,
getUnixTime,
func,
})
}); // /
// Users
router.get("/user/:username", (req, res) => {
const userID = func.get_userID(req.params.username)
let user = data.getdata('users', 'id', userID)
if (userID != -1) {
res.render("pages/user",
{
config,
locale,
posts: data.getdata('posts'),
user,
userID: userID,
comments: data.getdata('comments'),
fromUnixTime,
format,
getUnixTime,
func,
})
}
else if (userID == -1) {
res.render("partials/message",
{
message: locale.user_doesnt_exist,
config,
})
}
}); // /user/:username
// Posts
router.get("/post/:post_index", (req, res) => {
const postID = parseInt(req.params.post_index)
let post = data.getdata('posts','id', postID)
if (post == 1) { // data.getdata returns error code 1 if nothing is available
res.render("partials/message", {
message: locale.post_doesnt_exist,
config,
})
}
else if (typeof post["deleted"] == "undefined" || post["deleted"] == false) {
if (config.enable_hitcount) {
data.increment_hitcount(postID)
}
res.render("pages/post",
{
config,
locale,
post,
postID,
user: data.getdata('users','id', post.userID),
comments: data.getdata('comments','id', postID)["comments"],
fromUnixTime,
format,
getUnixTime,
func,
})
}
else {
console.log("Error loading page")
res.redirect(301,"/")
}
}); // /post/:post_index
// Tags
router.get("/tag/:tag", (req,res) => {
const tag = req.params.tag
res.render("pages/tag",
{
config,
locale,
tag,
posts: data.getdata('posts'),
users: data.getdata('users'),
comments: data.getdata('comments'),
fromUnixTime,
format,
getUnixTime,
func,
})
}); // /tag/:tag
// Comments
router.get("/comment/:postID-:commentID", (req,res) => {
const commentID = parseInt(req.params.commentID);
const postID = parseInt(req.params.postID);
let posts_comments = data.getdata('comments', 'id', postID)["comments"]
let comment = 1
// For loop to find the comment with matching ID
posts_comments.forEach((current_comment, index) => {
if (current_comment.id == commentID) {
comment = posts_comments[index]
}
})
// If comment doesn't exist, show error
if (comment == 1 || posts_comments == 1) { // Comment of this ID was not found
res.render("partials/message", {
config,
message: locale.comment_doesnt_exist,
})
}
else {
res.render("pages/comment",
{
config,
locale,
comment,
postID,
commentID,
fromUnixTime,
format,
getUnixTime,
func,
})
}
});
module.exports = router;

88
src/routes/syndication.js Normal file
View File

@@ -0,0 +1,88 @@
const express = require('express');
const config = require('../../config')
const data = require('../data')
const func = require('../functions')
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
const router = express.Router();
////////////////////// SYNDICATION ////////////////////////
// global RSS protocol gets
router.get("/rss", (req,res) => {
if (config.rss == false) {
res.render("partials/message", {
message: locale.rss_disabled,
config,
})
}
else {
res.setHeader('content-type', 'application/rss+xml');
res.render("syndication/global_rss", {
config,
posts: data.getdata('posts'),
func,
})
};
});
// user RSS protocol gets
router.get("/user/:username/rss", (req,res) => {
const username = req.params.username;
const userID = func.get_userID(username);
if (config.rss == false) {
res.render("partials/message", {
message: locale.rss_disabled,
config: config,
})
}
else {
res.setHeader('content-type', 'application/rss+xml');
res.render("syndication/user_rss", {
config,
posts: data.getdata('posts'),
func,
userID,
})
};
});
// global ATOM protocol gets
router.get("/atom", (req,res) => {
if (config.atom == false) {
res.render("partials/message", {
message: locale.atom_disabled,
config: config,
})
}
else {
res.setHeader('content-type', 'application/rss+xml');
res.render("syndication/global_atom", {
config,
posts: data.getdata('posts'),
func,
getUnixTime,
})
};
});
// user ATOM protocol gets
router.get("/user/:username/atom", (req,res) => {
const username = req.params.username;
const userID = func.get_userID(username);
if (config.atom == false) {
res.render("partials/message", {
message: locale.atom_disabled,
config: config,
})
}
else {
res.setHeader('content-type', 'application/rss+xml');
res.render("syndication/user_atom", {
config,
posts: data.getdata('posts'),
func,
userID,
getUnixTime,
})
};
});
module.exports = router;

89
src/server.js Normal file
View File

@@ -0,0 +1,89 @@
// Get the libraries
const fs = require('fs'); // For modifying and reading files
const express = require('express'); // For running a webserver in nodejs
const crypto = require('crypto'); // For encrypting passwords, I use sha512
// fromUnixTime(): Create a date from a Unix timestamp (in seconds). Decimal values will be discarded.
// format(): Return the formatted date string in the given format. The result may vary by locale.
// getUnixTime(): Get the seconds timestamp of the given date.
// find out more at https://date-fns.org/ \or docs: https://date-fns.org/docs/Getting-Started
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
const ejs = require("ejs")
const func = require("./functions.js")
const data = require("./data.js")
config = require('../config.json');
// Import the locale
try {
locale = require(`../locales/${config.locale}.json`);
}
catch (error) {
console.log("This locale doesn't exist, if you want to create it then you can create a PR")
console.log("Locale selected: ", config.locale)
}
// Define stuff to do with express (nodejs webserver)
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static(config.root_path));
// set the view engine to ejs
app.set('view engine', 'ejs');
app.set('views', '../views')
// Express JS routes
const syndication_routes = require('./routes/syndication.js');
const indexes_routes = require('./routes/indexes.js');
const standard_pages_routes = require('./routes/standard_pages.js');
const forms_routes = require('./routes/forms.js');
const form_actions_routes = require('./routes/form_actions.js');
app.use('/', syndication_routes);
app.use('/', indexes_routes);
app.use('/', standard_pages_routes);
app.use('/', forms_routes);
app.use('/', form_actions_routes);
function perform_checks()
{
console.log("Performing startup checks...")
exit_flag = false
required_values = ['site_admin','seperator','site_name','site_url','locale','port','cache_data','allow_signup','site_description','request_data_limit','enable_hitcount','charset','root_path','edit_account_base_url','new_post_url','signup_url','default_commenter_username','rss','atom','date_format','time_zone','css']
// Perform some standard checks:
// data_storage
switch (config.data_storage)
{
case 'mysql':
case 'json':
break
default:
console.log("[ ERROR ] invalid value in `data_storage`\nPlease modify config.json. Value should be 'mysql' or 'json'.")
exit_flag = true
}
// auto_generated
if (config.auto_generated)
{
console.log("[ ERROR ] `autogenerated` option set to true\nplease edit the config.json file to include your relevant information, then set to false or remove the autogenerated option.")
exit_flag = true
}
// Check required values are present
required_values.forEach((value) => { // Use a loop to check each required value is present
if (typeof config[value] == 'undefined') {
exit_flag = true
console.log(`[ ERROR ] \`${value}\` is undefined\nPlease set it to something, read the documentation for help.`)
}
});
if (exit_flag)
{
console.log("Exiting due to errors.")
process.exit(1)
}
}
perform_checks()
app.listen(config.port, () => {
console.log(`Server is running at http://localhost:${config.port} webroot: ${config.root_path}`);
console.log("Running in: ", __dirname)
});

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="<%= config.language %>
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_edit_user" method="POST">
<input name="userID" type="hidden" value="<%= userID %>">
<label><%= locale.password %>:</label><br/>
<input type="password" required id="password" name="password"><br/><br/>
<label><%= locale.prettyname %>:</label><br/>
<input name="prettyname" value="<%= user.prettyname %>"><br/><br/>
<label><%= locale.description %>:</label><br/>
<textarea name="description"><%= user.description %></textarea><br/><br/>
<label><%- locale.delete_account_confirmation %>: </label><input type="checkbox" name="agreement"><br/>
<input type="submit" value="Submit"><br/>
</form>
</body>
</html>

27
views/forms/edit_post.ejs Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="<%= config.language %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_edit_post" method="POST" onsubmit="sha512password()">
<input name="userID" type="hidden" value="<%= post['userID'] %>">
<input name="postID" type="hidden" value="<%= postID %>">
<label><%= locale.password %>:</label><br/>
<input type="password" required id="password" name="password"><br/><br/>
<label><%= locale.title %>:</label><br/>
<input value="<%=post['title'] %>" required name="title"><br/><br/>
<label><%= locale.post_content %>:</label><br/>
<textarea required name="content"><%= post['content'] %></textarea><br/><br/>
<label><%= locale.tags %>:</label><br/>
<input value="<%= post['tags'] %>" name="tags"><br/><br/>
<label>Delete forever (no undo): </label><input name="delete" type="checkbox"><br/>
<input type="submit" value="Submit"><br/>
</form>
</body>
</html>

25
views/forms/new_post.ejs Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
</html>
<head>
<%- include('../partials/head.ejs') %>
</head>
<body>
<form action="/submit_post" method="POST">
<label><%= locale.username %>:</label><br/>
<input required name="username"><br/><br/>
<label><%= locale.password %>:</label><br/>
<input type="password" required id="password" name="password"><br/><br/>
<label><%= locale.title %>:</label><br/>
<input required name="title"><br/><br/>
<label><%= locale.post_content %>:</label><br/>
<textarea required name="content"></textarea><br/><br/>
<label><%= locale.tags %>:</label><br/>
<input name="tags"><br/><br/>
<input type="submit" value="Submit"><br/>
</body>
</form></html>

24
views/forms/signup.ejs Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
</html lang="<%= config.language %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_signup" method="POST">
<label><%= locale.username %></label><br/>
<input required name="username"><br/><br/>
<label><%= locale.prettyname %></label><br/>
<input required name="prettyname"><br/><br/>
<label><%= locale.password %></label><br/>
<input type="password" required id="password" name="password"><br/><br/>
<label><%= locale.description %></label><br/>
<textarea id="description" name="description"></textarea><br/><br/>
<label><%- locale.signup_agreement %>: </label><input type="checkbox" name="agreement" required><br/>
<input type="submit" value="Submit"><br/>
</form>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<a id='home-page-link' href="/"><%= locale.home_page %></a>
/
<% if (config.rss == true) { %>
<a id='rss-link' href="/rss">
<img class='icon' src='/icons/rss.png' alt="<%= locale.rss_feed %>" title='<%= locale.rss_feed %>'>
</a>
<% } %>
<% if (config.atom == true) { %>
<a id='atom-link' href="/atom">
<img class='icon' src='/icons/atom.png' alt='<%= locale.atom_feed %>' title='<%= locale.atom_feed %>'>
</a>
<% } %>
/
<a id='new-post-link' href="<%= config.new_post_url %>"><%= locale.new_post %></a>
/
<% if (config.allow_signup == true) { %>
<a id='signup-link' href="<%= config.signup_url %>"><%= locale.sign_up %></a>
<% } %>
/
<form id='search-form' method="GET" action="/search" style="display: inline">
<input type="text" placeholder="🔍" name="q"><input type="submit" value="Search">
</form>
<br/>
<div id='site-name'>
<h1><a id='home-page-link' href="/"><%= config.site_name %></a></h1>
</div>
<div id='site-description'>
<h2><%- config.site_description %></h2>
</div>
<%- config.seperator %>

6
views/headers/tag.ejs Normal file
View File

@@ -0,0 +1,6 @@
<div id='tag-page-title'>
<h1>
#<%- tag %>
</h1>
</div>
<%- config.seperator %>

View File

17
views/headers/user.ejs Normal file
View File

@@ -0,0 +1,17 @@
<div id='user-page-title'>
<h1>
<%= user.prettyname %>
</h1>
</div>
<p><%- func.render_md(user.description) %></p>
<a if='edit-account-link' href="<%= config.edit_account_base_url %>/<%= userID %>">
<img class='icon' src='/icons/edit.png' alt='<%= locale.edit_account %>' title='<%= locale.edit_account %>'>
</a>
|
<a id='rss-link' href="/user/<%= user.username %>/rss">
<img class='icon' src='/icons/rss.png' alt="<%= locale.rss_feed %>" title='<%= locale.rss_feed %>'>
</a>
<a id='atom-link' href="/user/<%= user.username %>/atom">
<img class='icon' src='/icons/atom.png' alt='<%= locale.atom_feed %>' title='<%= locale.atom_feed %>'>
</a>
<%- config.seperator %>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="<%= config.charset %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
Misc:<br/>
<a href="/">Home Page</a><br/>
<a href="<%= config.new_post_url %>">New Post Form</a><br/>
<a href="<%= config.signup_url %>">Signup Form</a><br/>
Indexes:<br/>
<a href="/index/posts">Posts Index</a><br/>
<a href="/index/users">Users Index</a><br/>
<a href="/index/comments">Comments Index</a><br/>
Posts:<br/>
<% for (let postID = 0; postID < posts.length; postID++) { %>
<% if (posts[postID]["deleted"] != true) { %>
<a href="/post/<%= postID %>"><%= posts[postID]["title"] %></a><br/>
<% }; %>
<% }; %>
Comments:<br/>
<% for (let postID = 0; postID < comments.length; postID++) { %>
<% for (let comment_index = 0; comment_index < comments[postID]['comments'].length; comment_index++) { %>
<a href="/comment/<%= postID %>-<%= comment_index %>"><%= postID %>-<%= comment_index %></a><br/>
<% }; %>
<% }; %>
Users:<br/>
<% for (let userID = 0; userID < users.length; userID++) { %>
<% if (users[userID]["deleted"] != true) { %>
<a href="/user/<%= users[userID]["username"] %>"><%= users[userID]["username"] %></a><br/>
<% }; %>
<% }; %>
Edit Posts:<br/>
<% for (let postID = 0; postID < posts.length; postID++) { %>
<% if (posts[postID]["deleted"] != true) { %>
<a href="<%= config.edit_post_base_url %>/<%= postID %>">Edit <%= posts[postID]["title"] %></a><br/>
<% }; %>
<% }; %>
Edit Users:<br/>
<% for (let userID = 0; userID < users.length; userID++) { %>
<% if (users[userID]["deleted"] != true) { %>
<a href="<%= config.edit_account_base_url %>/<%= users[userID]["username"] %>">Edit <%= users[userID]["username"] %></a><br/>
<% }; %>
<% }; %>
<!-- TODO add tags -->
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="<%= config.charset %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<% for (let postID = 0; postID < comments.length; postID++) { %>
<% for (let comment_index = 0; comment_index < comments[postID]['comments'].length; comment_index++) { %>
<a href="/comment/<%= postID %>-<%= comment_index %>"><%= postID %>-<%= comment_index %></a><br/>
<% }; %>
<% }; %>
</body>
</html>

13
views/indexes/posts.ejs Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="<%= config.charset %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<% for (let postID = 0; postID < posts.length; postID++) { %>
<% if (posts[postID]["deleted"] != true) { %>
<a href="/post/<%= postID %>"><%= posts[postID]["title"] %></a><br/>
<% }; %>
<% }; %>
</body>
</html>

13
views/indexes/users.ejs Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="<%= config.charset %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<% for (let userID = 0; userID < users.length; userID++) { %>
<% if (users[userID]["deleted"] != true) { %>
<a href="/user/<%= users[userID]["username"] %>"><%= users[userID]["username"] %></a><br/>
<% }; %>
<% }; %>
</body>
</html>

26
views/pages/comment.ejs Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('../partials/head'); %>
</head>
<body>
<header id="site-header">
<%- include("../headers/site_wide") %>
</header>
<div id="comment">
<%- include("../partials/comment"); %>
</div>
<%- config.seperator %>
<b><%= locale.reply %>:</b>
<div id="post-commentform">
<!-- Comment form -->
<form method="POST" action="/submit_comment">
<input type="hidden" name="post_index" value="<%= postID %>">
<label><%= locale.username %>:</label><br/><input name="name"><br/><br/>
<label><%= locale.comment %>:</label><br/><textarea name="content">>> <%- postID %>-<%- commentID %>
</textarea><br/>
<button type="submit"><%= locale.submit %></button>
</form>
</div>
</body>
</html>

17
views/pages/post.ejs Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('../partials/head'); %>
</head>
<body>
<header id="site-header">
<%- include("../headers/site_wide") %>
</header>
<div id="posts">
<%- include('../posts/post'); %>
</div>
<footer id='footer'>
<%- include('../partials/footer'); %>
</footer>
</body>
</html>

37
views/pages/search.ejs Normal file
View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang='<%- config.locale %>'>
<head>
<%- include('../partials/head'); %>
</head>
<body>
<div id='site-header'>
<%- include('../headers/site_wide'); %>
</div>
<div id='advanced-search'>
<form method="GET" action="/search">
<label>Search Term:</label>
<input type='text' placeholder='🔍' name='q' value='<%- search_term %>'><br/>
<label>Search for:</label><br/>
<label>Post:</label>
<input type="checkbox" name="type" value="post" <% if (search_type.includes('post')) {%>checked<% } %>><br/>
<label>User:</label>
<input type="checkbox" name="type" value="user" <% if (search_type.includes('user')) {%>checked<% } %>><br/>
<input type="submit" value="Submit">
</form>
</div>
<%- config.seperator %>
<div id='results'>
<% search_results.posts.forEach((result, index) => { %>
<a href="/post/<%- result.id %>"><%- result.title %></a><br/>
<% }); %>
<% search_results.users.forEach((result, index) => { %>
<a href="/user/<%- result.username %>"><%- result.prettyname %></a><br/>
<% }); %>
</div>
</body>
</html>

28
views/pages/tag.ejs Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('../partials/head'); %>
</head>
<body>
<header id="site-header">
<%- include('../headers/site_wide'); %>
</header>
<header id='page-header'>
<%- include('../headers/tag'); %>
</header>
<div id="posts">
<% for (let index = posts.length - 1; index >= 0; index--) { %>
<% if ( posts[index].deleted != true) { %>
<% posts[index].tags.forEach((current_tag, tag_index) => { %>
<% if (current_tag.toLowerCase() == tag.toLowerCase()) { %>
<%- include('../posts/tag', {post: posts[index], postID: index, user: users[posts[index].userID], comments: comments[index]}); %>
<% } %>
<% }) %>
<% } %>
<% } %>
</div>
<footer id='footer'>
<%- include('../partials/footer'); %>
</footer>
</body>
</html>

38
views/pages/timeline.ejs Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('../partials/head'); %>
</head>
<body>
<header id="site-header">
<%- include("../headers/site_wide") %>
</header>
<header id='page-header'>
<%- include('../headers/timeline'); %>
</header>
<form method="POST" action="/submit_nothing" style="display:none">
<!-- Form is used to help mitigate spam as it is the first form on the front page -->
<input type="hidden" name="post_index" value="0">
<input placeholder="username" name="name"><br/>
<textarea placeholder="comment" name="content"></textarea><br/>
<button type="submit">Submit</button>
</form>
<div id="posts">
<% for (let index = posts.length - 1; index >= 0; index--) { %>
<% if (posts[index]["deleted"] != true) { %>
<%- include('../posts/timeline', {post: posts[index], postID: index, user: users[posts[index].userID], comments: comments[index]}); %>
<% } %>
<% } %>
</div>
<form method="POST" action="/submit_nothing" style="display:none">
<!-- Form is used to help mitigate spam as it is the last form on the front page -->
<input type="hidden" name="post_index" value="0">
<input placeholder="username" name="name"><br/>
<textarea placeholder="comment" name="content"></textarea><br/>
<button type="submit">Submit</button>
</form>
<footer id='footer'>
<%- include('../partials/footer'); %>
</footer>
</body>
</html>

24
views/pages/user.ejs Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('../partials/head'); %>
</head>
<body>
<header id="site-header">
<%- include("../headers/site_wide") %>
</header>
<header id='page-header'>
<%- include('../headers/user'); %>
</header>
<div id="posts">
<% for (let index = posts.length - 1; index >= 0; index--) { %>
<% if (posts[index].userID == userID) { %>
<%- include('../posts/user', {post: posts[index], postID: index, user: user, comments: comments[index]}); %>
<% } %>
<% } %>
</div>
<footer id='footer'>
<%- include('../partials/footer'); %>
</footer>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<span id='comment-name'>
<b><%= comment.name %></b>
</span>
<span id='comment-date' class='date'>
<%= func.unix_time_to_date_format(comment.pubdate) %>
</span>
<span id='comment-id' style='display: inline'>
<a href='/comment/<%= postID %>-<%= comment.id %>'>No. <%= postID %>-<%= comment.id %></a>:<br/>
</span>
<span id='comment-content'>
<%- func.render_comment(comment.content) %>
</span>
<br/>

View File

@@ -0,0 +1,4 @@
<a id='site-index-link' href="/index/pages"><%= locale.site_index %></a><br/>
<%= locale.site_ran_by %> <%= config.site_admin %><br/>
<%- locale.attribution %><br/>
<%= locale.AI_consent %> <!-- remove consent for AI scrapers -->

9
views/partials/head.ejs Normal file
View File

@@ -0,0 +1,9 @@
<meta charset="<%- config.charset %>">
<title><%= config.site_name %></title>
<style>
</style>
<link rel="stylesheet" href="/default.css">
<link rel="stylesheet" href="/custom.css">

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="<%= config.language %>">
<head>
<%- include('head') %>
</head>
<body>
<div id="header">
<%- include("../headers/site_wide") %>
</div>
<%- message %>
</body>
</html>

60
views/posts/post.ejs Normal file
View File

@@ -0,0 +1,60 @@
<div id="post-header">
<h2>
<a href='/post/<%= post['id'] %>'><%= post.title %></a>
</h2>
</div>
<div id="post-tags">
<%- func.render_tags(post.tags) %><br/>
</div>
<div id="post-content">
<p>
<%- func.render_md(post.content) %><br/>
</p>
</div>
<div id="post-details">
<span id="post-author">
<i><%= locale.written_by %> <a href="/user/<%= user.username %>"><%= user.prettyname %></a></i>
</span>
-
<span id="post-pubdate">
<i><%= locale.published %>: <%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</span>
<div id="post-editdate">
<i><%= locale.last_modified %>: <%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</div>
<div id="post-edit">
<a href="<%= config.edit_post_base_url %>/<%= post["id"] %>">
<img class='icon' src='/icons/edit.png' alt='<%= locale.edit_post %>' title='<%= locale.edit_post %>'>
</a><br/>
</div>
<% if (config.enable_hitcount == true) { %>
<div id='post-hitcount'>
Hitcount: <%- post.hitcount %>
</div>
<% } %>
</div>
<hr/>
<div id="post-commentform">
<!-- Comment form -->
<form method="POST" action="/submit_comment">
<input type="hidden" name="post_index" value="<%= post["id"] %>">
<label><%= locale.username %>:</label><br/><input name="name"><br/><br/>
<label><%= locale.comment %>:</label><br/><textarea name="content"></textarea><br/>
<button type="submit"><%= locale.submit %></button>
</form>
</div>
<div id="post-comments">
<% comments.forEach((comment, index) => { %>
<div id="comment-no-<%= post.id %>-<%= comment.id %>" class="comment post-comment-<%= index %>">
<%- include('../partials/comment', {comment: comment}) %>
</div>
<% }) %>
</div>
<%- config.seperator %>

32
views/posts/tag.ejs Normal file
View File

@@ -0,0 +1,32 @@
<div id="post-header">
<h2>
<a href='/post/<%= post['id'] %>'><%= post.title %></a>
</h2>
</div>
<div id="post-tags">
<%- func.render_tags(post.tags) %><br/>
</div>
<div id="post-content">
<p>
<%- func.render_md(post.content) %>
</p>
</div>
<div id="post-details">
<span id="post-author">
<i><a href="/user/<%= user.username %>"><%= user.prettyname %></a></i>
</span>
-
<span id="post-pubdate">
<i><%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</span>
<br/>
<div id="post-edit">
<a href="<%= config.edit_post_base_url %>/<%= post["id"] %>">
<img class='icon' src='/icons/edit.png' alt='<%= locale.edit_post %>' title='<%= locale.edit_post %>'>
</a><br/>
</div>
</div>
<%- config.seperator %>

32
views/posts/timeline.ejs Normal file
View File

@@ -0,0 +1,32 @@
<div id="post-header">
<h2>
<a href='/post/<%= post['id'] %>'><%= post.title %></a>
</h2>
</div>
<div id="post-tags">
<%- func.render_tags(post.tags) %><br/>
</div>
<div id="post-content">
<p>
<%- func.render_md(post.content) %>
</p>
</div>
<div id="post-details">
<span id="post-author">
<i><a href="/user/<%= user.username %>"><%= user.prettyname %></a></i>
</span>
-
<span id="post-pubdate">
<i><%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</span>
<br/>
<div id="post-edit">
<a href="<%= config.edit_post_base_url %>/<%= post["id"] %>">
<img class='icon' src='/icons/edit.png' alt='<%= locale.edit_post %>' title='<%= locale.edit_post %>'>
</a><br/>
</div>
</div>
<%- config.seperator %>

32
views/posts/user.ejs Normal file
View File

@@ -0,0 +1,32 @@
<div id="post-header">
<h2>
<a href='/post/<%= post['id'] %>'><%= post.title %></a>
</h2>
</div>
<div id="post-tags">
<%- func.render_tags(post.tags) %><br/>
</div>
<div id="post-content">
<p>
<%- func.render_md(post.content) %>
</p>
</div>
<div id="post-details">
<span id="post-author">
<i><a href="/user/<%= user.username %>"><%= user.prettyname %></a></i>
</span>
-
<span id="post-pubdate">
<i><%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</span>
<br/>
<div id="post-edit">
<a href="<%= config.edit_post_base_url %>/<%= post["id"] %>">
<img class='icon' src='/icons/edit.png' alt='<%= locale.edit_post %>' title='<%= locale.edit_post %>'>
</a><br/>
</div>
</div>
<%- config.seperator %>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="<%= config.charset %>" ?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title><%= config.site_name %></title>
<link><%= config.site_url %></title>
<description><%= config.site_description %></description>
<updated><%= func.unix_time_to_atom_date(getUnixTime(new Date())) %></updated>
<id><%= config.site_url %></id>
<% for (let postID = posts.length-1; postID >= 0; postID--) { %>
<% if (posts[postID]["deleted"] != true) { %>
<entry>
<title><%= posts[postID]["title"] %></title>
<link><%= config.site_url %>/post/<%= postID %></link>
<summary><![CDATA[<%- func.render_md(posts[postID]["content"]) %>]]></summary>
<guid isPermaLink="true"><%= config.site_url %>/post/<%= postID %></guid>
<pubDate><%# func.unix_time_to_atom_date(posts[postID]['pubdate']) %></pubDate>
<% for (let tag_index = 0; tag_index < posts[postID]['tags'].length; tag_index++) { %>
<category><![CDATA[<%= posts[postID]['tags'][tag_index] %>]]></category>
<% } %>
</entry>
<% } %>
<% } %>
</feed>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="<%= config.charset %>" ?>
<rss version="2.0">
<channel>
<title><%= config.site_name %></title>
<link><%= config.site_url %></title>
<description><%= config.site_description %></description>
<% for (let postID = posts.length-1; postID >= 0; postID--) { %>
<% if (posts[postID]["deleted"] != true) { %>
<item>
<title><%= posts[postID]["title"] %></title>
<link><%= config.site_url %>/post/<%= postID %></link>
<description><![CDATA[<%- func.render_md(posts[postID]["content"]) %>]]></description>
<guid isPermaLink="true"><%= config.site_url %>/post/<%= postID %></guid>
<pubDate><%= func.unix_time_to_rss_date(posts[postID]['pubdate']) %></pubDate>
<% for (let tag_index = 0; tag_index < posts[postID]['tags'].length; tag_index++) { %>
<category><![CDATA[<%= posts[postID]['tags'][tag_index] %>]]></category>
<% } %>
</item>
<% } %>
<% } %>
</channel>
</rss>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="<%= config.charset %>" ?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title><%= config.site_name %></title>
<link><%= config.site_url %></title>
<description><%= config.site_description %></description>
<updated><%= func.unix_time_to_atom_date(getUnixTime(new Date())) %></updated>
<id><%= config.site_url %></id>
<% for (let postID = posts.length-1; postID >= 0; postID--) { %>
<% if (posts[postID]["userID"] == userID) { %>
<% if (posts[postID]["deleted"] != true) { %>
<entry>
<title><%= posts[postID]["title"] %></title>
<link><%= config.site_url %>/post/<%= postID %></link>
<summary><![CDATA[<%- func.render_md(posts[postID]["content"]) %>]]></summary>
<guid isPermaLink="true"><%= config.site_url %>/post/<%= postID %></guid>
<pubDate><%# func.unix_time_to_atom_date(posts[postID]['pubdate']) %></pubDate>
<% for (let tag_index = 0; tag_index < posts[postID]['tags'].length; tag_index++) { %>
<category><![CDATA[<%= posts[postID]['tags'][tag_index] %>]]></category>
<% } %>
</entry>
<% } %>
<% } %>
<% } %>
</feed>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="<%= config.charset %>" ?>
<rss version="2.0">
<channel>
<title><%= config.site_name %></title>
<link><%= config.site_url %></title>
<description><%= config.site_description %></description>
<% for (let postID = posts.length-1; postID >= 0; postID--) { %>
<% if (posts[postID]["userID"] == userID) { %>
<% if (posts[postID]["deleted"] != true) { %>
<item>
<title><%= posts[postID]["title"] %></title>
<link><%= config.site_url %>/post/<%= postID %></link>
<description><![CDATA[<%- func.render_md(posts[postID]["content"]) %>]]></description>
<guid isPermaLink="true"><%= config.site_url %>/post/<%= postID %></guid>
<pubDate><%= func.unix_time_to_rss_date(posts[postID]['pubdate']) %></pubDate>
<% for (let tag_index = 0; tag_index < posts[postID]['tags'].length; tag_index++) { %>
<category><![CDATA[<%= posts[postID]['tags'][tag_index] %>]]></category>
<% } %>
</item>
<% } %>
<% } %>
<% } %>
</channel>
</rss>

73
webroot/default.css Normal file
View File

@@ -0,0 +1,73 @@
body {
max-width: 900px;
margin: auto;
}
#site-header {
margin-top: 10px;
text-align: center;
}
#footer {
margin-bottom: 10px;
}
.icon {
width: 15px;
}
input, textarea, button {
border: none;
border-radius: 5px;
font-size: 20px;
}
textarea {
width: 100%;
height: 250px;
field-sizing: content; /* Only supported by Chromium */
}
a {
text-decoration: none;
}
#search-form input {
border: none;
border-radius: 0px;
font-size: 14px;
}
}
a:hover {
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #333333;
color: #ffffff;
}
a {
color: #546BEB;
}
a:visited {
color: #EF46F1;
}
input, textarea, button {
background-color: #1E1E1E;
color: #ffffff;
}
hr {
color: white;
}
.comment:nth-child(even) {
background: #1E1E1E;
}
}
@media (prefers-color-scheme: light) {
input, textarea, button {
background-color: #DEDEDE;
color: #000000;
}
hr {
color: black;
}
.comment:nth-child(even) {
background: #EEEEEE;
}
}

BIN
webroot/icons/atom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

BIN
webroot/icons/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

BIN
webroot/icons/rss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B