Compare commits

...

26 Commits

Author SHA1 Message Date
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
31 changed files with 419 additions and 114 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,8 @@ posts.json
comments.json
users.json
config.json
data.json
hitcount.txt
*.swp
webroot/
data/
images/*

View File

@@ -2,6 +2,7 @@ This software aims to provide a lot of power to the web admin who is running the
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/>
> [!CAUTION]
> This software is not finished yet, so it's very buggy and probably really insecure<br/>
@@ -24,7 +25,8 @@ Read the [configuation guide](docs/CONFIG.md) for configuration help (in config.
* hitcount
* Markdown syntax in posts
* Commenting on posts and replying to other comments
* site wide custom CSS
* site wide custom CSS and strings
* Search functionality
* Page indexes
# Bugs:
@@ -33,14 +35,15 @@ Read the [configuation guide](docs/CONFIG.md) for configuration help (in config.
# Planned features/todo list:
* federation (looks tricky)
* All strings (including in edit and post page) customisable
* formatable custom strings
* inline comments and docs
* clean up code a bit
* /postID and /userID pages
* site index
* Make EJS modification more user friendly
* 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)

View File

@@ -1 +0,0 @@
{"hitcount":27}

View File

@@ -14,13 +14,14 @@ All options show an example configuartion value and the variable type + an expla
| 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 |
## Basic Customisation
| name | example value | variable type | explanation |
|-----------------|----------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|locale|"en"|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|This is what %S represents in the formatting, this will go inbetween posts and generally to seperate out content on pages.|
|site_name|"My Blog"|String|This is what %Y represents; it's the name of your instance, a human readable string.|
|locale|"en-GB"|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.|
@@ -35,13 +36,14 @@ 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|
|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
* You can edit all the strings on the site in /locales/\<your-locale>.json

View File

@@ -5,12 +5,13 @@
"site_url": "https://example.com",
"language": "en-US",
"port": 8080,
"cache_data": false,
"allow_signup": true,
"site_description": "Read my blogs!",
"timeline_length": 20,
"enable_hitcount": true,
"charset": "UTF-8",
"root_path": "/path/to/webroot",
"root_path": "../webroot/",
"edit_account_base_url": "/edit_account",
"new_post_url": "/post",
"signup_url": "/signup",
@@ -20,17 +21,5 @@
"atom": true,
"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",
"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",
"delete_account_confirmation": "Delete my account - (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

@@ -1,22 +1,86 @@
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', postID);
if (typeof post.hitcount != 'undefined') {
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(data, index=-1) {
if (config["data_storage"] == "json") {
if (data == "posts" || data == 'users' || data == 'comments') {
let result = require(`../data/${data}.json`)
let result = func.require_module(`../data/${data}.json`)
if (index != -1) {
if (index < result.length) {
return result[index]
}
else {
return 1 // This index doesn't exist
}
}
return result
}
else if (data == "other_data") {
let result = require('../data/data.json') // This file is actually called data.json
return result
else if (data == "hitcount") {
let result = func.require_module('../data/data.json') // This file is actually called data.json
return result["hitcount"]
}
else {
console.log("Error, invalid requested")
@@ -24,6 +88,7 @@ export function getdata(data, index=-1) {
}
}
// NOT YET WORKING!
if (config["data_storage"] == "mysql") {
const mysql = require('mysql');
let con = mysql.createConnection({
@@ -39,6 +104,7 @@ export function getdata(data, index=-1) {
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;
});
@@ -46,6 +112,7 @@ export function getdata(data, index=-1) {
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;
});
@@ -54,3 +121,30 @@ export function getdata(data, index=-1) {
});
}
}
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
}
}
}

View File

@@ -4,6 +4,19 @@ 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
@@ -86,8 +99,8 @@ export function escape_input(input) {
}
// Render comment content by replacing the >> int with a url link to that comment
// Syntax: ">> postID-commentID"
export function render_comment(comment_content) {
console.log(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>")
@@ -96,9 +109,11 @@ export function render_comment(comment_content) {
.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({
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,
@@ -109,12 +124,3 @@ export function render_md(content) {
return md.render(content)
};
// Literally just +1 to the hitcount
export function increment_hitcount() {
if (config.data_storage == 'json') {
let other_data = require('../data/data.json');
other_data.hitcount += 1
console.log(`/ Is loaded, hitcount: ${other_data.hitcount}`)
fs.writeFileSync(`../data/data.json`, `${JSON.stringify(other_data)}`, 'utf-8');
}
};

View File

@@ -32,7 +32,7 @@ export function initialise() {
}
catch (error) {
console.log("Creating generic data file")
fs.writeFileSync(`../data/data.json`, `{"hitcount": 0, "comment_counter": 0}`)
fs.writeFileSync(`../data/data.json`, `{"hitcount": 0}`)
}
try {
const config = require("../config.json");

View File

@@ -200,4 +200,26 @@ router.post("/submit_edit_post", (req,res) => {
}
}); // /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('partials/search', {
config,
locale,
search_results,
search_term,
search_type,
})
}); // /search
module.exports = router;

View File

@@ -38,19 +38,19 @@ router.get(`${config.edit_account_base_url}/:user_id`, (req,res) => {
res.render("forms/edit_account", {
config,
locale,
user: users[userID],
user: data.getdata('users', userID),
userID
});
}); // /delete_account
router.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']]
const postID = req.params.post_id
const post = data.getdata('posts', postID)
const user = data.getdata('users', post.userID)
res.render("forms/edit_post", {
config,
locale,
post,
post_id,
postID,
user,
});
}); // /edit/:post_id

View File

@@ -9,9 +9,9 @@ const router = express.Router();
router.get("/index/pages", (req,res) => {
res.render("indexes/all_pages", {
config,
posts,
users,
comments: comments.comments,
posts: data.getdata('posts'),
users: data.getdata('users'),
comments: data.getdata('comments'),
});
}); // /index/posts
router.get("/index/posts", (req,res) => {

View File

@@ -7,10 +7,12 @@ const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date util
const router = express.Router();
///////////////////// Standard Pages //////////////////////
// Timeline
router.get("/", (req,res) => {
// Increment the hitcount
if (config.enable_hitcount) {
func.increment_hitcount()
data.increment_hitcount()
}
res.render("pages/timeline",
@@ -27,6 +29,8 @@ router.get("/", (req,res) => {
func,
})
}); // /
// Users
router.get("/user/:username", (req, res) => {
const userID = func.get_userID(req.params.username)
let user = data.getdata('users', userID)
@@ -53,11 +57,15 @@ router.get("/user/:username", (req, res) => {
})
}
}); // /user/:username
router.get("/post/:post_index", (req, res) => {
const postID = req.params.post_index
let post = data.getdata('posts', postID)
if (post["deleted"] == true || post == 1) { // data.getdata returns error code 1 if nothing is available
// Posts
router.get("/post/:post_index", (req, res) => {
const postID = parseInt(req.params.post_index)
let post = data.getdata('posts', postID)
if (config.enable_hitcount) {
data.increment_hitcount(postID)
}
if (post == 1) { // data.getdata returns error code 1 if nothing is available
res.render("partials/message", {
message: locale.post_doesnt_exist,
config,
@@ -85,6 +93,7 @@ router.get("/post/:post_index", (req, res) => {
}); // /post/:post_index
// Tags
router.get("/tag/:tag", (req,res) => {
const tag = req.params.tag
res.render("pages/tag",
@@ -101,13 +110,23 @@ router.get("/tag/:tag", (req,res) => {
func,
})
}); // /tag/:tag
// Comments
router.get("/comment/:postID-:commentID", (req,res) => {
const commentID = req.params.commentID;
const postID = req.params.postID;
const commentID = parseInt(req.params.commentID);
const postID = parseInt(req.params.postID);
let posts_comments = data.getdata('comments', postID)
let comment = posts_comments[commentID]
if (comment == -1) {
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,

View File

@@ -3,6 +3,8 @@ 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 ////////////////////////

View File

@@ -6,7 +6,7 @@
<body>
<form action="/submit_edit_post" method="POST" onsubmit="sha512password()">
<input name="userID" type="hidden" value="<%= post['userID'] %>">
<input name="postID" type="hidden" value="<%= post_id %>">
<input name="postID" type="hidden" value="<%= postID %>">
<label><%= locale.password %>:</label><br/>
<input type="password" required id="password" name="password"><br/><br/>

View File

@@ -1,5 +1,27 @@
<a href="/"><%= locale.home_page %></a>
<a href="/index/pages"><%= locale.site_index %></a>
<a href="<%= config.new_post_url %>"><%= locale.new_post %></a>
<a id='home-page-link' href="/"><%= locale.home_page %></a>
/
<% if (config.rss == true) { %>
<a id='global-rss-link' href="/rss"><%= locale.rss_feed %></a>
<% } %>
/
<% if (config.atom == true) { %>
<a id='global-atom-link' href="/atom"><%= 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><%- config.site_name %></h2>
</div>
<div id='site-description'>
<h2><%- config.site_description %></h2>
</div>
<%- config.seperator %>

View File

@@ -1,4 +1,6 @@
<h1>
<div id='tag-page-title'>
<h1>
<%= locale.posts_tagged %>: "<%- tag %>"
</h1>
</h1>
</div>
<%- config.seperator %>

View File

@@ -1,20 +0,0 @@
<h1>
<%- config.site_name %>
</h1>
<h2>
<%- config.site_description %>
</h2>
<% if (config.rss == true) { %>
<a href="/rss"><%= locale.rss_feed %></a><br/>
<% } %>
<% if (config.atom == true) { %>
<a href="/atom"><%= locale.atom_feed %></a><br/>
<% } %>
<a href="<%= config.new_post_url %>"><%= locale.new_post %></a><br/>
<% if (config.allow_signup == true) { %>
<a href="<%= config.signup_url %>"><%= locale.sign_up %></a><br/>
<% } %>
<% if (config.enable_hitcount == true) { %>
<%= locale.hitcount %>: <%= hitcount %>
<% } %>
<%- config.seperator %>

View File

@@ -1,8 +1,10 @@
<h1>
<div id='user-page-title'>
<h1>
<%= user.prettyname %>
</h1>
<p><%# func.render_md(user.description) %></p>
<a href="<%= config.edit_account_base_url %>/<%= userID %>"><%= locale.edit_account %></a><br/>
<a href="/user/<%= user.username %>/rss"><%= locale.rss_feed %></a><br/>
<a href="/user/<%= user.username %>/atom"><%= locale.atom_feed %></a>
</h1>
</div>
<p><%- func.render_md(user.description) %></p>
<a id='edit-account-link' href="<%= config.edit_account_base_url %>/<%= userID %>"><%= locale.edit_account %></a><br/>
<a id='rss-link' href="/user/<%= user.username %>/rss"><%= locale.rss_feed %></a><br/>
<a id='atom-link' href="/user/<%= user.username %>/atom"><%= locale.atom_feed %></a>
<%- config.seperator %>

View File

@@ -21,7 +21,7 @@
Comments:<br/>
<% for (let postID = 0; postID < comments.length; postID++) { %>
<% for (let comment_index = 0; comment_index < comments[postID].length; comment_index++) { %>
<a href="/comment/<%= comments[postID][comment_index]["id"] %>"><%= comments[postID][comment_index]["id"] %></a><br/>
<a href="/comment/<%= postID %>-<%= comment_index %>"><%= postID %>-<%= comment_index %></a><br/>
<% }; %>
<% }; %>
Users:<br/>
@@ -33,13 +33,13 @@
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/>
<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/>
<a href="<%= config.edit_account_base_url %>/<%= users[userID]["username"] %>">Edit <%= users[userID]["username"] %></a><br/>
<% }; %>
<% }; %>
<!-- TODO add tags -->

View File

@@ -6,7 +6,7 @@
<body>
<% for (let postID = 0; postID < comments.length; postID++) { %>
<% for (let comment_index = 0; comment_index < comments[postID].length; comment_index++) { %>
<a href="/comment/<%= comments[postID][comment_index]["id"] %>"><%= comments[postID][comment_index]["id"] %></a><br/>
<a href="/comment/<%= postID %>-<%= comment_index %>"><%= postID %>-<%= comment_index %></a><br/>
<% }; %>
<% }; %>
</body>

View File

@@ -12,8 +12,8 @@
<% 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 == tag) { %>
<%- include('../posts/tag', {post: posts[index], user: users[posts[index].userID], comments: comments[index]}); %>
<% if (current_tag.toLowerCase() == tag.toLowerCase()) { %>
<%- include('../posts/tag', {post: posts[index], postID: index, user: users[posts[index].userID], comments: comments[index]}); %>
<% } %>
<% }) %>
<% } %>

View File

@@ -22,6 +22,13 @@
<% } %>
<% } %>
</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>
<%- include('../partials/footer'); %>
</footer>

View File

@@ -1,5 +1,13 @@
<b><%= comment.name %></b>
<%= func.unix_time_to_date_format(comment.pubdate) %>
<a href='/comment/<%= postID %>-<%= comment.id %>'>No. <%= postID %>-<%= comment.id %></a>:<br/>
<%- func.render_comment(comment.content) %>
<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

@@ -1,3 +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 -->

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

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang='<%- config.locale %>'>
<head>
<%- include('../partials/head'); %>
</head>
<body>
<div id='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>

View File

@@ -24,6 +24,11 @@
<div id="post-editdate">
<i><%= locale.last_modified %>: <%= func.unix_time_to_date_format(post.pubdate) %></i><br/>
</div>
<% if (config.enable_hitcount == true) { %>
<div id='post-hitcount'>
Hitcount: <%- post.hitcount %>
</div>
<% } %>
</div>
<div id="post-commentform">
@@ -38,7 +43,7 @@
<div id="post-comments">
<% comments.forEach((comment, index) => { %>
<div id="comment-no<%= comment.id %>" class="post-comment-<%= index %>">
<div id="comment-no-<%= post.id %>-<%= comment.id %>" class="comment post-comment-<%= index %>">
<%- include('../partials/comment', {comment: comment}) %>
</div>
<% }) %>

View File

@@ -41,7 +41,7 @@
<div id="post-comments">
<% comments.forEach((comment, index) => { %>
<div id="comment-no<%= comment.id %>" class="post-comment-<%= index %>">
<div id="comment-no-<%= post.id %>-<%= comment.id %>" class="comment post-comment-<%= index %>">
<%- include('../partials/comment', {comment: comment}) %>
</div>
<% }) %>

View File

@@ -1,11 +1,11 @@
<div id="post-header">
<h1>
<%= post.title %>
<a href='/post/<%= post['id'] %>'><%= post.title %></a>
</h1>
</div>
<div id="post-content">
<p>
<%- func.render_md(post.content) %><br/>
<%- func.render_md(post.content) %>
</p>
</div>
<div id="post-details">
@@ -41,7 +41,7 @@
<div id="post-comments">
<% comments.forEach((comment, index) => { %>
<div id="comment-no<%= comment.id %>" class="post-comment-<%= index %>">
<div id="comment-no-<%= post.id %>-<%= comment.id %>" class="comment post-comment-<%= index %>">
<%- include('../partials/comment', {comment: comment}) %>
</div>
<% }) %>

View File

@@ -38,7 +38,7 @@
<div id="post-comments">
<% comments.forEach((comment, index) => { %>
<div id="comment-no<%= comment.id %>" class="post-comment-<%= index %>">
<div id="comment-no-<%= post.id %>-<%= comment.id %>" class="comment post-comment-<%= index %>">
<%- include('../partials/comment', {comment: comment}) %>
</div>
<% }) %>

4
webroot/custom.css Normal file
View File

@@ -0,0 +1,4 @@
/* Put any CSS you want in here, it will be applied to the whole website */
* {
font-family: sans-serif;
}

