diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cca1ed7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv +.env +db.sqlite +flask_session \ No newline at end of file diff --git a/db.sqlite b/db.sqlite new file mode 100644 index 0000000..a16919c Binary files /dev/null and b/db.sqlite differ diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..145aa9e --- /dev/null +++ b/dockerfile @@ -0,0 +1,25 @@ +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 + +# Set environment variables +ENV FLASK_APP=main.py + +# run the application +ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.main:app" ] \ No newline at end of file diff --git a/flask_session/2029240f6d1128be89ddc32729463129 b/flask_session/2029240f6d1128be89ddc32729463129 new file mode 100644 index 0000000..60b84f8 Binary files /dev/null and b/flask_session/2029240f6d1128be89ddc32729463129 differ diff --git a/index.html b/index.html deleted file mode 100644 index ee49c02..0000000 --- a/index.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - Alfie's basement - - - - - - - - - - - - - - -
- -
-
-
- -
-

Alfie King

-

server backend survivor

-
-
-
- -
-

A lil bit abt me

-

- Im not good with writing so dont expect much here. I am a student who is learning c++ and python. I've Done a few projects that i think - are decent enough to show off, so I have put them on this website. I like to mess around with linux and have a few servers that I run. I've - been running a server for a few years now, and I have learned a lot from it. I have also switched to linux on my main computer, which has been - slightly annoying at times (mainly because one of my most played games' anticheat doesn't support on linux atm. Also, the lack of photoshop is - a pain). -

- I would like to make some more projects in the future, but I am not sure what I want to make yet. I tend to make thing on impulse a lot, and motivation - is "lacking" at times. So the few ideas I do have may never come to fruition. I hope to get better at art so i could hopefully make a game that is somewhat - interesting. But im at a lack of ideas at the moment. -

- I would also like to have a functional blog on this site, but I bearly talk about much so I dont know what I would write about. I like to ramble on about - random things, but I dont think that would be very interesting to read, and I think that I would forget to update it. I have a tumblr that I have had for a few - years now, but I dont post on it (the social anxiety is too much for me :<). However I hope to get better at that in the future. -

-
-
- - - - - - - - -
-
- -
-

-

-
-
-
- - - - - - - - - - - - - - - - -
-
-
-

Projects & stuff

-

just some projects ive worked on over time

- -
-
-

The button collection™

-
-
-
-
-

Some News

-
(dont expect this to be updated often tho :P)
- -
-
- -
-
- - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f87e99e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv +flask-session +requests +flask \ No newline at end of file diff --git a/src/__pycache__/database.cpython-313.pyc b/src/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..7d2b662 Binary files /dev/null and b/src/__pycache__/database.cpython-313.pyc differ diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..5b03fdc --- /dev/null +++ b/src/database.py @@ -0,0 +1,42 @@ +import sqlite3 + +class Database: + def __init__(self, db_name='db.sqlite'): + self.connection = sqlite3.connect(db_name, check_same_thread=False) + self.cursor = self.connection.cursor() + self.create_snake_table() + + def create_snake_table(self): + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS snake ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + score INTEGER NOT NULL + ) + ''') + self.connection.commit() + + def insert_snake(self, name, score): + old_score = self.get_snake_score(name) + print(f"Old score for {name}: {old_score}") + print(f"New score for {name}: {score}") + if old_score is not None and score <= old_score: + return + + self.cursor.execute(''' + INSERT INTO snake (name, score) + VALUES (?, ?) + ''', (name, score)) + self.connection.commit() + + def get_snake_score(self, name): + self.cursor.execute('SELECT score FROM snake WHERE name = ? ORDER BY score DESC LIMIT 1', (name,)) + result = self.cursor.fetchone() + return result[0] if result else None + + def get_snake_scores(self): + self.cursor.execute('SELECT * FROM snake ORDER BY score DESC') + return self.cursor.fetchall() + + def close(self): + self.connection.close() \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..5c298b2 --- /dev/null +++ b/src/main.py @@ -0,0 +1,110 @@ +from flask import Flask, request, render_template, send_from_directory +from flask_session import Session +from dotenv import load_dotenv +from os import getenv as env +import logging, requests +try: + import src.database as database +except ImportError: + import database + + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +load_dotenv() + + +app = Flask( + __name__, + template_folder=env('TEMPLATE_FOLDER', default='../templates'), + static_folder=env('STATIC_FOLDER', default='../static'), + static_url_path=env('STATIC_URL_PATH', default='/static') +) +app.config["SESSION_PERMANENT"] = True +app.config["SESSION_TYPE"] = "filesystem" +Session(app) + + +db = database.Database(db_name=env('DB_NAME', default='db.sqlite')) + + +@app.route('/') +def index(): + logging.info("Rendering index page") + return render_template('index.html') + + +@app.route('/robots.txt') +@app.route('/sitemap.xml') +@app.route('/favicon.ico') +def web_stuffs(): + return send_from_directory( + app.static_folder, + request.path[1:], + ) + + +@app.route('/404') +@app.errorhandler(404) +def not_found(): + unformatted_scores = db.get_snake_scores() + scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] + return render_template('404.html', scores=scores) + + +@app.route('/404/submit', methods=['POST']) +def snake_submit(): + unformatted_scores = db.get_snake_scores() + scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] + + data = request.form + username = data.get('username', '').strip() + score = data.get('snake-score', '').strip() + token = data.get('cap-token', '').strip() + + if not username or not score or not token: + logging.error("Missing required fields: username=%s, score=%s, token=%s", username, score, token) + return render_template('404.html', scores=scores, error='Missing required fields'), 400 + + try: + score = int(score) + except ValueError: + logging.error("Invalid score value: %s", score) + return render_template('404.html', scores=scores, error='Invalid score value'), 400 + + if score <= 0 or score > 10000 or len(username) < 3 or len(username) > 20: + logging.error("Invalid score or username length: score=%s, username=%s", score, username) + return render_template('404.html', scores=scores, error='Invalid score or username length'), 400 + + cap_response = requests.post( + env('CAP_VERIFY_URL', default='https:////siteverify'), + json={ + 'secret': env('CAP_SECRET', default=''), + 'response': token, + } + ) + + if cap_response.status_code != 200 or not cap_response.json().get('success', "false") != "true": + logging.error("Captcha verification failed: %s", cap_response.json()) + return render_template('404.html', scores=scores, error='Captcha verification failed'), 400 + + db.insert_snake(name=username, score=int(score)) + logging.info("Snake submitted: name=%s, score=%d", username, score) + + unformatted_scores = db.get_snake_scores() + scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] + return render_template('404.html', scores=scores, success='Score submitted successfully!') + + +@app.route('/500') +@app.errorhandler(500) +def internal_error(error="An internal server error occurred."): + logging.error("Internal server error: %s", error) + return render_template('500.html'), 500 + + +if __name__ == '__main__': + app.run( + host=env('HOST', default='0.0.0.0'), + port=env('PORT', default=5000), + debug=env('DEBUG', default=False).lower() == 'true' + ) \ No newline at end of file diff --git a/static/css/404.css b/static/css/404.css new file mode 100644 index 0000000..ff24cf5 --- /dev/null +++ b/static/css/404.css @@ -0,0 +1,113 @@ +canvas#snakeCanvas { + margin: 15px; + box-sizing: border-box; + border: 2px solid var(--secondary-background-color); + border-radius: 10px; +} + +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; +} \ No newline at end of file diff --git a/static/css/500.css b/static/css/500.css new file mode 100644 index 0000000..2f74df6 --- /dev/null +++ b/static/css/500.css @@ -0,0 +1,8 @@ +.bluescreen { + background-color: #0077D6; + color: #fff; + padding:40px; + display: flex; + flex-direction: column; + gap: 20px; +} \ No newline at end of file diff --git a/static/css/index.css b/static/css/base.css similarity index 100% rename from static/css/index.css rename to static/css/base.css diff --git a/static/css/cap.css b/static/css/cap.css new file mode 100644 index 0000000..d978715 --- /dev/null +++ b/static/css/cap.css @@ -0,0 +1,21 @@ +cap-widget { + --cap-background: var(--secondary-background-color-but-slightly-transparent); + --cap-border-color: var(--secondary-background-color); + --cap-border-radius: 14px; + --cap-widget-height: 30px; + --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; +} \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..0d875df Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/js/index.js b/static/js/base.js similarity index 99% rename from static/js/index.js rename to static/js/base.js index b0f6ce8..1c983a7 100644 --- a/static/js/index.js +++ b/static/js/base.js @@ -1,5 +1,3 @@ -// TYPERWRITER - const values = [ "Web developer", "Pc games enjoyer", diff --git a/static/js/snake.js b/static/js/snake.js new file mode 100644 index 0000000..61a4edc --- /dev/null +++ b/static/js/snake.js @@ -0,0 +1,174 @@ +const canvas = document.getElementById('snakeCanvas'); +const ctx = canvas.getContext('2d'); + +const gridSize = 20; +const tileSize = 100; +const snakeSize = 60; +const foodSize = 80; +canvas.width = gridSize * tileSize; +canvas.height = gridSize * tileSize; + +let snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }]; +let direction = { x: 0, y: 0 }; +let food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; +let score = 0; +let gameOver = false; + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // draw grid of checkerboard pattern + for (let x = 0; x < gridSize; x++) { + for (let y = 0; y < gridSize; y++) { + ctx.fillStyle = (x + y) % 2 === 0 ? + getComputedStyle(document.documentElement).getPropertyValue('--background-color') : + getComputedStyle(document.documentElement).getPropertyValue('--secondary-background-color'); + ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + } + + // Draw snake + snake.forEach(segment => { + let nextVec = { x: 0, y: 0 }; + // if there is a segment after the current segment + if (snake.indexOf(segment) < snake.length - 1) { + const nextSegment = snake[snake.indexOf(segment) + 1]; + nextVec.x = nextSegment.x - segment.x; + nextVec.y = nextSegment.y - segment.y; + } + + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary-color'); + if (nextVec.x === 0 && nextVec.y === 0) { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2, + segment.y * tileSize + (tileSize - snakeSize) / 2, + snakeSize, + snakeSize + ); + } else if (nextVec.x > 0 || nextVec.y > 0) { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2, + segment.y * tileSize + (tileSize - snakeSize) / 2, + snakeSize + nextVec.x * (tileSize - snakeSize), + snakeSize + nextVec.y * (tileSize - snakeSize) + ); + } else { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2 + nextVec.x * (tileSize - snakeSize), + segment.y * tileSize + (tileSize - snakeSize) / 2 + nextVec.y * (tileSize - snakeSize), + snakeSize + Math.abs(nextVec.x) * (tileSize - snakeSize), + snakeSize + Math.abs(nextVec.y) * (tileSize - snakeSize) + ); + } + + }); + + // Draw food + ctx.fillStyle = '#ff4d4d'; + ctx.fillRect( + food.x * tileSize + (tileSize - foodSize) / 2, + food.y * tileSize + (tileSize - foodSize) / 2, + foodSize, + foodSize + ); +} + +function update() { + if (gameOver) return; + + // Move snake + const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; + + // Add new head + snake.unshift(head); + + // Check for food collision + if (head.x === food.x && head.y === food.y) { + score += 10; // Increase score + placeFood(); + } else { + snake.pop(); // Remove tail if no food eaten + } + + // Check for wall collision + if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) { + gameOver = true; + return; + } + + // Check for self collision + for (let i = 1; i < snake.length; i++) { + if (head.x === snake[i].x && head.y === snake[i].y) { + gameOver = true; + return; + } + } +} + +function placeFood() { + do { + food.x = Math.floor(Math.random() * gridSize); + food.y = Math.floor(Math.random() * gridSize); + } while (snake.some(segment => segment.x === food.x && segment.y === food.y)); +} + +function changeDirection(event) { + switch (event.key) { + case 'w': + if (direction.y === 0) direction = { x: 0, y: -1 }; + break; + case 's': + if (direction.y === 0) direction = { x: 0, y: 1 }; + break; + case 'a': + if (direction.x === 0) direction = { x: -1, y: 0 }; + break; + case 'd': + if (direction.x === 0) direction = { x: 1, y: 0 }; + break; + } +} + +// Menu to start the game +function menu() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--background-color'); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.textAlign = 'center'; + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-color'); + ctx.font = '200px Arial'; + ctx.fillText('Snake Game', canvas.width / 2, canvas.height / 2); + ctx.font = '100px Arial'; + ctx.fillText('Press W/A/S/D to move', canvas.width / 2, canvas.height / 2 + 100); + ctx.fillText('Click to start', canvas.width / 2, canvas.height / 2 + 200); + + canvas.addEventListener('click', startGame); +} + +function gameLoop() { + if (!gameOver) { + update(); + draw(); + setTimeout(gameLoop, 100); + } else { + document.removeEventListener('keydown', changeDirection); + document.getElementById('snake-score').value = score; + alert(`Game Over! Your score: ${score}`); + menu(); + } +} + +function startGame() { + snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }]; + direction = { x: 1, y: 0 }; + food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; + score = 0; + gameOver = false; + canvas.removeEventListener('click', startGame); + document.addEventListener('keydown', changeDirection); + gameLoop(); +} + +menu(); \ No newline at end of file diff --git a/robots.txt b/static/robots.txt similarity index 100% rename from robots.txt rename to static/robots.txt diff --git a/sitemap.xml b/static/sitemap.xml similarity index 100% rename from sitemap.xml rename to static/sitemap.xml diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..6ac41f9 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block title %}404 - Not Found{% endblock %} +{% block description %}The page you are looking for does not exist.{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+

404

+

+ It seems like the thing you are looking for is not here :[ +

+ while you're here, why not play some snake? +

+ +
+
+
+

Submit score

+
+ + + + +
+
+
+

Leaderboard

+
    + {% for score in scores %} +
  • + {{ score.position }} + {{ score.name }} + {{ score.score }} +
  • + {% endfor %} +
+
+
+{% if error %} + +

Error

+

{{ error }}

+ +
+ +{% endif %} +{% endblock %} + +{% block scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..ab6dfe4 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}500 - Internal Server Error{% endblock %} +{% block description %}An unexpected error occurred on the server.{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+

:(

+

+ Oopsie Woopsie! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this! +

+
+{% endblock %} \ No newline at end of file diff --git a/404.html b/templates/base.html similarity index 74% rename from 404.html rename to templates/base.html index 4b902f4..dbe782e 100644 --- a/404.html +++ b/templates/base.html @@ -3,19 +3,21 @@ - Alfie's basement + {% block title %}Alfie's basement{% endblock %} - - + + - - + + + {% block head %} + {% endblock %}
@@ -31,8 +33,9 @@
  • Spotify
  • Steam
  • YouTube
  • -
  • Tumblr
  • Instagram +
  • Tumblr
  • Reddit
  • +
  • 404 >:3
  • @@ -58,7 +61,7 @@
          |\      _,,,---,,_
    ZZZzz /,`.-'`' -. ;-;;,_
    |,4- ) )-,_. ,\ ( `'-'
    '---''(_/--' `-'\_)
    - haj + haj
    @@ -70,18 +73,26 @@
    -
    -

    404

    -

    - It seems like the thing you are looking for is not here :[ -

    -
    + + {% block content %}{% endblock %}

    legal stuff idk :3 | icba to © this :P | made with ♥ and caffine

    - + {% block scripts %}{% endblock %} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..6f33ca9 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,115 @@ +{% extends "base.html" %} + +{% block title %}Alfie's basement{% endblock %} +{% block description %}server backend survivor{% endblock %} + +{%block content %} +
    +

    A lil bit abt me

    +

    + Im not good with writing so dont expect much here. I am a student who is learning c++ and python. I've Done a few projects that i think + are decent enough to show off, so I have put them on this website. I like to mess around with linux and have a few servers that I run. I've + been running a server for a few years now, and I have learned a lot from it. I have also switched to linux on my main computer, which has been + slightly annoying at times (mainly because one of my most played games' anticheat doesn't support on linux atm. Also, the lack of photoshop is + a pain). +

    + I would like to make some more projects in the future, but I am not sure what I want to make yet. I tend to make thing on impulse a lot, and motivation + is "lacking" at times. So the few ideas I do have may never come to fruition. I hope to get better at art so i could hopefully make a game that is somewhat + interesting. But im at a lack of ideas at the moment. +

    + I would also like to have a functional blog on this site, but I bearly talk about much so I dont know what I would write about. I like to ramble on about + random things, but I dont think that would be very interesting to read, and I think that I would forget to update it. I have a tumblr that I have had for a few + years now, but I dont post on it (the social anxiety is too much for me :<). However I hope to get better at that in the future. +

    +
    +
    + + + + + + + + +
    +
    + +
    +

    +

    +
    +
    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +

    Projects & stuff

    +

    just some projects ive worked on over time

    +
      +
    • +

      alfieking.dev

      +

      + This website is a project that I have been working on for a while now. I have made a few versions of it, but I have + never been happy with them. I am quite happy with this version atm since it is more organized and has a design that I + like. + source code +

      +
    • +
    • +

      owo (the terminal command)

      +

      + I made this project as a joke, I can't remember exactly what I baised it off other than the fact that it was somthing + similar to this but in a different language. I originally made it in python, but I have since rewritten it in c++ so + that it would be faster and so that I could learn c++. + source code +

      +
    • +
    • +

      prismic

      +

      + Prismic is a basic message board that I made, it was mainly made to learn how to use templating and more backend + web development. it uses flask for the web framework and uses a sqlite database to store the messages. I have thought + about remaking it in c++ since I found a c++ web framework that I would like to try out. + Primic | source code +

      +
    • +
    +
    +
    +

    The button collection™

    +
    +
    +
    +
    +

    Some News

    +
    (dont expect this to be updated often tho :P)
    +
      +
    • +

      18-06-2025

      +

      + I plan on updating the site soon, I have a few ideas that I want to implement. I want to make it more + interactive and fun to use. I also want to add a blog section so that I can write about random things that I find interesting. I also + want to add a few more projects that I have been working on. Annoyingly I think it would be a good idea to remake this site with some sort of + framework so i can use templating, however this kinda bothers me since I like the simplicity of this site. And prefer to keep it as a static site + that i can just throw at nginx. +

      +
    • +
    +
    +{% endblock %} \ No newline at end of file