lots of fixes and more EJS

This commit is contained in:
2025-07-23 02:20:38 +01:00
parent 929151a16d
commit 797d894621
24 changed files with 634 additions and 598 deletions

View File

@@ -4,7 +4,7 @@ Please don't use this yet, it's not finished<br/>
See the software in action: [deadvey.com](https://deadvey.com)<br/> See the software in action: [deadvey.com](https://deadvey.com)<br/>
# Confiuration # Confiuration
Read the [configuation guide](CONFIG.md) for configuration help (in config.json) Read the [configuation guide](docs/CONFIG.md) for configuration help (in config.json)
# Features # Features
* post creation via the web frontend (no need to remote to your server to make a post) * post creation via the web frontend (no need to remote to your server to make a post)
@@ -18,23 +18,21 @@ Read the [configuation guide](CONFIG.md) for configuration help (in config.json)
* Commenting on posts * Commenting on posts
* sign up and delete account * sign up and delete account
* ejs * ejs
* custom CSS _file_
# Bugs # Bugs
* probably scales like shit * probably scales like shit
* probably insecure as hell * probably insecure as hell
# Planned features/todo list # Planned features/todo list
* custom CSS _file_ * URGENT give each post and user a hard postID to prevent potential issues
* custom strings use format indicators * edit user (could be on instead of the delete_account page)
* seperate functions into modules
* user specific RSS feeds * user specific RSS feeds
* atom * atom
* federation (looks tricky) * federation (looks tricky)
* All strings (including in edit and post page) customisable * All strings (including in edit and post page) customisable
* formatable custom strings * formatable custom strings
* split code into files to tidy it up a bit
* inline comments and docs * inline comments and docs
* give each post a hard postID to prevent potential issues
* clean up code a bit * clean up code a bit
* comment pages? * comment pages?

552
app.js
View File

@@ -1,552 +0,0 @@
// Get the libraries
const fs = require('fs'); // For modifying and reading files
const express = require('express'); // For running a webserver in nodejs
const showdown = require('showdown') // For converting markdown to html on demand, https://showdownjs.com/
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")
// There's only one possible argument, so we can just check if the user passed that one
// TODO I plan on adding more such as --help and --post so I should make this more robust at some point
if (process.argv[2] == "--first-time") {
initialise() // Creates any files such users.js, posts.js, comments.js or config.js if they are not present
}
// Define the modules now so they are global
let users // contains a list of users, each user is an object containing username,prettyname,hash and description
let posts // contains a list of posts,
let comments // contains a list of comments
let config // contains a set of configuration for the site, see example-config.js for an example
let ejs_templates
try {
// We're going to try and import the modules,
users = require('./users.json');
posts = require('./posts.json');
comments = require('./comments.json');
config = require('./config.json');
ejs_templates = require("./ejs-templates.js")
}
catch (error) {
// if they don't all import then
// inform the user to pass --first-time and exit with an error code
console.log("A file is missing!")
console.log("Run with --first-time to initialise the program")
console.log(error)
process.exit(1)
}
// https://showdownjs.com/docs/available-options
let converter = new showdown.Converter({
simpleLineBreaks: true, // Parse line breaks as <br/> in paragraphs (GitHub-style behavior).
tables: true, // Enable support for tables syntax.
strikethrough: true, // Enable support for strikethrough: ~~text~~
tasklists: true, // Enable support for GitHub style tasklists. - [x] and - [ ]
encodeEmails: true, //Enable automatic obfuscation of email addresses. emails are encoded via character entities
headerLevelStart: 3, //Set starting level for the heading tags.
})
// The footer div is globale because it's a site wide, so define it here
let footer_div = config.site_wide_footer
footer_div = replace_format_indicators(footer_div)
// 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');
// Initialise the program by creating users.js, comments.js, posts.js and config.js
// All require default content in them to start off with
// Then exit successfully
// returns nothing
function initialise() {
try {
const users = require("./users.js");
}
catch (error) {
console.log("Creating users file")
fs.writeFileSync(`${__dirname}/users.json`, `{\n"users": []\n}`)
}
try {
const posts = require("./posts.json");
}
catch (error) {
console.log("Creating posts file")
fs.writeFileSync(`${__dirname}/posts.json`, `{\n"posts": []\n}`)
}
try {
const comments = require("./comments.json");
}
catch (error) {
console.log("Creating comments file")
fs.writeFileSync(`${__dirname}/comments.json`, `{\n"comments": [],\n"counter": 0}`)
}
try {
const config = require("./config.js");
}
catch (error) {
console.log("Copying the example config to config.js")
console.log("!!! PLEASE MODIFY config.js TO YOUR NEEDS !!!")
fs.copyFile('example-config.js', 'config.js', (err) => {
console.log("Error copying file")
})
}
console.log("Successfully initialised")
process.exit(0)
}
// 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
function get_userID(username) {
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 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)
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}`
}
// 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
function hyperlink_tags(tags) {
string = "" // Initialises the string
for (let tag_index = 0; tag_index < tags.length; tag_index++) { // Loop over each tag
string += `<a href="/tag/${tags[tag_index]}">${tags[tag_index]}</a>` // Adds the tag to the string as a HTML href
if (tag_index < tags.length - 1) { // If there are more tags, then insert a comma
string += ", ";
}
}
return string
}
// See the readme format indicators section for a full list of format indicators
// This function replaces the format indicators in a template to the content they represent
// accepts the template (string),
// the post index (int) as an optional paramter to indicate what post is to be used (for replacing things like content and titles of posts)
// the tag (strig) as an optional parameter to indicate what tag is being used (for /tag/:tag pages)
// the user index (int) is an optional parameter to indicate what user is to be used (for replacng things like the header of the user page)
// returns the template with it's format indiactors replaced (string)
function replace_format_indicators(template, post_index=-1, tag_name="tag", user_index=-1) {
output_string = template // These should always be replaceable
.replaceAll("%%", "&#37;")
.replaceAll("%J", "/delete_account")
.replaceAll("%P", "/post")
.replaceAll("%O", `/edit/${post_index}`)
.replaceAll("%Q", "/signup")
.replaceAll("%R", "/rss")
.replaceAll("%Y", config.site_name)
.replaceAll("%W", config.site_description)
.replaceAll("%Z", config.attribution)
.replaceAll("%S", config.seperator)
if (post_index >= 0) { // These can only be replaced if a post is specified (by default the post id is -1)
post_object = posts[post_index] // Defines the post object for easy reference
output_string = output_string
.replaceAll("%A", (post_object["tags"]))
.replaceAll("%B", (hyperlink_tags(post_object["tags"])))
.replaceAll("%C", converter.makeHtml(post_object["content"]))
//.replaceAll("%D", unix_time_to_date_format(post_object["pubdate"]))
//.replaceAll("%E", unix_time_to_date_format(post_object["editdate"]))
.replaceAll("%F", users[post_object["userID"]]['prettyname'])
.replaceAll("%G", tag_name)
.replaceAll("%I", converter.makeHtml(users[post_object['userID']]['description']))
.replaceAll("%L", `/post/${post_index}`)
.replaceAll("%M", return_comments(post_index))
.replaceAll("%N", users[post_object["userID"]]['username'])
.replaceAll("%S", config.seperator)
.replaceAll("%T", post_object["title"])
.replaceAll("%U", `/user/${users[post_object["userID"]]['username']}`)
.replaceAll("%X", `<form method="POST" action="/submit_comment">
<input type="hidden" name="post_index" value="${post_index}">
<input placeholder="username" name="name"><br/>
<textarea placeholder="comment" name="content"></textarea><br/>
<button type="submit">Submit</button>
</form>`)
}
if (user_index >= 0) { // these should only be replaced if a user is specified (by default the user id is -1)
output_string = output_string
.replaceAll("%F", users[user_index]['prettyname'])
.replaceAll("%G", tag_name)
.replaceAll("%I", converter.makeHtml(users[user_index]['description']))
.replaceAll("%L", `/post/${post_index}`)
.replaceAll("%N", users[user_index]['username'])
.replaceAll("%S", config.seperator)
.replaceAll("%U", `/user/${users[user_index]['username']}`)
}
if (config.enable_hitcount == true) { // Finally, the hitcounter should only be replaced if config.enable_hitcount is true
output_string = output_string
.replaceAll("%H", fs.readFileSync('hitcount.txt'))
}
return output_string
}
// 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
function escape_input(input) {
let output = input
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\\", "&#92;")
.replaceAll('"', "&#34;")
.replaceAll("'", "&#39;")
.replaceAll("/", "&#47;")
.replaceAll("%", "&#37;")
return output
}
// TODO make the formatting customisable
function return_comments(post_id) {
const post_comments = comments.comments[post_id]
let comment_content = ""
for (let comment_index = 0; comment_index < post_comments.length; comment_index++) {
let comment = {...post_comments[comment_index]};
comment['content'] = comment['content']
.replaceAll(/>> ([0-9]*)/g, "<a href='#$1'>>> $1</a>")
.replaceAll(/&gt;&gt; ([0-9]*)/g, "<a href='#$1'>>> $1</a>")
.replaceAll("\n", "<br/>")
//comment_content += `<div id="${comment['id']}">${comment['name']} ${unix_time_to_date_format(comment['pubdate'])} No. ${comment['id']}<br/>${comment['content']}</div><br/>`
}
return comment_content
}
// RSS protocol gets
app.get(config.rss_url, (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.length-1; i >= 0; i--) {
rss_content += `
<item>
<title>${posts[i]["title"]}</title>
<link>${config.site_url}/post/${i}</link>
<description><![CDATA[${converter.makeHtml(posts[i]["content"])}]]></description>
<guid isPermaLink="true">${config.site_url}/post/${i}</guid>
<pubDate>${unix_time_to_rss_date(posts[i]['pubdate'])}</pubDate>`
for (let j = 0; j < posts[i]['tags'].length; j++) {
rss_content += `<category><![CDATA[${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) => {
// Increment the hitcount
if (config.enable_hitcount) {
let hitcount = parseInt(fs.readFileSync('hitcount.txt'))
hitcount += 1
console.log(`/ Is loaded, hitcount: ${hitcount}`)
fs.writeFileSync(`${__dirname}/hitcount.txt`, `${hitcount}`, 'utf-8');
}
res.render("pages/timeline",
{
config: config,
posts: posts,
users: users,
comments: comments.comments,
hitcount: fs.readFileSync("hitcount.txt"),
fromUnixTime: fromUnixTime,
format: format,
getUnixTime: getUnixTime,
hyperlink_tags: hyperlink_tags,
})
}); // /
app.get("/user/:username", (req, res) => {
const userID = get_userID(req.params.username)
console.log(userID)
console.log(users[userID].prettyname)
res.render("pages/user",
{
config: config,
posts: posts,
user: users[userID],
userID: userID,
comments: comments.comments,
fromUnixTime: fromUnixTime,
format: format,
getUnixTime: getUnixTime,
hyperlink_tags: hyperlink_tags,
})
}); // /user/:username
app.get("/post/:post_index", (req, res) => {
const postID = req.params.post_index
res.render("pages/post",
{
config,
post: posts[postID],
postID: postID,
user: users[posts[postID].userID],
comments: comments.comments[postID],
fromUnixTime,
format,
getUnixTime,
hyperlink_tags,
})
}); // /post/:post_index
app.get("/tag/:tag", (req,res) => {
const tag = req.params.tag
res.render("pages/tag",
{
config: config,
tag: tag,
posts: posts,
users: users,
comments: comments.comments,
fromUnixTime: fromUnixTime,
format: format,
getUnixTime: getUnixTime,
hyperlink_tags: hyperlink_tags,
})
}); // /tag/:tag
app.get(config.new_post_url, (req,res) => {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><form action="/submit_post" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<input placeholder="title" required name="title"><br/>
<textarea placeholder="post content*" required name="content"></textarea><br/>
<input placeholder="Tags (comma seperated)" name="tags"><br/>
<input type="submit" value="Submit"><br/>
<small>* Markdown supported</small>
</form></html>`);
}); // /post
app.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.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><form action="/submit_signup" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="prettyname" required name="prettyname"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<textarea placeholder="description (social links, what you do etc), supports markdown" id="description" name="description"></textarea><br/>
<label>${config.signup_agreement}: </label><input type="checkbox" name="agreement" required><br/>
<input type="submit" value="Submit"><br/></form></html>`);
}
// if the server does not allow signup
else if (config.allow_signup == false) {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.signups_unavailable}</body></html>`)
}
// If allow_signup is undefined or not a boolean, error
else {
res.redirect(301,"/")
console.log("Error, invalid value for allow_signup (bool)")
}
}); // /signup
app.get(config.delete_account_url, (req,res) => {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><form action="/submit_delete_account" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<label>${config.delete_account_confirmation}: </label><input type="checkbox" name="agreement" required><br/>
<input type="submit" value="Submit"><br/></form></html>`);
}); // /delete_account
app.get(`${config.edit_post_base_url}/:post_id`, (req,res) => {
const post_id = req.params.post_id
const post = posts[post_id]
const user = 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}">
<input placeholder="${user['prettyname']}'s password" type="password" required id="password" name="password"><br/>
<input placeholder="title" value="${post['title']}" required name="title"><br/>
<textarea placeholder="content" required name="content">${post['content']}</textarea><br/>
<input placeholder="tags (comma seperated)" value="${post['tags']}" name="tags"><br/>
<label>Delete forever (no undo): </label><input name="delete" type="checkbox"><br/>
<input type="submit" value="Submit"><br/>
<small>* Markdown supported</small>
</form></html>`);
}); // /edit/:post_id
app.post("/submit_comment", (req,res) => {
const unix_timestamp = getUnixTime(new Date())
let name = escape_input(req.body.name)
if (name == "") {
name = config.default_commenter_username
}
new_comment = {
"name": name,
"content": escape_input(req.body.content),
"id": comments.counter,
"pubdate": unix_timestamp
};
let counter = comments.counter+1;
comments.comments[req.body.post_index].push(new_comment);
fs.writeFileSync(`${__dirname}/comments.json`, `${JSON.stringify(comments)}`, 'utf-8');
res.redirect(301,`/post/${req.body.post_index}`)
}); // /submit_comment
app.post("/submit_post", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const username = escape_input(req.body.username)
const title = escape_input(req.body.title)
const content = escape_input(req.body.content)
const tags = escape_input(req.body.tags).split(',');
const unix_timestamp = getUnixTime(new Date())
if (get_userID(username) == -1) {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.user_doesnt_exit}</body></html>`)
}
else if (users[get_userID(username)]['hash'] == password) { // Password matches
console.log(username, "is submitting a post titled:", title);
posts.push({
"userID": get_userID(username),
"title": title,
"content": content,
"pubdate": unix_timestamp,
"editdate": unix_timestamp,
"tags": tags,
})
fs.writeFileSync(`${__dirname}/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
comments.comments.push([])
fs.writeFileSync(`${__dirname}/comments.json`, `${JSON.stringify(comments)}`)
res.redirect(302, "/");
}
else {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.incorrect_password}</body></html>`)
}
}); // /submit_post
app.post("/submit_signup", (req,res) => {
const password = crypto.createHash('sha512').update(req.body.password).digest('hex');
const username = escape_input(req.body.username)
const prettyname = escape_input(req.body.prettyname)
const description = escape_input(req.body.description)
// Check that signups are allowed
if (config.allow_signup == true) {
// get_userID will return -1 if the user does not exist
// so this checks that the user does not exist
if (get_userID(username) == -1) {
users.push({
"username": username,
"prettyname": prettyname,
"hash": password,
"description": description,
})
fs.writeFileSync(`${__dirname}/users.json`, `${JSON.stringify(users)}`, 'utf-8');
res.redirect(301, `/user/${username}`)
}
// if the user does exist then
else {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.user_exists}</body></html>`)
}
}
else if (config.allow_signup == false) {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.signups_unavailable}</body></html>`)
}
// 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
app.post("/submit_delete_account", (req,res) => {
// Get the form info
const password = crypto.createHash("sha512").update(req.body.password).digest("hex");
const username = escape_input(req.body.username)
// get the userID
const userID = get_userID(username)
if (userID >= 0) { // The user exists
if (password == users[userID]['hash']) { // password matches
console.log(username, "(userID:", userID, ") is trying deleting their account")
// Delete the user
users.splice(userID,1)
// 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.splice(postid,1) // delete the post
comments.comments.splice(postid,1) // the comments for this post should also be delete
}
};
// Write these changes
fs.writeFileSync(`${__dirname}/users.json`, `${JSON.stringify(users)}`, 'utf-8');
fs.writeFileSync(`${__dirname}/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
fs.writeFileSync(`${__dirname}/comments.json`, `${JSON.stringify(comments.comments)}\nexport const counter = ${comments.counter}`, 'utf-8');
res.redirect(301,"/")
}
else { // password does not match
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.incorrect_password}</body></html>`)
};
}
else {
res.send(`</html><head><meta charset="${config.charset}"><style>${config.css}</style></head><body>${config.user_doesnt_exist}</body></html>`)
}
}); // /submit_delete_account
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[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.splice(postID,1)
comments.comments.splice(postID,1)
fs.writeFileSync(`${__dirname}/comments.json`, `${JSON.stringify(comments.comments)}\nexport const counter = ${comments.counter}`, 'utf-8');
}
fs.writeFileSync(`${__dirname}/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
res.redirect(302, "/");
}
else {
res.send(`Invalid Password for user`,users[userID]['prettyname']);
}
}); // /submit_edit
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

@@ -1 +0,0 @@
example-custom.css

33
data/example-config.json Executable file
View File

@@ -0,0 +1,33 @@
{
"seperator": "<hr/>",
"site_name": "My Blog",
"site_url": "https://example.com",
"language": "en",
"port": 8080,
"allow_signup": true,
"site_description": "Read my blogs!",
"timeline_length": 20,
"enable_hitcount": true,
"charset": "UTF-8",
"root_path": "/home/deadvey/code/web/blogger-webroot/",
"delete_account_url": "/delete_account",
"new_post_url": "/post",
"signup_url": "/signup",
"edit_post_base_url": "/edit",
"default_comenter_username": "Anon",
"rss": true,
"rss_url": "/rss",
"date_format": "yyyy-MM-dd",
"time_zone": "+0000",
"string": {
"signup_agreement": "I agree to not post illegal or hateful content",
"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",
"delete_account_confirmation": "I agree that my account and all of my posts will be permanently deleted instantly",
"incorrect_password": "Incorrect Password",
"rss_disabled": "Sorry, RSS is disabled",
"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>"
},
"css": ""
}

View File

@@ -46,7 +46,8 @@ Read more at [date-fns](https://date-fns.org/v4.1.0/docs/format)<br/>
## Advanced Customisation ## 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. * /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/> * "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. 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.
## Custom Strings ## Custom Strings
All of these values are of type String All of these values are of type String

View File

@@ -1 +0,0 @@
export let timeline = '<html><head><meta charset="<%=charset%>"><style></style></head><body><div id="header">${header_div}</div><div id="posts">${posts_div}</div></body><footer>${footer_div}</footer></html>'

View File

@@ -2,13 +2,14 @@
"seperator": "<hr/>", "seperator": "<hr/>",
"site_name": "My Blog", "site_name": "My Blog",
"site_url": "https://example.com", "site_url": "https://example.com",
"language": "en",
"port": 8080, "port": 8080,
"allow_signup": true, "allow_signup": true,
"site_description": "Read my blogs!", "site_description": "Read my blogs!",
"timeline_length": 20, "timeline_length": 20,
"enable_hitcount": true, "enable_hitcount": true,
"charset": "UTF-8", "charset": "UTF-8",
"root_path": "/path/to/root/of/website", "root_path": "/path/to/blogger-webroot",
"delete_account_url": "/delete_account", "delete_account_url": "/delete_account",
"new_post_url": "/post", "new_post_url": "/post",
"signup_url": "/signup", "signup_url": "/signup",
@@ -18,20 +19,15 @@
"rss_url": "/rss", "rss_url": "/rss",
"date_format": "yyyy-MM-dd", "date_format": "yyyy-MM-dd",
"time_zone": "+0000", "time_zone": "+0000",
"timeline_header": "<h1>%Y</h1><h2>%W</h2><a href='%P'>Create Post</a><br/><a href='%R'>RSS Feed</a><br/><a href='%Q'>Sign Up</a><br/><a href='%D'>Delete Account</a><br/>Hit count: %H%S", "string": {
"user_page_header": "<h1>%F's posts:</h1>%I%S", "signup_agreement": "I agree to not post illegal or hateful content",
"tag_page_header": "<h1>Posts tagged: %G</h1>%S", "signups_unavailable": "Sorry, this server does not allow signups",
"user_post_format": "<h2>%T</h2><p>%C</p><i>%B</i><br/><a href='%L'>Permalink</a><br/>%X%M%S", "user_exists": "Sorry, this user already exists, try a different username",
"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>%S%X%M%S", "user_doesnt_exist": "Sorry, this user does not exist",
"timeline_post_format": "<h3>%T</h3><p>%C</p><a href='%L'>Permalink</a><br/><i>By <a href='%U'>%N</a></i>%X%M%S", "delete_account_confirmation": "I agree that my account and all of my posts will be permanently deleted instantly",
"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", "incorrect_password": "Incorrect Password",
"site_wide_footer": "Site is ran by DeaDvey<br/>%Z", "rss_disabled": "Sorry, RSS is disabled",
"signup_agreement": "I agree to not post illegal or hateful content", "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>"
"signups_unavailable": "Sorry, this server does not allow signups", },
"user_exists": "Sorry, this user already exists, try a different username", "css": ""
"user_doesnt_exist": "Sorry, this user does not exist",
"delete_account_confirmation": "I agree that my account and all of my posts will be permanently deleted instantly",
"incorrect_password": "Incorrect Password",
"css": "",
"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>"
} }

View File

@@ -1,3 +0,0 @@
body {
/* Put your custom css here */
}

View File

@@ -1,10 +0,0 @@
// 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, format) {
date = fromUnixTime(unix_time)
formatted_date = format(date, format)
return formatted_date
}

71
src/functions.js Normal file
View File

@@ -0,0 +1,71 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url)
// 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
const config = require("../config.json")
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
const config = require("../config.json")
let date = fromUnixTime(unix_time)
let formatted_date = format(date, "EEE, dd MMM yyyy HH:mm:ss")
return `${formatted_date} ${config.time_zone}`
}
// 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 hyperlink_tags(tags) {
let string = "" // Initialises the string
for (let tag_index = 0; tag_index < tags.length; tag_index++) { // Loop over each tag
string += `<a href="/tag/${tags[tag_index]}">${tags[tag_index]}</a>` // Adds the tag to the string as a HTML href
if (tag_index < tags.length - 1) { // If there are more tags, then insert a comma
string += ", ";
}
}
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("../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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\\", "&#92;")
.replaceAll('"', "&#34;")
.replaceAll("'", "&#39;")
.replaceAll("/", "&#47;")
.replaceAll("%", "&#37;")
return output
}

39
src/initialise.js Normal file
View File

@@ -0,0 +1,39 @@
// Initialise the program by creating users.js, comments.js, posts.js and config.js
// All require default content in them to start off with
// Then exit successfully
// returns nothing
function initialise() {
try {
const users = require("../data/users.js");
}
catch (error) {
console.log("Creating users file")
fs.writeFileSync(`../data/users.json`, `{\n"users": []\n}`)
}
try {
const posts = require("../data/posts.json");
}
catch (error) {
console.log("Creating posts file")
fs.writeFileSync(`../data/posts.json`, `{\n"posts": []\n}`)
}
try {
const comments = require("../data/comments.json");
}
catch (error) {
console.log("Creating comments file")
fs.writeFileSync(`../data/comments.json`, `{\n"comments": [],\n"counter": 0}`)
}
try {
const config = require("../data/config.js");
}
catch (error) {
console.log("Copying the example config to config.js")
console.log("!!! PLEASE MODIFY config.js TO YOUR NEEDS !!!")
fs.copyFile('example-config.js', 'config.js', (err) => {
console.log("Error copying file")
})
}
console.log("Successfully initialised")
process.exit(0)
}

389
src/server.js Normal file
View File

@@ -0,0 +1,389 @@
// Get the libraries
const fs = require('fs'); // For modifying and reading files
const express = require('express'); // For running a webserver in nodejs
const showdown = require('showdown') // For converting markdown to html on demand, https://showdownjs.com/
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 init = require("./initialise.js")
// There's only one possible argument, so we can just check if the user passed that one
// TODO I plan on adding more such as --help and --post so I should make this more robust at some point
if (process.argv[2] == "--first-time") {
init.initialise() // Creates any files such users.js, posts.js, comments.js or config.js if they are not present
}
// Define the modules now so they are global
let users // contains a list of users, each user is an object containing username,prettyname,hash and description
let posts // contains a list of posts,
let comments // contains a list of comments
let config // contains a set of configuration for the site, see example-config.js for an example
try {
// We're going to try and import the modules,
users = require('../data/users.json');
posts = require('../data/posts.json');
comments = require('../data/comments.json');
config = require('../config.json');
}
catch (error) {
// if they don't all import then
// inform the user to pass --first-time and exit with an error code
console.log("A file is missing!")
console.log("Run with --first-time to initialise the program")
console.log(error)
process.exit(1)
}
// https://showdownjs.com/docs/available-options
let converter = new showdown.Converter({
simpleLineBreaks: true, // Parse line breaks as <br/> in paragraphs (GitHub-style behavior).
tables: true, // Enable support for tables syntax.
strikethrough: true, // Enable support for strikethrough: ~~text~~
tasklists: true, // Enable support for GitHub style tasklists. - [x] and - [ ]
encodeEmails: true, //Enable automatic obfuscation of email addresses. emails are encoded via character entities
headerLevelStart: 3, //Set starting level for the heading tags.
})
// 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')
////////////////////// SYNDICATION ////////////////////////
// RSS protocol gets
app.get(config.rss_url, (req,res) => {
if (config.rss == false) {
res.render("partials/message", {
message: config.string.rss_disabled,
config: config,
})
}
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.length-1; i >= 0; i--) {
rss_content += `
<item>
<title>${posts[i]["title"]}</title>
<link>${config.site_url}/post/${i}</link>
<description><![CDATA[${converter.makeHtml(posts[i]["content"])}]]></description>
<guid isPermaLink="true">${config.site_url}/post/${i}</guid>
<pubDate>${func.unix_time_to_rss_date(posts[i]['pubdate'])}</pubDate>`
for (let j = 0; j < posts[i]['tags'].length; j++) {
rss_content += `<category><![CDATA[${posts[i]['tags'][j]}]]></category>`
};
rss_content += "</item>"
}
rss_content += `
</channel>
</rss>`
res.setHeader('content-type', 'application/rss+xml');
res.send(rss_content)
};
});
///////////////////// Standard Pages //////////////////////
app.get("/", (req,res) => {
// Increment the hitcount
if (config.enable_hitcount) {
let hitcount = parseInt(fs.readFileSync('../data/hitcount.txt'))
hitcount += 1
console.log(`/ Is loaded, hitcount: ${hitcount}`)
fs.writeFileSync(`../data/hitcount.txt`, `${hitcount}`, 'utf-8');
}
res.render("pages/timeline",
{
config,
posts,
users,
comments: comments.comments,
hitcount: fs.readFileSync("../data/hitcount.txt"),
fromUnixTime,
format,
getUnixTime,
func,
})
}); // /
app.get("/user/:username", (req, res) => {
const userID = func.get_userID(req.params.username)
console.log(userID)
console.log(users[userID].prettyname)
res.render("pages/user",
{
config: config,
posts: posts,
user: users[userID],
userID: userID,
comments: comments.comments,
fromUnixTime: fromUnixTime,
format: format,
getUnixTime: getUnixTime,
func,
})
}); // /user/:username
app.get("/post/:post_index", (req, res) => {
const postID = req.params.post_index
res.render("pages/post",
{
config,
post: posts[postID],
postID: postID,
user: users[posts[postID].userID],
comments: comments.comments[postID],
fromUnixTime,
format,
getUnixTime,
func,
})
}); // /post/:post_index
app.get("/tag/:tag", (req,res) => {
const tag = req.params.tag
res.render("pages/tag",
{
config: config,
tag: tag,
posts: posts,
users: users,
comments: comments.comments,
fromUnixTime: fromUnixTime,
format: format,
getUnixTime: getUnixTime,
func,
})
}); // /tag/:tag
///////////////////// Form pages ////////////////////////////
app.get(config.new_post_url, (req,res) => {
res.render("forms/new_post", {
config
});
}); // /post
app.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
});
}
// if the server does not allow signup
else if (config.allow_signup == false) {
res.render("partials/message", {
message: config.string.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
app.get(config.delete_account_url, (req,res) => {
res.render("forms/delete_account", { config });
}); // /delete_account
app.get(`${config.edit_post_base_url}/:post_id`, (req,res) => {
const post_id = req.params.post_id
const post = posts[post_id]
const user = users[post['userID']]
res.render("forms/edit_post", {
config,
post,
post_id,
user,
});
}); // /edit/:post_id
////////////////////// Form actions /////////////////////////
app.post("/submit_comment", (req,res) => {
const unix_timestamp = getUnixTime(new Date())
let name = func.escape_input(req.body.name)
if (name == "") {
name = config.default_commenter_username
}
new_comment = {
"name": name,
"content": func.escape_input(req.body.content),
"id": comments.counter,
"pubdate": unix_timestamp
};
let counter = comments.counter+1;
comments.comments[req.body.post_index].push(new_comment);
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`, 'utf-8');
res.redirect(301,`/post/${req.body.post_index}`)
}); // /submit_comment
app.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 = func.escape_input(req.body.content)
const tags = func.escape_input(req.body.tags).split(',');
const unix_timestamp = getUnixTime(new Date())
if (func.get_userID(username) == -1) {
res.render("partials/message", {
message: config.string.user_doesnt_exit,
config,
})
}
else if (users[func.get_userID(username)]['hash'] == password) { // Password matches
console.log(username, "is submitting a post titled:", title);
posts.push({
"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.comments.push([])
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`)
res.redirect(302, "/");
}
else {
res.render("partials/message", {
message: config.string.incorrect_password,
config,
})
}
}); // /submit_post
app.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 = func.escape_input(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({
"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: config.string.user_exists,
config,
})
}
}
else if (config.allow_signup == false) {
res.render("partials/message", {
message: config.string.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
app.post("/submit_delete_account", (req,res) => {
// Get the form info
const password = crypto.createHash("sha512").update(req.body.password).digest("hex");
const username = func.escape_input(req.body.username)
// get the userID
const userID = func.get_userID(username)
if (userID >= 0) { // The user exists
if (password == users[userID]['hash']) { // password matches
console.log(username, "(userID:", userID, ") is trying deleting their account")
// Delete the user
users.splice(userID,1)
// 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.splice(postid,1) // delete the post
comments.comments.splice(postid,1) // the comments for this post should also be delete
}
};
// 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.comments)}\nexport const counter = ${comments.counter}`, 'utf-8');
res.redirect(301,"/")
}
else { // password does not match
res.render("partials/message", {
message: config.string.incorrect_password,
config
}
)
};
}
else {
res.render("partials/message", {
message: config.string.user_doesnt_exist,
config,
})
}
}); // /submit_delete_account
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[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.splice(postID,1)
comments.comments.splice(postID,1)
fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments.comments)}\nexport const counter = ${comments.counter}`, 'utf-8');
}
fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8');
res.redirect(302, "/");
}
else {
res.render("partials/message", {
message: config.string.incorrect_password,
config,
})
}
}); // /submit_edit
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,14 @@
<!DOCTYPE html>
<html lang="<%= config.language %>
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_delete_account" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<label><%- config.string.delete_account_confirmation %>: </label><input type="checkbox" name="agreement" required><br/>
<input type="submit" value="Submit"><br/>
</form>
</body>
</html>

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

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="<%= config.language %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_edit" method="POST" onsubmit="sha512password()">
<input name="userID" type="hidden" value="<%= post['userID'] %>">
<input name="postID" type="hidden" value="<%= post_id %>">
<input placeholder="<%= user['prettyname'] %>'s password" type="password" required id="password" name="password"><br/>
<input placeholder="title" value=" <%=post['title'] %>" required name="title"><br/>
<textarea placeholder="content" required name="content"><%= post['content'] %></textarea><br/>
<input placeholder="tags (comma seperated)" value="<%= post['tags'] %>" name="tags"><br/>
<label>Delete forever (no undo): </label><input name="delete" type="checkbox"><br/>
<input type="submit" value="Submit"><br/>
<small>* Markdown supported</small>
</form>
</body>
</html>

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

@@ -0,0 +1,16 @@
<!DOCTYPE html>
</html>
<head>
<%- include('../partials/head.ejs') %>
</head>
<body>
<form action="/submit_post" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<input placeholder="title" required name="title"><br/>
<textarea placeholder="post content*" required name="content"></textarea><br/>
<input placeholder="Tags (comma seperated)" name="tags"><br/>
<input type="submit" value="Submit"><br/>
<small>* Markdown supported</small>
</body>
</form></html>

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

@@ -0,0 +1,16 @@
<!DOCTYPE html>
</html lang="<%= config.language %>">
<head>
<%- include("../partials/head") %>
</head>
<body>
<form action="/submit_signup" method="POST">
<input placeholder="username" required name="username"><br/>
<input placeholder="prettyname" required name="prettyname"><br/>
<input placeholder="password" type="password" required id="password" name="password"><br/>
<textarea placeholder="description (social links, what you do etc), supports markdown" id="description" name="description"></textarea><br/>
<label><%- config.string.signup_agreement %>: </label><input type="checkbox" name="agreement" required><br/>
<input type="submit" value="Submit"><br/>
</form>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<%- config.site_description %> <%- config.site_description %>
</h2> </h2>
<a href="<%= config.rss_url %>">RSS Feed</a><br/> <a href="<%= config.rss_url %>">RSS Feed</a><br/>
<a href="<%= config.new_post_url %>">New post</a><br/>
<a href="<%= config.signup_url %>">Sign Up</a><br/> <a href="<%= config.signup_url %>">Sign Up</a><br/>
<a href="<%= config.delete_account_url %>">Delete Account</a><br/> <a href="<%= config.delete_account_url %>">Delete Account</a><br/>
<% if (config.enable_hitcount == true) { %> <% if (config.enable_hitcount == true) { %>

View File

@@ -1,2 +1,2 @@
<b><%= comment.name %></b> <%= format(fromUnixTime(comment.pubdate), config.date_format) %> <i>No. <%= comment.id %></i>:<br/> <b><%= comment.name %></b> <%= func.unix_time_to_date_format(comment.pubdate) %> <i>No. <%= comment.id %></i>:<br/>
<%= comment.content %> <%= comment.content %>

View File

@@ -1,2 +1,2 @@
Site is ran by deadvey<br/> Site is ran by deadvey<br/>
<%- config.attribution %> <%- config.string.attribution %>

View File

@@ -5,4 +5,4 @@
</style> </style>
<link rel="stylesheet" href="custom.css"> <link rel="stylesheet" href="/custom.css">

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="<%=config.language%>
<head>
<%- include('head') %>
</head>
<body>
<%- message %>
</body>
</html>

View File

@@ -6,10 +6,10 @@
By <a href="/user/<%= user.username %>"><%= user.username %></a><br/> By <a href="/user/<%= user.username %>"><%= user.username %></a><br/>
</i> </i>
<br/> <br/>
<%- hyperlink_tags(post.tags) %><br/> <%- func.hyperlink_tags(post.tags) %><br/>
<a href="<%= config.edit_post_base_url %>/<%= postID %>">Edit</a><br/> <a href="<%= config.edit_post_base_url %>/<%= postID %>">Edit</a><br/>
<i>Published: <%= format(fromUnixTime(post.pubdate), config.date_format) %></i><br/> <i>Published: <%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
<i>Last Modified: <%= format(fromUnixTime(post.editdate), config.date_format) %></i><br/> <i>Last Modified: <%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
<%- config.seperator %> <%- config.seperator %>

View File

@@ -3,7 +3,7 @@
</h3> </h3>
<%= post.content %><br/> <%= post.content %><br/>
<a href="/post/<%- postID %>">Permalink</a><br/> <a href="/post/<%- postID %>">Permalink</a><br/>
<%- hyperlink_tags(post.tags) %> <%- func.hyperlink_tags(post.tags) %>
<!-- Comment form --> <!-- Comment form -->
<form method="POST" action="/submit_comment"> <form method="POST" action="/submit_comment">