Compare commits

...

8 Commits

Author SHA1 Message Date
6360b41e9a fkin finally 2025-08-21 23:23:14 +01:00
217adf91e9 templating 2025-08-21 21:15:21 +01:00
a7404fed3d crow setup 2025-08-20 23:08:54 +01:00
bfb1b8a21e sitemap
All checks were successful
Build and push container image / build-and-push-image (push) Successful in 3m59s
2025-08-13 22:54:24 +01:00
ef459d728a update link
All checks were successful
Build and push container image / build-and-push-image (push) Successful in 3m16s
2025-08-10 19:53:31 +01:00
5690bcadf9 I fkin found em
All checks were successful
Build and push container image / build-and-push-image (push) Successful in 4m21s
2025-08-08 20:31:55 +01:00
8b5b80f7c5 Merge remote-tracking branch 'refs/remotes/gitea/main'
All checks were successful
Build and push container image / build-and-push-image (push) Successful in 3m7s
2025-08-07 21:26:35 +01:00
66806ad922 events 2025-08-07 21:25:10 +01:00
41 changed files with 408 additions and 660 deletions

8
.gitignore vendored
View File

@@ -1,6 +1,2 @@
.venv
.env
db.sqlite
flask_session
__pycache__
app.log
build/
.vscode/

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "thirdparty/inja"]
path = thirdparty/inja
url = https://github.com/pantor/inja
[submodule "thirdparty/nlohmann"]
path = thirdparty/nlohmann
url = https://github.com/nlohmann/json

View File

@@ -1,22 +0,0 @@
FROM python:alpine
# Set the working directory
WORKDIR /app
# Copy the requirements file into the container
COPY requirements.txt .
# Install the required packages
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install gunicorn
# Copy the rest of the application code into the container
COPY src src
COPY templates templates
COPY static static
# Expose the port the app runs on
EXPOSE 5000
# run the application
ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.wsgi:app" ]

15
include/errors.hpp Normal file
View File

@@ -0,0 +1,15 @@
#ifndef ERRORS_HPP
#define ERRORS_HPP
#include <crow.h>
#include "templating.hpp"
extern Templating templating;
struct CustomErrorHandler {
struct context {};
void before_handle(crow::request& req, crow::response& res, context&) {}
void after_handle(crow::request& req, crow::response& res, context&);
};
#endif

25
include/templating.hpp Normal file
View File

@@ -0,0 +1,25 @@
#ifndef TEMPLATING_HPP
#define TEMPLATING_HPP
#include <inja/inja.hpp>
#include <crow.h>
#include <string>
class Templating {
public:
explicit Templating(const std::string& template_dir);
crow::response render_template(const std::string& template_name, const inja::json& data);
crow::response render_template(const std::string& template_name);
std::string render_template_string(const std::string& template_name, const inja::json& data);
std::string render_template_string(const std::string& template_name);
private:
inja::Environment inja_env;
std::string template_dir; // absolute path to templates
std::string preprocess_template(const std::string& template_name);
};
#endif

51
makefile Normal file
View File

@@ -0,0 +1,51 @@
# Directories
INCLUDE_DIRS = include thirdparty/inja/include thirdparty/nlohmann/include
SRC_DIR = src
BUILD_DIR = build
# Compiler and linker settings
CXX = g++
LIBS =
CXXFLAGS = -std=c++17 $(foreach dir,$(INCLUDE_DIRS),-I$(dir))
# Source and object files
SRC = $(wildcard $(SRC_DIR)/*.cpp)
# Target executable
UNAME := $(shell uname -s)
BUILD_DIR := $(BUILD_DIR)/$(UNAME)
OBJ_DIR := $(BUILD_DIR)/objs
BIN = $(BUILD_DIR)/main
# Object files corresponding to the source files (now in obj directory)
OBJS = $(addprefix $(OBJ_DIR)/, $(addsuffix .o, $(basename $(notdir $(SRC)))))
# development target with debugging
dev: CXXFLAGS += -g -Wall -Wformat
dev: all
# Release target
release: CXXFLAGS += -O3
release: all
# Create directories for build output
dirs:
@mkdir -p $(BUILD_DIR)
@mkdir -p $(OBJ_DIR)
# Clear build directory
clear:
@find $(OBJ_DIR) -type f -name '*.o' -exec rm -f {} +
# Pattern rule for source files in src directory
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
all: dirs clear $(BIN)
@echo Build complete
$(BIN): $(OBJS)
$(CXX) -o $@ $^ $(CXXFLAGS) $(LIBS)
clean:
rm -rf $(BUILD_DIR)

View File

@@ -1,4 +0,0 @@
python-dotenv
flask-session
requests
flask

5
run.sh
View File

@@ -1,5 +0,0 @@
#!/bin/bash
[ ! -f .env ] || export $(grep -v '^#' .env | xargs)
flask --app src.wsgi.py --debug run

13
src/errors.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "errors.hpp"
std::string render_404_template(const crow::request& req) {
return templating.render_template_string("errors/404.html", {{"requested_url", req.url}});
}
void CustomErrorHandler::after_handle(crow::request& req, crow::response& res, context&) {
if (res.code == 404 && res.body.empty()) {
res.set_header("Content-Type", "text/html");
res.body = render_404_template(req);
}
}

18
src/main.cpp Normal file
View File

@@ -0,0 +1,18 @@
#define CROW_STATIC_DIRECTORY "../static"
#include "templating.hpp"
#include "errors.hpp"
#include <crow.h>
Templating templating{"../templates"};
int main() {
crow::App<CustomErrorHandler> app;
CROW_ROUTE(app, "/")([]() {
return templating.render_template("index.html");
});
app.port(8080).multithreaded().run();
}

View File

@@ -1,48 +0,0 @@
# Imports
from flask import Blueprint, render_template
from os import getenv as env
import logging
import src.routes.snake as snake
# Create blueprint
bp = Blueprint(
'error_handlers',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Route for 500 error
@bp.route('/500')
@bp.app_errorhandler(500)
def internal_server_error(error=None):
if error is not None:
log.error("Internal server error: %s", error)
return render_template('errors/500.html'), 500
# Route for 404 error
@bp.route('/404')
@bp.app_errorhandler(404)
def not_found(error=None):
if error is not None:
log.warning("Page not found: %s", error)
scores = snake.get_leaderboard()
token = snake.generate_start_token()
return render_template('errors/404.html', scores=scores, token=token, cap_key=env('CAP_KEY', default='')), 404 if error is not None else 200
# Route for 400 error
@bp.route('/400')
@bp.app_errorhandler(400)
def bad_request(error=None):
if error is not None:
log.warning("Bad request: %s", error)
return render_template('errors/400.html', error=error), 400

View File

@@ -1,53 +0,0 @@
# Imports
from flask import Blueprint, render_template, request, abort, send_from_directory, send_file
from os import getenv as env
import logging
# Create blueprint
bp = Blueprint(
'generic',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Route for index page
@bp.route('/')
def index():
return render_template('index.html')
# Route for favicon
@bp.route('/favicon.ico')
def favicon():
return send_file('../static/content/other/favicon.ico')
# Route for robots.txt
@bp.route('/robots.txt')
def robots():
return send_file('../static/content/other/robots.txt')
# Route for sitemap.xml
@bp.route('/sitemap.xml')
def sitemap():
return send_file('../static/content/other/sitemap.xml')
# Catch-all route for generic pages
@bp.route('/<path:filename>')
def catch_all(filename):
# try to find template in the pages directory and add .html extension
if not filename.endswith('.html'):
filename += '.html'
try:
return render_template(f'pages/{filename}')
except Exception as e:
abort(404, f"Template '{filename}' not found: {e}")

View File

@@ -1,146 +0,0 @@
# Imports
from flask import Blueprint, abort, request, redirect
from os import urandom, getenv as env
import src.utils.database as database
import src.utils.cap as cap
import logging, datetime, threading, time
# Create blueprint
bp = Blueprint(
'snake',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Create database instance
db = database.Database(db_name=env('DB_NAME', default='db.sqlite'))
db.execute('CREATE TABLE IF NOT EXISTS snake_scores (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score INTEGER)')
db.execute('''CREATE TABLE IF NOT EXISTS snake_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip TEXT UNIQUE NOT NULL
)''')
# Input validation function
def valid_length(value, min_length=1, max_length=100):
if not isinstance(value, str):
return False
return min_length <= len(value) <= max_length
def valid_score(score, game_token):
start_time = db.execute('SELECT created_at FROM snake_tokens WHERE token = ?', (game_token,)).fetchone()
if not start_time:
log.error("Game token not found.")
return False
start_time = datetime.datetime.fromisoformat(start_time[0])
current_time = datetime.datetime.now()
elapsed_time = (current_time - start_time).total_seconds()
if elapsed_time < score / 10 * 3 + 10: # assuming that each point takes 3 seconds to achieve and 10 seconds to start the game and do captcha
log.error("Score is too high for the elapsed time.")
return False
if score <= 0 or score > 10000: # Arbitrary upper limit for scores
log.error("Score is out of valid range.")
return False
if score % 10 != 0:
log.error("Score is not a multiple of 10.")
return False
# delete the token after score validation
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
log.info(f"Score {score} validated successfully for token {game_token}.")
return True
# Route for score submission
@bp.route('/snake/submit', methods=['POST'])
def submit_score():
name = request.form.get('name')
score = request.form.get('score')
captcha_token = request.form.get('cap-token')
game_token = request.form.get('game_token')
if not cap.verify_captcha(captcha_token):
log.error("Captcha verification failed.")
abort(400, "Captcha verification failed")
if not name or not score or not captcha_token or not game_token:
log.error("Name, score, captcha token, or game token is missing.")
abort(400, "Missing required fields")
if not valid_length(name, min_length=3, max_length=15):
log.error("Invalid name length.")
abort(400, "Name must be between 3 and 15 characters long.")
if not valid_score(int(score), game_token):
log.error("Invalid score.")
abort(400, "Score not vilid, so either you are trying to cheat the leaderboard or something is seriously wrong.")
try:
db.execute('INSERT INTO snake_scores (name, score) VALUES (?, ?)', (name, int(score)))
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
log.info(f"Score submitted: {name} - {score}")
return redirect('/404')
except Exception as e:
log.error(f"Database error: {e}")
abort(500, "Internal server error while submitting score.")
# Generate a unique game token
def generate_start_token():
"""Generate a unique start token for the game."""
token = urandom(16).hex()
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
ip_token = db.execute('SELECT token FROM snake_tokens WHERE ip = ?', (ip,)).fetchone()
if ip_token:
log.info(f"Token already exists for IP: {ip}, reusing token.")
return ip_token[0]
log.info(f"Generated start token: {token}")
db.execute('INSERT INTO snake_tokens (token, ip) VALUES (?, ?)', (token, ip))
return token
# Get leaderboard scores
def get_leaderboard():
"""Fetch scores from the leaderboard."""
try:
scores = db.execute('SELECT name, score FROM snake_scores ORDER BY score DESC').fetchall()
leaderboard = [{'position': i + 1, 'name': score[0], 'score': score[1]} for i, score in enumerate(scores)]
log.info("Leaderboard fetched successfully.")
return leaderboard
except Exception as e:
log.error(f"Error fetching leaderboard: {e}")
return []
# Clear all tokens older than 1 hour
def clear_old_tokens():
while True:
try:
one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
db.execute('DELETE FROM snake_tokens WHERE created_at < ?', (one_hour_ago,))
log.info("Old tokens cleared.")
except Exception as e:
log.error(f"Error clearing old tokens: {e}")
time.sleep(3600) # Run every hour
# Start the token clearing thread
token_thread = threading.Thread(target=clear_old_tokens, daemon=True)
token_thread.start()

76
src/templating.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include "templating.hpp"
#include <filesystem>
#include <fstream>
#include <regex>
#include <stdexcept>
Templating::Templating(const std::string& template_dir)
: inja_env(std::filesystem::canonical(template_dir).string()),
template_dir(std::filesystem::canonical(template_dir).string())
{
inja_env.set_search_included_templates_in_files(true);
}
std::string Templating::preprocess_template(const std::string& template_name) {
namespace fs = std::filesystem;
fs::path abs_template_dir = fs::path(template_dir);
fs::path abs_template_file = fs::canonical(abs_template_dir / template_name);
std::ifstream file(abs_template_file);
if (!file.is_open()) {
throw std::runtime_error("Failed to open template file: " + abs_template_file.string());
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
std::regex extends_regex(R"(\{\%\s*extends\s*(['"])(.+?)\1\s*\%\})");
std::smatch match;
if (std::regex_search(content, match, extends_regex)) {
std::string quote = match[1].str();
std::string original_path = match[2].str();
if (original_path.find("/") == std::string::npos &&
!original_path.empty() &&
original_path.front() != '/') {
fs::path abs_extended_template = fs::canonical(abs_template_dir / original_path);
fs::path rel_path = fs::relative(abs_extended_template, abs_template_file.parent_path());
std::string new_path = rel_path.generic_string();
content = std::regex_replace(content, extends_regex,
"{% extends " + quote + new_path + quote + " %}");
}
}
return content;
}
crow::response Templating::render_template(const std::string& template_name, const inja::json& data) {
try {
std::string preprocessed = preprocess_template(template_name);
inja::Template tpl = inja_env.parse(preprocessed);
std::string rendered = inja_env.render(tpl, data);
return crow::response(rendered);
} catch (const std::exception& e) {
return crow::response(500, e.what());
}
}
crow::response Templating::render_template(const std::string& template_name) {
return render_template(template_name, inja::json{});
}
std::string Templating::render_template_string(const std::string& template_name, const inja::json& data) {
std::string preprocessed = preprocess_template(template_name);
inja::Template tpl = inja_env.parse(preprocessed);
return inja_env.render(tpl, data);
}
std::string Templating::render_template_string(const std::string& template_name) {
return render_template_string(template_name, inja::json{});
}

View File

@@ -1,42 +0,0 @@
# Imports
from os import getenv as env
import requests, logging
# Create logger
log = logging.getLogger(__name__)
# Function to verify CAPTCHA response
def verify_captcha(token: str) -> bool:
"""
Verify the CAP response token with the CAP server.
Args:
token (str): The CAP response token to verify.
Returns:
bool: True if the token is valid, False otherwise.
"""
if not token:
return False
try:
response = requests.post(
f"https://cap.alfieking.dev/{env('CAP_KEY', default='')}/siteverify",
json={
'secret': env('CAP_SECRET', default=''),
'response': token,
},
timeout=10
)
response.raise_for_status()
if response.status_code != 200:
log.error("CAPTCHA verification failed with status code: %s", response.status_code)
return False
return response.json().get('success', False)
except Exception as e:
log.error("Error verifying CAPTCHA: %s", e)
return False

View File

@@ -1,18 +0,0 @@
# Imports
import sqlite3
# Database class
class Database:
def __init__(self, db_name='db.sqlite'):
self.connection = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.connection.cursor()
def execute(self, query, params=None):
if params is None:
params = []
self.cursor.execute(query, params)
self.connection.commit()
return self.cursor
def close(self):
self.connection.close()

View File

@@ -1,58 +0,0 @@
# Imports
from flask import Flask
from flask_session import Session
from dotenv import load_dotenv
from os import getenv as env, listdir
import logging, importlib
# Load env
load_dotenv()
# Create console log handler
console_log = logging.StreamHandler()
console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s"))
console_log.setLevel(logging.INFO)
# Create file log handler
file_log = logging.FileHandler(env('LOG_FILE', default='app.log'), mode=env('LOG_MODE', default='a'))
file_log.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
file_log.setLevel(logging.DEBUG)
# Add handlers to the logger
log = logging.getLogger()
log.setLevel(logging.DEBUG)
log.addHandler(console_log)
log.addHandler(file_log)
log.info("Logging initialized")
# Create flask app
app = Flask(
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Configure sessions
app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
# Load routes
routes_dir = env('ROUTES_DIR', default='src/routes')
for filename in listdir(routes_dir):
if not filename.endswith('.py') and filename.startswith('__'):
continue
module_name = f"{routes_dir.replace('/', '.')}.{filename[:-3]}"
try:
module = importlib.import_module(module_name)
if hasattr(module, 'bp'):
app.register_blueprint(module.bp)
log.info(f"Registered blueprint: {module_name}")
except Exception as e:
log.error(f"Failed to register blueprint {module_name}: {e}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@@ -1,6 +1,8 @@
User-agent: *
Allow: /
Disallow: /404.html
Disallow: /404
Disallow: /500
Disallow: /400
Sitemap: https://alfieking.dev/sitemap.xml

View File

@@ -1,6 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.example.com/index.html</loc>
<loc>https://alfieking.dev/</loc>
</url>
<url>
<loc>https://alfieking.dev/toaster</loc>
</url>
<url>
<loc>https://alfieking.dev/events</loc>
</url>
<url>
<loc>https://alfieking.dev/events/paws-n-pistons</loc>
</url>
<url>
<loc>https://alfieking.dev/events/crittersmk</loc>
</url>
</urlset>

View File

@@ -1,155 +0,0 @@
#snakeContainer {
padding: 15px;
}
canvas#snakeCanvas {
box-sizing: border-box;
border: 2px solid var(--secondary-background-color);
border-radius: 10px;
width: 100%;
}
form {
display: flex;
flex-direction: column;
width: min-content;
gap: 1rem;
padding: 15px;
}
form input[type="text"] {
padding: 10px;
box-sizing: border-box;
border: 1px solid var(--secondary-background-color);
border-radius: 6px;
background-color: var(--secondary-background-color-but-slightly-transparent);
color: var(--text-color);
}
form button[type="submit"] {
padding: 10px;
box-sizing: border-box;
border: 1px solid var(--secondary-background-color);
border-radius: 6px;
background-color: var(--secondary-background-color-but-slightly-transparent);
color: var(--text-color);
font-weight: bold;
cursor: pointer;
}
.flex-row {
display: flex;
flex-direction: row;
gap: 1rem;
}
.min-width {
width: min-content;
}
.max-width {
width: 100%;
}
#snakeLeaderboardSection {
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
max-height: 272px ;
}
#snakeLeaderboard {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
overflow-y: scroll;
}
#snakeLeaderboard li {
padding: 5px 20px;
background-color: var(--secondary-background-color-but-slightly-transparent);
color: var(--text-color);
font-size: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
#snakeLeaderboard li:nth-child(even) {
background-color: var(--secondary-background-color);
}
dialog {
width: 90%;
max-width: 500px;
padding: 20px;
background-color: var(--background-color);
color: var(--text-color);
border: 2px solid var(--secondary-background-color);
border-radius: 8px;
}
dialog button {
padding: 10px;
width: 100%;
box-sizing: border-box;
border: 2px solid var(--secondary-background-color);
border-radius: 6px;
background-color: var(--secondary-background-color-but-slightly-transparent);
color: var(--text-color);
font-weight: bold;
cursor: pointer;
margin-top: 10px;
}
dialog h2 {
margin: 0;
font-size: 1.5rem;
}
dialog p {
margin: 0;
font-size: 1rem;
}
@media screen and (max-width: 850px) {
.flex-row {
flex-direction: column;
}
form {
align-items: center;
width: 100%;
}
.min-width {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
}
@media screen and (max-width: 650px) {
.mobileOnly {
display: flex;
}
.pcOnly {
display: none;
}
}
@media screen and (min-width: 651px) {
.mobileOnly {
display: none !important;
}
.pcOnly {
display: flex;
}
}

View File

@@ -1,20 +0,0 @@
cap-widget {
--cap-background: var(--secondary-background-color-but-slightly-transparent);
--cap-border-color: var(--secondary-background-color);
--cap-border-radius: 14px;
--cap-widget-width: 230px;
--cap-widget-padding: 14px;
--cap-gap: 15px;
--cap-color: var(--text-color);
--cap-checkbox-size: 25px;
--cap-checkbox-border: 1px solid var(--secondary-background-color);
--cap-checkbox-border-radius: 6px;
--cap-checkbox-background: none;
--cap-checkbox-margin: 2px;
--cap-font: "Space Mono", "serif";
--cap-spinner-color: var(--primary-color);
--cap-spinner-background-color: var(--secondary-background-color-but-slightly-transparent);
--cap-spinner-thickness: 5px;
--cap-credits-font-size: 12px;
--cap-opacity-hover: 0.8;
}

13
static/css/directory.css Normal file
View File

@@ -0,0 +1,13 @@
section#directory ul {
list-style: none;
padding: 0;
margin: 0;
}
section#directory li {
margin: 0.5rem 0;
}
section#directory a:hover {
scale: 1.05;
}

17
static/css/gallery.css Normal file
View File

@@ -0,0 +1,17 @@
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
}
.gallery img {
max-width: 100%;
height: auto;
border-radius: 10px;
}
.gallery-date {
margin: 1rem 0 .25rem 0;
font-size: 2rem;
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Alfie's basement{% endblock %}</title>
<link rel="icon" href="/static/content/icon.webp">
<link rel="icon" href="/static/content/general_images/icon.webp">
<link rel="stylesheet" href="/static/css/base.css">
<meta name="description" content="{% block description %}server backend survivor{% endblock %}">
<meta name="keywords" content="{% block keywords %}Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server, developer, backend, selfhost, homelab{% endblock %}">
@@ -13,8 +13,8 @@
<meta name="theme-color" content="#63de90" data-react-helmet="true">
<meta property="og:site_name" content="Alfieking.dev">
<meta property="og:url" content="https://alfieking.dev/">
<meta property="og:title" content="{{ self.title() }}">
<meta property="og:description" content="{{ self.description() }}">
<meta property="og:title" content="{% block og-title %}Home - Alfie's basement{% endblock %}">
<meta property="og:description" content="{% block og-description %}server backend survivor{% endblock %}">
<meta property="og:image" content="{% block og_image %}/static/content/general_images/icon.webp{% endblock %}">
{% block head %}
{% endblock %}
@@ -28,6 +28,7 @@
<ul>
<li><a href="/">Home</a></li>
<li><a href="/toaster">Toaster</a></li>
<li><a href="/events">Events</a></li>
<li><a href="https://git.alfieking.dev/acetheking987">Gitea</a></li>
<li><a href="https://www.last.fm/user/acetheking987">LastFm</a></li>
<li><a href="https://prismic.alfieking.dev">Prismic</a></li>
@@ -36,7 +37,6 @@
<li><a href="https://www.youtube.com/@acetheking987">YouTube</a></li>
<li><a href="https://acetheking987.tumblr.com/">Tumblr</a></li>
<li><a href="https://www.reddit.com/user/acetheking987">Reddit</a></li>
<li><a href="/404">404 >:3</a></li>
</ul>
</section>
</nav>

View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}/{{ directory }} - Alfie's basement{% endblock %}
{% block description %}server backend survivor{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/directory.css">
{% endblock %}
{% block scripts %}
{% endblock %}
{% block content %}
<section id="directory">
<h1>/{{ directory }}</h1>
<ul>
{% for page in pages %}
<li><a href="/{{ directory }}{{ page.split('.')[0] }}">{{ page.split('.')[0] }}</a></li>
{% endfor %}
</ul>
</section>
{% endblock %}

View File

@@ -3,80 +3,18 @@
{% block title %}404 - Not Found{% endblock %}
{% block description %}The page you are looking for does not exist.{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/404.css">
<link rel="stylesheet" href="/static/css/cap.css">
{% endblock %}
{% block content %}
<section>
<h1>404</h1>
<p>
It seems like the thing you are looking for is not here :[
Hey so you know that thing you were looking for? Yeah, it doesn't exist. :P
<br><br>
<span class="pcOnly">
while you're here, why not play some snake?
</span>
<span class="mobileOnly">
You can't play snake on mobile, sorry :(
</span>
So why not try going back to the <a href="/">homepage</a>?
</p>
<div class="pcOnly" id="snakeContainer">
<canvas id="snakeCanvas"></canvas>
</div>
</section>
<section class="pcOnly flex-row">
<section class="min-width">
<h2>Submit score</h2>
<form action="/snake/submit" method="POST" id="snakeForm">
<input type="text" id="name" name="name" maxlength=15 minlength=3 placeholder="Your name" required>
<cap-widget id="captcha" data-cap-api-endpoint="https://cap.alfieking.dev/{{ cap_key }}/"></cap-widget>
<input type="hidden" id="score" name="score" value="0">
<input type="hidden" id="game_token" name="game_token" value="{{ token}}">
<button type="submit" id="submit">Submit</button>
</form>
</section>
<section class="max-width" id="snakeLeaderboardSection">
<h2>Leaderboard</h2>
<ul id="snakeLeaderboard">
{% for score in scores %}
<li>
<span>{{ score.position }}</span>
<span>{{ score.name }}</span>
<span>{{ score.score }}</span>
</li>
{% endfor %}
</ul>
</section>
</section>
<section class="max-width mobileOnly" id="snakeLeaderboardSection">
<h2>Leaderboard</h2>
<ul id="snakeLeaderboard">
{% for score in scores %}
<li>
<span>{{ score.position }}</span>
<span>{{ score.name }}</span>
<span>{{ score.score }}</span>
</li>
{% endfor %}
</ul>
</section>
{% if error %}
<dialog id="errorDialog">
<h2>Error</h2>
<p>{{ error }}</p>
<button onclick="errorDialog.close()">Close</button>
</dialog>
<script>
const errorDialog = document.getElementById('errorDialog');
if (errorDialog) {
errorDialog.showModal();
}
</script>
{% endif %}
{% endblock %}
{% block scripts %}
<script src="/static/js/snake.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
{% endblock %}
<section>
<h2>The actual error for the 2 ppl who care</h2>
<p>
404: {{ requested_url }} not found :3
</p>
</section>

View File

@@ -2,12 +2,15 @@
{% block title %}Home - Alfie's basement{% endblock %}
{% block description %}server backend survivor{% endblock %}
{% block og-title %}Home - Alfie's basement{% endblock %}
{% block og-description %}server backend survivor{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/index.css">
{% endblock %}
{%block content %}
{% block content %}
<section>
<h1>A lil bit abt me</h1>
<p>

View File

@@ -0,0 +1,45 @@
{% extends "bases/base.html" %}
{% block title %}Critters MK - Alfie's basement{% endblock %}
{% block description %}furry corner{% endblock %}
{% block og_image %}/static/content/Toaster_v1.0_Dark.png{% endblock %}
{% block keywords %}
Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server, developer, backend, selfhost, homelab, furry, protogen, toaster,
fursona, fur, furmeet, fursuit, persona, character, protogen fursona, protogen character, protogen fursona design,
protogen character design, critters mk, critters cmk, paws n pistons, paws'n'pistons, paws n pistons furry meet, paws'n'pistons furry meet,
protogen v1.0, toaster v1.0
{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/gallery.css">
{% endblock %}
{% block content %}
<section>
<h1>Critters MK</h1>
<p>
Critters Mk is a fur meet based in Milton Keynes, UK. They hold a meet once a month on saturdays around the area, usually at local parks. They are a
therian friendly group, and there are a variety of fursuiters and non-fursuiters that attend. The group is very welcoming to newcomers, and the meets
are a great way to meet new people in the furry community. There is also atleast one fursuit maker in the group to my knowledge, so if you are looking
for a fursuit, you may be able to find one there. The group is also very active on Telegram, and they have a Twitter page where they post updates about
the meets.
<br><br>
I have attended a few of the meets, and I enjoyed talking to the people there. I plan on attending every meet I can in the future, as well as any other local events
that I can find. If you are in the area, I highly recommend checking them out. You can find more information about the group on their
<a href="https://x.com/cmkfurmeet" target="_blank">Twitter page</a> (I don't know if I can link their Telegram group :<)
</p>
</section>
<section>
<h1>Photos :3</h1>
<p>
Here are some photos from the meets I have attended. I will add more as I attend more meets.
</p>
<h2 class="gallery-date">26th July 2025</h2>
<div class="gallery">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% extends "bases/base.html" %}
{% block title %}Paws'N'Pistons - Alfie's basement{% endblock %}
{% block description %}furry corner{% endblock %}
{% block og_image %}/static/content/Toaster_v1.0_Dark.png{% endblock %}
{% block keywords %}
Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server, developer, backend, selfhost, homelab, furry, protogen, toaster,
fursona, fur, furmeet, fursuit, persona, character, protogen fursona, protogen character, protogen fursona design,
protogen character design, critters mk, critters cmk, paws n pistons, paws'n'pistons, paws n pistons furry meet, paws'n'pistons furry meet,
protogen v1.0, toaster v1.0
{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/gallery.css">
<style>
#woooooo {
max-width: 50%;
height: auto;
border-radius: 10px;
margin: 10px;
}
@media (max-width: 600px) {
#woooooo {
max-width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<section>
<h1>Paws'N'Pistons</h1>
<p>
Paws'N'Pistons is a furry car meet that takes place across the UK. They are a very welcoming group that is open to all furries, regardless
of whether you have a car or not. Their meets last all day and involve multiple hours of driving across the country to various locations. They
also have a <a href="https://pawsnpistons.com" target="_blank">shop</a> where you can buy car stickers and other merch. They also hand out free
stickers at their meets. Their main social media is their <a href="https://www.instagram.com/paws_n_pistons/" target="_blank">Instagram</a>, where
they post photos from their meets and updates about upcoming events.
<br><br>
I've only attended one meet on the 3rd of August 2025, and it was a great experience. The people were very chill and I enjoyed driving around with them.
ALSO, one of them offered to let me try their fursuit!!! Im now going to speedrun going broke trying to get a fursuit of my own cus of this :3 (I was
already planning on getting one, but this just made me want one more).
<br>
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_200906329.jpg" alt="me in a fursuit" id="woooooo">
<br>
The fursuit belongs to <a href="https://www.tiktok.com/@trickythefox" target="_blank">Tricky the Fox</a>, they are a very chill person and I had a great time talking to them ^w^.
<h1>Photos :3</h1>
<p>
Here are some photos from the meets I have attended. I will add more as I attend more meets.
</p>
<h2 class="gallery-date">3rd Aug 2025</h2>
<div class="gallery">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150249916.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_140629639.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141242090.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_182023562.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_184321576.jpg" alt="Paws'N'Pistons">
</div>
</section>
{% endblock %}

View File

@@ -84,16 +84,16 @@ protogen v1.0, toaster v1.0
</p>
<ul id="fur-meets">
<li>
<a href="https://x.com/cmkfurmeet"><b>CrittersCMK</b></a> - A furmeet in Milton Keynes.
<a href="/events/crittersmk"><b>Critters MK</b></a> - A furmeet in Milton Keynes.
<div class="fur-meet-gallery-small">
<img src="/static/content/fur_meets/26-08-2025_critters_mk/PXL_20250726_152110445.jpg" alt="CrittersCMK">
<img src="/static/content/fur_meets/26-08-2025_critters_mk/PXL_20250726_155134418.jpg" alt="CrittersCMK">
<img src="/static/content/fur_meets/26-08-2025_critters_mk/PXL_20250726_155226274.jpg" alt="CrittersCMK">
<img src="/static/content/fur_meets/26-08-2025_critters_mk/PXL_20250726_155434701.jpg" alt="CrittersCMK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
</div>
</li>
<li>
<a href="https://www.instagram.com/paws_n_pistons/"><b>Paws'N'Pistons</b></a> - A furry car meet around the UK.
<a href="/events/paws-n-pistons"><b>Paws'N'Pistons</b></a> - A furry car meet around the UK.
<div class="fur-meet-gallery-small">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
@@ -101,6 +101,7 @@ protogen v1.0, toaster v1.0
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
</div>
</li>
<p>Click on the links to view more photos from each event :3</p>
</ul>
</section>
{% endblock %}

1
thirdparty/inja vendored Submodule

Submodule thirdparty/inja added at 593ff96024

1
thirdparty/nlohmann vendored Submodule

Submodule thirdparty/nlohmann added at 000db7a6a2