99
webroot/robots.txt Normal file
View File

@@ -0,0 +1,99 @@
User-agent: AddSearchBot
User-agent: AI2Bot
User-agent: Ai2Bot-Dolma
User-agent: aiHitBot
User-agent: Amazonbot
User-agent: Andibot
User-agent: anthropic-ai
User-agent: Applebot
User-agent: Applebot-Extended
User-agent: Awario
User-agent: bedrockbot
User-agent: bigsur.ai
User-agent: Brightbot 1.0
User-agent: Bytespider
User-agent: CCBot
User-agent: ChatGPT Agent
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: Claude-Web
User-agent: ClaudeBot
User-agent: CloudVertexBot
User-agent: cohere-ai
User-agent: cohere-training-data-crawler
User-agent: Cotoyogi
User-agent: Crawlspace
User-agent: Datenbank Crawler
User-agent: DeepSeekBot
User-agent: Devin
User-agent: Diffbot
User-agent: DuckAssistBot
User-agent: Echobot Bot
User-agent: EchoboxBot
User-agent: FacebookBot
User-agent: facebookexternalhit
User-agent: Factset_spyderbot
User-agent: FirecrawlAgent
User-agent: FriendlyCrawler
User-agent: Gemini-Deep-Research
User-agent: Google-CloudVertexBot
User-agent: Google-Extended
User-agent: Google-Firebase
User-agent: GoogleAgent-Mariner
User-agent: GoogleOther
User-agent: GoogleOther-Image
User-agent: GoogleOther-Video
User-agent: GPTBot
User-agent: iaskspider/2.0
User-agent: ICC-Crawler
User-agent: ImagesiftBot
User-agent: img2dataset
User-agent: ISSCyberRiskCrawler
User-agent: Kangaroo Bot
User-agent: LinerBot
User-agent: meta-externalagent
User-agent: Meta-ExternalAgent
User-agent: meta-externalfetcher
User-agent: Meta-ExternalFetcher
User-agent: meta-webindexer
User-agent: MistralAI-User
User-agent: MistralAI-User/1.0
User-agent: MyCentralAIScraperBot
User-agent: netEstate Imprint Crawler
User-agent: NovaAct
User-agent: OAI-SearchBot
User-agent: omgili
User-agent: omgilibot
User-agent: OpenAI
User-agent: Operator
User-agent: PanguBot
User-agent: Panscient
User-agent: panscient.com
User-agent: Perplexity-User
User-agent: PerplexityBot
User-agent: PetalBot
User-agent: PhindBot
User-agent: Poseidon Research Crawler
User-agent: QualifiedBot
User-agent: QuillBot
User-agent: quillbot.com
User-agent: SBIntuitionsBot
User-agent: Scrapy
User-agent: SemrushBot-OCOB
User-agent: SemrushBot-SWA
User-agent: ShapBot
User-agent: Sidetrade indexer bot
User-agent: TerraCotta
User-agent: Thinkbot
User-agent: TikTokSpider
User-agent: Timpibot
User-agent: VelenPublicWebCrawler
User-agent: WARDBot
User-agent: Webzio-Extended
User-agent: wpbot
User-agent: YaK
User-agent: YandexAdditional
User-agent: YandexAdditionalBot
User-agent: YouBot
Disallow: /