Initial commit x2

This commit is contained in:
Hammer1279 2025-04-24 15:10:31 +00:00
parent eeb058d7f3
commit 59ad480879
Signed by: Hammer1279
GPG Key ID: BDADBCBD10ACDCFB
13 changed files with 2730 additions and 1 deletions

View File

@ -1,3 +1,5 @@
# ht-dev.de # ht-dev.de
Source for the main Website Source for the main Website
Currently this still uses mustache, I plan on migrating everything to handlebars as soon as possible

7
config.json Normal file
View File

@ -0,0 +1,7 @@
{
"port": 8080,
"logging": false,
"logfile": "server.log",
"trustedProxies": [],
"webconfig": "./pages.jsonc"
}

349
index.js Normal file
View File

@ -0,0 +1,349 @@
// HT-Web Server (possibly the new Web Framework)
import fs from 'fs';
import express from 'express';
import superagent from 'superagent';
import { containsCidr } from 'cidr-tools';
import { join, resolve, dirname } from 'path';
import { parse } from "jsonc-parser";
import morgan from 'morgan';
import { fileURLToPath } from 'url';
import { readFile } from 'fs/promises';
import { mustache } from "consolidate";
import { renderFile, render as ejsRender } from 'ejs';
import { engine as handlebars, create } from 'express-handlebars';
import NodeCache from 'node-cache';
import config from './config.json' with {
type: "json"
};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// allow comments in pages file
const pages = parse(fs.readFileSync(config.webconfig).toString());
const cache = new NodeCache({
stdTTL: 3600,
useClones: false
});
const app = express();
app.use(morgan('dev')); // development and console logging
if (config.logging) {
// Ensure log file directory exists
const logDir = dirname(join(process.cwd(), config.logFile));
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
fs.appendFileSync(join(process.cwd(), config.logfile), `Server started at ${new Date().toUTCString()}\n`);
app.use(morgan('combined', {
stream: fs.createWriteStream(join(process.cwd(), config.logfile), { flags: 'a' })
})); // production
process.once('SIGINT', () => {
fs.appendFileSync(join(process.cwd(), config.logfile), `Server stopped at ${new Date().toUTCString()}\n`);
process.exit();
});
}
app.use((req, res, next) => {
res.setHeader('Server', "HT Web-Framework V2");
res.setHeader('X-Powered-By', "HT Web-Framework V2");
next();
});
app.use((req, res, next) => {
// Enable for choosing framing options, not migrated yet
// if (config.management.embedSite) {
// res.setHeader('X-Frame-Options', 'ALLOW-FROM ' + config.management.embedSite);
// res.setHeader('Content-Security-Policy', 'frame-ancestors ' + config.management.embedSite);
// } else {
// res.setHeader('X-Frame-Options', 'DENY');
// res.setHeader('Content-Security-Policy', 'frame-ancestors \'none\'');
// }
res.setHeader('X-Content-Type-Options', 'nosniff');
// res.setHeader('Content-Security-Policy', 'default-src \'self\'');
res.setHeader('Referrer-Policy', 'same-origin');
res.setHeader('Feature-Policy', 'geolocation \'none\'; microphone \'none\'; camera \'none\'; speaker \'none\'; vibrate \'none\'; payment \'none\'; usb \'none\';');
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
next();
});
// real IP assignment
app.use(async (req, res, next) => {
let ip = "0.0.0.0/0";
if ("x-forwarded-for" in req.headers) {
if (containsCidr(["127.0.0.1", "::1", ...config.trustedProxies], req.ip)) {
ip = req.headers['x-forwarded-for'] || req.ip;
} else {
console.warn("Proxy IP not in list:", req.ip);
return res.sendStatus(403);
}
} else if ("cf-connecting-ip" in req.headers){
if (!cache.has("cfcidrList")) {
cache.set("cfcidrList", (await superagent.get("https://api.cloudflare.com/client/v4/ips")).body);
}
const cfcidrList = cache.get("cfcidrList");
if (!cfcidrList.success) {
return next(cfcidrList.errors.join(", "));
}
if (containsCidr([...cfcidrList.result.ipv4_cidrs, ...cfcidrList.result.ipv6_cidrs], req.ip)) {
ip = req.headers['cf-connecting-ip'] || req.ip;
} else {
console.warn("CF IP not in list:", req.ip);
return res.sendStatus(403);
}
} else {
ip = req.ip; // Do nothing
}
req.realIp = ip;
next();
});
app.set('views', join(__dirname, 'views'));
app.engine('mustache', mustache);
app.engine('ejs', renderFile);
app.engine('handlebars', handlebars());
app.set('view engine', pages.settings.defaultType);
const extension = "." + pages.settings.defaultType;
const hbs = create({
extname: ".handlebars",
// defaultLayout: 'base.mustache',
defaultLayout: false,
layoutsDir: join(__dirname, "private", "templates"),
partialsDir: join(__dirname, "private", "templates"),
});
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/static", express.static(join(process.cwd(), "public")));
/**
* Send a page to the client
* @param {import('express').Request} req Express Request
* @param {import('express').Response} res Express Response
* @param {string} view Name of the view/file to render
* @param {object} context context to pass to the template, can be modified
* @param {Function} [cb] (optional) callback
* @returns {Promise<void>}
*/
async function render(req, res, view, context, cb) {
// Inject Servers
context.pages = [...pages.navpages];
// Active Detection
context.pages.forEach(element => {
element.active = req.url == element.url;
});
if (context.type == "html") {
console.debug("Rendering HTML");
res.sendFile(join(viewsDir, view + ".html"), { headers: { "Content-Type": "text/html" } });
return;
} else if (context.type == "ejs") {
console.debug("Rendering EJS");
const ejsResult = await renderFile(join(viewsDir, view + ".ejs"), context);
partials['view'] = join(partialsDir, 'ejs.mustache');
res.render(partials['base'], { ...context, partials: partials, ejs: ejsResult }, cb);
return;
} else if (context.type == "handlebars") {
console.debug("Rendering Handlebars");
const handlebarsResult = await hbs.renderView(join(viewsDir, view + ".handlebars"), context);
partials['view'] = join(partialsDir, 'handlebars.mustache');
res.render(partials['base'], { ...context, partials: partials, handlebars: handlebarsResult }, cb);
return;
} else if (context.type == "mustache") {
// Add current view
partials['view'] = join(viewsDir, view + ".mustache");
res.render(partials['base'], { ...context, partials: partials }, cb);
} else {
throw new Error("Unknown template type: " + context.type);
}
}
/**
* check for site access here, if allowed, call {@link render()}
* @param {import('express').Request} req Express Request
* @param {import('express').Response} res Express Response
* @param {string} view Name of the view/file to render
* @param {object} context context to pass to the template, can be modified
* @param {string[]} perms Array of permissions that need to be fulfilled by the user
* @param {Function} [cb] (optional) callback
* @returns {Promise<void>}
*/
async function renderRestricted(req, res, view, context, perms, cb) {
res.sendStatus(500);
throw new Error("Not implemented yet");
}
function isJSON(str) {
try {
return JSON.stringify(str) && !!str;
} catch (e) {
return false;
}
}
// Read partial templates
const partialsDir = join(__dirname, "private", "templates");
const viewsDir = join(__dirname, "private", "views");
// Map Partials
const partials = {}
fs.readdirSync(partialsDir).map(part => partials[part.replace(extension, '')] = join(partialsDir, part));
// Generate Routes
for (const path in pages.paths.get) {
if (Object.hasOwn(pages.paths.get, path)) {
const element = pages.paths.get[path];
if (element.file) {
app.get(path, async (req, res, next) => {
let patches = {
type: element.type ?? pages.settings.defaultType,
}
if (element.scripts) {
try {
for await (const file of element.scripts) {
const module = await import("file://" + resolve(join("private", "scripts", file + ".js")));
let result = module.get ? await module.get(element, { req, res, next }, config) : await module.default(element, { req, res, next }, config);
if (isJSON(result) && !(result?.done ?? false)) {
patches = { ...patches, ...result }
} else {
console.debug("Request already handled, returning...")
return;
}
}
} catch (error) {
return next(error);
}
}
if (element.settings && element.settings.subtitle) {
patches.title = element.settings.subtitle + " | " + settings.title;
}
const context = { ...settings, ...element.settings, ...patches };
if (element.restriction) {
renderRestricted(req, res, element.file, context, element.restriction);
} else {
// TODO: add way to handle the script already handling requests and then skip this
try {
await render(req, res, element.file, context);
} catch (error) {
// TODO: check why errors are just ignored
console.debug(error);
// return next(error);
}
}
})
} else {
app.get(path, async (req, res, next) => {
if (element.scripts) {
try {
for await (const file of element.scripts) {
const module = await import("file://" + resolve(join('web', 'scripts', file + ".js")));
module.get ? await module.get(element, { req, res, next }, config) : await module.default(element, { req, res, next }, config);
}
} catch (error) {
return next(error);
}
} else {
next();
}
})
}
}
}
for (const path in pages.paths.post) {
if (Object.hasOwn(pages.paths.post, path)) {
const element = pages.paths.post[path];
if (element.file) {
app.post(path, async (req, res, next) => {
let patches = {
type: element.type ?? pages.settings.defaultType,
}
if (element.scripts) {
try {
for await (const file of element.scripts) {
const module = await import("file://" + resolve(join('web', 'scripts', file + ".js")));
let result = module.post ? await module.post(element, { req, res, next }, config) : await module.default(element, { req, res, next }, config);
if (isJSON(result) && !(result?.done ?? false)) {
patches = { ...patches, ...result }
} else {
console.debug("Request already handled, returning...")
return;
}
}
} catch (error) {
return next(error);
}
}
if (element.settings && element.settings.subtitle) {
patches.title = element.settings.subtitle + " | " + settings.title;
}
const context = { ...settings, ...element.settings, ...patches };
if (element.restriction) {
renderRestricted(req, res, element.file, context, element.restriction);
} else {
// TODO: add way to handle the script already handling requests and then skip this
try {
await render(req, res, element.file, context);
} catch (error) {
// TODO: check why errors are just ignored
console.debug(error);
// return next(error);
}
}
})
} else {
app.post(path, async (req, res, next) => {
if (element.scripts) {
try {
for await (const file of element.scripts) {
const module = await import("file://" + resolve(join('web', 'scripts', file + ".js")));
module.post ? await module.post(element, { req, res, next }, config) : await module.default(element, { req, res, next }, config);
}
} catch (error) {
return next(error);
}
} else {
next();
}
})
}
}
}
let settings = {
...pages.settings,
pages: [...pages.navpages],
};
app.get('/', async (req, res) => {
try {
const currentConfigFile = await readFile(join(process.cwd(), 'config.json'), 'utf-8');
const currentConfig = JSON.parse(currentConfigFile);
res.json(currentConfig);
} catch (error) {
console.error('Error reading config file:', error);
res.status(500).json({ error: 'Failed to read configuration' });
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
// res.status(500).json({
// error: 'Internal Server Error',
// message: err.message,
// stack: err.stack
// });
res.status(500).send(`<title>ERR: ${err.message}</title><h1>500: Internal Server Error</h1><p>${err.stack.replaceAll('\n', "<br />")}</p>`);
});
app.listen(config.port, () => {
console.log(`Server listening on port ${config.port}`);
});

2028
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "ht-dev.de",
"version": "1.0.0",
"description": "Source for the main Website",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.ht-dev.de/Hammer1279/ht-dev.de.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cidr-tools": "^11.0.3",
"consolidate": "^1.0.4",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-handlebars": "^8.0.1",
"jsonc-parser": "^3.3.1",
"morgan": "^1.10.0",
"mustache": "^4.2.0",
"node-cache": "^5.1.2",
"superagent": "^10.2.0"
},
"type": "module"
}

34
pages.jsonc Normal file
View File

@ -0,0 +1,34 @@
// Defines the routes used by the Webserver
{
"settings": {
"title": "Title", // tab title
"headerTitle": "Header Title", // title in navbar
// base_* are the default stylesheets and scripts (client side) to load on every page
"base_stylesheets": [], // stylesheets that will always be loaded
"base_scripts": [], // scripts that will always be loaded
"widgets": [], // widgets are the easiest way for quick dynamic data, but should otherwise be avoided
"defaultType": "handlebars" // default template engine to use
},
"paths": {
"get": {
"/": {
"file": "home",
"type": "html", // possible values: "mustache", "handlebars", "ejs", "html"
"scripts": [],
"settings": { // these are part of the context of a object
"stylesheets": [],
"scripts": [],
"widgets": []
},
"restriction": false
}
},
"post": {}
},
"navpages": [
{
"name": "Home",
"url": "/"
}
]
}

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<link rel="icon" type="image/jpg" href="/static/img/logo.jpg" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
{{#base_stylesheets}}
<link rel="stylesheet" href="/static/css/{{&.}}">
{{/base_stylesheets}}
{{#stylesheets}}
<link rel="stylesheet" href="/static/css/{{&.}}">
{{/stylesheets}}
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@1/dist/es-module-shims.min.js" crossorigin="anonymous"></script>
<script type="importmap">
{
"imports": {
"@popperjs/core": "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/popper.min.js",
"js-cookie": "https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/+esm",
"superagent": "https://cdn.jsdelivr.net/npm/superagent@10.1.0/+esm"
}
}
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<meta name="description" content="{{description}}">
<meta name="author" content="&copy;2025 Hammer1279">
<meta name="robots" content="index,follow">
<meta property="og:type" content="website">
<meta property="og:title" content="{{header}}" />
<meta property="og:description" content="{{description}}" />
<meta property="og:url" content="https://swprobuilders.com" />
<meta property="og:image" content="/static/img/logo.jpg" />
<meta property="og:site_name" content="{{title}}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#0f2537">
{{! <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.esm.min.js" crossorigin="anonymous"></script> }}
</head>

66
private/views/home.html Normal file
View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HT-Dev DE</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<meta name="description" content="{{description}}">
<meta name="author" content="&copy;2025 Hammer1279">
<meta name="robots" content="index,follow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div style="max-width: 70%; margin: 0 auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px;">
<div class="center">
<h1>HT-Dev.DE</h1>
<h2>Hello World! Hello from Germany!</h2>
<p>My personal effort for decentralizing and personalizing the web again.</p>
<p>This page is in honor to the old (and free) internet.</p>
</div>
<br>
<p>I'm mostly a backend developer, so forgive this crude website, its backend is really good tho :D</p>
<p>However you have found this domain, welcome! We have lots of services here:</p>
<ul style="text-align: start;">
<li><a href="https://chat.ht-dev.de" target="_blank">Matrix Homeserver</a></li>
<li><a href="https://git.ht-dev.de" target="_blank">Gitea</a></li>
<li><a href="https://cloud.ht-dev.de" target="_blank">Nextcloud</a></li>
<li><a href="#" style="color: gray; pointer-events: none; text-decoration: none;">Compute (currently out of service)</a></li>
<li><a href="https://mail.ht-dev.de" target="_blank">E-Mail (redirect for using @ht-dev.de accounts)</a></li>
<li><a href="telnet://ht-dev.de" style="color: gray; pointer-events: none; text-decoration: none;">Telnet BBS-ish Server (getting reworked)</a></li>
</li>
</ul>
<p>Personal Open-Source Projects to check out:</p>
<ul style="text-align: start;">
<li><a href="https://github.com/Hammer1279/node-proxy-manager" target="_blank">Node-Proxy-Manager (this site is behind this)</a></li>
<li><a href="https://git.ht-dev.de/Hammer1279/node-telnet-client" target="_blank">A Node.js Telnet client with encryption with our own telnet server</a></li>
</ul>
<p>Hosted Sites via our Servers:</p>
<ul style="text-align: start;">
<li>ht-dev.de (you are here)</li>
<li><a href="https://drillkea.com" target="_blank">DrillKEA.com</a></li>
<li><a href="https://SWProBuilders.com" target="_blank">SWProBuilders.com</a></li>
</ul>
<br>
<p>If you need to contact me, there are several ways:</p>
<ul style="text-align: start;">
<li>Email: <a href="mailto:hammer@ht-dev.de">hammer@ht-dev.de</a></li>
<li>Matrix: <a href="https://matrix.to/#/@hammer1279:ht-dev.de" target="_blank">@hammer1279:ht-dev.de</a></li>
<li>Matrix (Alt): <a href="https://matrix.to/#/@hammer1279:matrix.org" target="_blank">@hammer1279:matrix.org</a></li>
<li>GitHub: <a href="https://github.com/Hammer1279" target="_blank">github.com/Hammer1279</a></li>
</ul>
</div>
<footer style="text-align: center; margin-top: 20px;">
<p>HT-Dev.DE &copy;2025 Hammer1279</p>
<p>All rights reserved.</p>
<p>Powered by <a href="https://git.ht-dev.de/Hammer1279/ht-dev.de" target="_blank">HT-Web-Framework V2</a> (to serve a static html page??)</p>
</footer>
</body>
</html>

27
public/css/style.css Normal file
View File

@ -0,0 +1,27 @@
@font-face {
font-family: 'visitor1';
src: url('/static/font/visitor1.ttf'), url('../font/visitor1.ttf');
}
@font-face {
font-family: 'visitor2';
src: url('/static/font/visitor2.ttf'), url('../font/visitor2.ttf');
}
body {
font-family: 'visitor1', sans-serif;
font-size: x-large;
background-color: #000;
color: #fff;
/* text-align: center; */
background-image: url('/static/img/stars3.gif'), url('../img/stars3.gif');
background-repeat: repeat;
}
h1, h2, h3, h4, h5, h6 {
text-align: center;
}
.center {
text-align: center;
}

BIN
public/font/visitor1.ttf Normal file

Binary file not shown.

BIN
public/font/visitor2.ttf Normal file

Binary file not shown.

147
public/html/home-live.html Normal file
View File

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Internet Explorer / Classic Edge -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- mobile device scaling stuff -->
<title>HT-Dev DE</title>
<!-- copied from my usual template, still here for when needed -->
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> -->
<!-- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> -->
<meta name="title" content="HT-DEV DE">
<meta name="description" content="HT-DEV Homepage">
<meta name="author" content="&copy;2025 Hammer1279">
<meta name="robots" content="index,follow">
<meta name="color-scheme" content="dark"> <!-- tell things like dark reader that this site is dark -->
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="../css/style.css"> <!-- local link for development -->
</head>
<body>
<p style="font-size: x-small;">Logo is still WIP</p>
<div style="max-width: 70%; margin: 0 auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px;">
<div class="center">
<h1>HT-Dev.DE</h1>
<h2>Hello World! Hello from Germany!</h2>
<p>My personal effort for decentralizing and personalizing the web again.</p>
<p>This page is in honor to the old (and free) internet.</p>
</div>
<br>
<p>I'm mostly a backend developer, so please forgive this crude website.</p>
<p>This page is still under active development and will improve hopefully.</p>
<p>However you have found this domain, welcome! We have lots of services here:</p>
<ul style="text-align: start;">
<li><a href="https://git.ht-dev.de" target="_blank">Gitea</a></li>
<li><a href="https://wiki.ht-dev.de" target="_blank">HT Wiki</a></li>
<li><a href="https://cloud.ht-dev.de" target="_blank">Nextcloud</a></li>
<li><a href="https://web.ht-dev.de" target="_blank">Wordpress</a></li>
<li>Matrix Homeserver + Clients<br>
<ul>
<li><a href="https://chat.ht-dev.de" target="_blank">Schildichat (old)</a></li>
<li><a href="https://cinny.ht-dev.de" target="_blank">Cinny (new)</a></li>
</ul>
</li>
<li><a href="#" style="color: gray; pointer-events: none; text-decoration: none;">Compute (currently out of
service)</a></li>
<li><a href="https://mail.ht-dev.de" target="_blank">E-Mail (redirect for using @ht-dev.de accounts)</a>
</li>
<li><a href="telnet://ht-dev.de" style="color: gray; pointer-events: none; text-decoration: none;">Telnet
BBS-ish Server (getting reworked)</a></li>
</ul>
<p>Personal Open-Source Projects to check out:</p>
<ul style="text-align: start;">
<li><a href="https://github.com/Hammer1279/node-proxy-manager" target="_blank">Node-Proxy-Manager</a> (this
domain is behind this)</li>
<li><a href="https://git.ht-dev.de/Hammer1279/node-telnet-client" target="_blank">A Node.js Telnet client
with encryption</a> (with our own telnet server)</li>
</ul>
<p>Hosted Sites via our Servers:</p>
<ul style="text-align: start;">
<li>ht-dev.de (except this page right here)</li>
<li><a href="https://drillkea.com" target="_blank">DrillKEA.com</a> (previously)</li>
<li><a href="https://SWProBuilders.com" target="_blank"
style="color: gray; pointer-events: none; text-decoration: none;">SWProBuilders.com (getting
reworked)</a></li>
</ul>
<br>
<p>If you need to contact me, there are several ways:</p>
<ul style="text-align: start;">
<li>Email: <a href="mailto:hammer@ht-dev.de">hammer@ht-dev.de</a></li>
<li>Matrix: <a href="https://matrix.to/#/@hammer1279:ht-dev.de" target="_blank">@hammer1279:ht-dev.de</a>
</li>
<li>Matrix (Alt): <a href="https://matrix.to/#/@hammer1279:matrix.org"
target="_blank">@hammer1279:matrix.org</a></li>
<li>GitHub: <a href="https://github.com/Hammer1279" target="_blank">github.com/Hammer1279</a></li>
</ul>
</div>
<br>
<div style="max-width: 70%; margin: 0 auto; text-align: left;">
<small>Badges and cool sites:</small>
</div>
<div id="linkback" style="margin: auto; max-width: 70%; font-size: x-small">
<a href="http://www.mabsland.com/Adoption.html" target="_blank"><img src="http://www.mabsland.com/Pandas/Censor_PGc.gif" width="88" height="31"></a>
<a href="https://cadence.moe/blog/2024-10-05-created-by-a-human-badges" target="_blank"><img
src="https://cadence.moe/static/img/created-by-a-human/created-by-a-human.svg"
alt="Created by a human with a heart" width="88" height="31"></a>
<!-- <a href="https://ht-dev.de" target="_blank"><img src="https://ht-dev.de/static/img/button.png" width="88" height="31"></a> -->
<iframe src="//incr.easrng.net/badge?key=htdev" style="background: url(//incr.easrng.net/bg.gif)"
title="increment badge" width="88" height="31" frameborder="0"></iframe>
<!-- <script type="text/javascript" src="https://www.free-counters.org/count/hp0t"></script> -->
<!-- <script>
// Wait for the SVG to be loaded
setTimeout(() => {
const svg = document.getElementById('besucherzaehler2');
if (svg) {
svg.setAttribute('width', '88');
svg.setAttribute('height', '31');
// Scale using the original aspect ratio
svg.setAttribute('viewBox', '0 0 100 45');
svg.style.preserveAspectRatio = 'xMinYMid meet';
}
}, 1000);
</script> -->
<img src="https://capstasher.neocities.org/88x31Buttons/css.png" width="88" height="31">
<img src="https://retrojcities.neocities.org/files/desktopviewing.gif" width="88" height="31">
<img src="https://capstasher.neocities.org/88x31Buttons/logoab8.gif" width="88" height="31">
<a href="https://windows.com" target="_blank"><img src="https://capstasher.neocities.org/88x31Buttons/made_with_windows.gif" width="88" height="31"></a>
<a href="https://code.visualstudio.com/" target="_blank"><img
src="https://capstasher.neocities.org/88x31Buttons/vscbutton.gif" width="88" height="31"></a>
<!-- <img src="https://capstasher.neocities.org/88x31Buttons/apple_fatal_error.gif" width="88" height="31"> -->
<img src="https://cyber.dabamos.de/88x31/fckfb.gif" width="88" height="31">
<img src="https://cyber.dabamos.de/88x31/fckgoogle.gif" width="88" height="31">
<a href="https://www.mozilla.org/firefox/" target="_blank"><img src="https://cyber.dabamos.de/88x31/firefoxget.gif" width="88" height="31"></a>
<a href="https://ublockorigin.com/"><img src="https://retrojcities.neocities.org/files/ublock-now.png"></a>
<img src="https://retrojcities.neocities.org/files/dsbutton.gif" width="88" height="31">
<a href="https://archive.org/" target="_blank"><img src="https://cyber.dabamos.de/88x31/internetarchive.gif" width="88" height="31"></a>
<a href="https://cadence.moe" target="_blank"><img src="https://cadence.moe/static/img/cadence_now.png"
alt="The text &quot;cadence now!&quot; on a purple background. There is a moon-shaped logo on the left side and a tiny star in the bottom right."
width="88" height="31"></a>
<a href="https://goblin-heart.net/sadgrl" target="_blank"><img
src="https://goblin-heart.net/sadgrl/assets/images/buttons/sadgrlonline.gif" width="88" height="31"></a>
<a target="_blank" href="https://melonking.net"><img alt="Visit Melonking.Net!"
src="https://melonking.net/images/badges/MELON-BADGE-2.GIF" style="image-rendering: pixelated"></a>
<a href="https://capstasher.neocities.org/"><img src="https://capstasher.neocities.org/csc_88x31.png" width="88"
height="31"></a>
</div>
<div id="otherimg" style="margin: auto; max-width: 70%; font-size: small">
<p>Honestly no idea how...</p>
<a href="http://www.nerdtests.com/ft_cg.php?im"><img src="http://www.nerdtests.com/images/ft/cg.php?val=0848" alt="My computer geek score is greater than 100% of all people in the world! How do you compare? Click here to find out!"> </a>
<script type="text/javascript" src="https://www.free-counters.org/count/hp1k"></script>
<p style="font-size: small;">The visitor counter above usually links back to a horse insurance website and hides it and breaks if you remove it... NO!</p>
</div>
<!-- TODO: reenable once done editing -->
<link href="https://melonking.net/styles/flood.css" rel="stylesheet" type="text/css" media="all" />
<script src="https://melonking.net/scripts/flood.js"></script>
<footer style="text-align: center; margin-top: 20px;">
<p>HT-Dev.DE &copy;2025 Hammer1279</p>
<p>All rights reserved.</p>
<!-- <p>Powered by <a href="https://git.ht-dev.de/Hammer1279/ht-dev.de" target="_blank">HT-Web-Framework V2</a> (to serve a static html page??)</p> -->
<!-- ^ yeah not anymore lol -->
</footer>
<p style="font-size: small;">Unique Visitors <span style="font-size: xx-small;">(we don't talk about its accuracy)</span></p>
<img src="https://hitwebcounter.com/counter/counter.php?page=20392516&style=0027&nbdigits=4&type=ip&initCount=0"
title="Counter Widget" Alt="Visit counter For Websites" border="0" width="100" height="31" />
</body>
</html>

BIN
public/img/stars3.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB