3 Commits
Author SHA1 Message Date
jake 363497d354 Add Plausible 2026-04-05 20:46:41 +01:00
jake b97c9ea3ee First implementation of comments 2026-02-19 12:58:42 +00:00
jake 82def98e7e First implementation of comments 2026-02-19 12:46:43 +00:00
14 changed files with 137 additions and 110 deletions
-3
View File
@@ -439,9 +439,6 @@ Alias "/static" "/var/www/jc/static/"
</Directory> </Directory>
Alias "/robots.txt" "/var/www/jc/static/robots.txt" Alias "/robots.txt" "/var/www/jc/static/robots.txt"
Alias "/.well-known/security.txt" "/var/www/jc/static/security.txt"
Alias "/llms.txt" "/var/www/jc/static/llms.txt"
<Files jc.wsgi> <Files jc.wsgi>
Require all granted Require all granted
+2 -2
View File
@@ -6,7 +6,7 @@ set -x -o pipefail
content_dir="" content_dir=""
if [[ -d "../jc-content" ]]; then if [[ -d "../jc-content" ]]; then
content_dir="-v $(realpath ../jc-content):/var/www/jc/projects:z" content_dir="-v $(realpath ../jc-content):/var/www/jc/projects"
fi fi
docker run -e DISCORD_ERR_HOOK=dummy $1 $content_dir jc-ng-localtest docker run -e DISCORD_ERR_HOOK=dummy $1 -v $(pwd)/src/:/var/www/jc $content_dir jc-ng-localtest
+4 -2
View File
@@ -16,6 +16,7 @@ from .content import ContentArea
from .contact import ContactForm from .contact import ContactForm
from .storage import LocalStorage from .storage import LocalStorage
from .links import Links from .links import Links
from .comments import *
app = Flask(__name__) app = Flask(__name__)
@@ -29,6 +30,7 @@ projects = ContentArea(
app.register_blueprint(projects, url_prefix='/projects') app.register_blueprint(projects, url_prefix='/projects')
app.register_blueprint(ContactForm('contact', __name__), url_prefix='/contact') app.register_blueprint(ContactForm('contact', __name__), url_prefix='/contact')
app.register_blueprint(Links(path.join(md_path, 'links.json'), 'links', __name__), url_prefix='/links') app.register_blueprint(Links(path.join(md_path, 'links.json'), 'links', __name__), url_prefix='/links')
app.register_blueprint(Approval(path.join(projects.md_directory.uri, 'comments.db'), 'comments', __name__), url_prefix='/comments')
class DiscordLogger(logging.Handler): class DiscordLogger(logging.Handler):
''' Simple logging handler to send a message to Discord ''' ''' Simple logging handler to send a message to Discord '''
@@ -72,7 +74,7 @@ def inject_branding() -> dict:
case _: case _:
brand = req_domain brand = req_domain
return {'branding': brand, 'url': req_domain} return {'branding': brand}
@app.route('/') @app.route('/')
def index() -> str: def index() -> str:
@@ -140,7 +142,7 @@ def sitemap():
url = ET.SubElement(root, 'url') url = ET.SubElement(root, 'url')
ET.SubElement(url, 'loc').text = base_url + route ET.SubElement(url, 'loc').text = base_url + route
ET.SubElement(url, 'lastmod').text = date ET.SubElement(url, 'lastmod').text = date
for article in projects.get_all_posts(): for article in projects.get_live_posts():
if 'link' in article.metadata: if 'link' in article.metadata:
continue continue
url = ET.SubElement(root, 'url') url = ET.SubElement(root, 'url')
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/python3
import sqlite3
from flask import Blueprint, Response, request
from uuid import uuid4
from requests import post
from os import environ
class PostComments():
def __init__(self, post_id: str, db_path: str):
self.__db_path = db_path
self.__post_id = post_id
self._webhook = environ['DISCORD_WEBHOOK']
with sqlite3.connect(db_path) as db:
cursor = db.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS comments (name TEXT, comment TEXT, date INT, post_id TEXT, approved BOOL, key TEXT)")
self.comments = cursor.execute(
"SELECT name, comment, date FROM comments WHERE approved = 1 AND post_id = ? ORDER BY date DESC",
(post_id,)
).fetchall()
def send_to_discord(self, name: str, comment: str, comment_id: int, key: str):
''' Send the message '''
message_to_send = f'New comment from {name}\n\n{comment}'
if len(message_to_send) > 2000:
chars_to_lose = len(message_to_send) - 1900
message_to_send = message_to_send[-chars_to_lose:]
message_to_send += f'\n\n[Approve](https://jakecharman.co.uk/comments/approve/{comment_id}?key={key})'
post(self._webhook, data={'content': message_to_send}, timeout=30)
def make_comment(self, name: str, comment: str):
with sqlite3.connect(self.__db_path) as db:
key = str(uuid4())
cursor = db.cursor()
cursor.execute(
"INSERT INTO comments (name, comment, date, post_id, approved, key) VALUES (?, ?, datetime('now'), ?, 0, ?)",
(name, comment, self.__post_id, key)
)
db.commit()
self.send_to_discord(name, comment, cursor.lastrowid, key)
return cursor.lastrowid
class Approval(Blueprint):
def __init__(self, db_path: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__db_path = db_path
self.add_url_rule('/approve/<comment_id>', view_func=self.approve)
def approve(self, comment_id: str):
with sqlite3.connect(self.__db_path) as db:
cursor = db.cursor()
key = cursor.execute("SELECT key FROM comments WHERE rowid = ?", (comment_id,)).fetchone()[0]
if request.args.get('key') == key:
with sqlite3.connect(self.__db_path) as db:
cursor = db.cursor()
cursor.execute("UPDATE comments SET approved = 1 WHERE rowid = ?", (comment_id,))
db.commit()
return Response(status=200)
return Response(status=403)
+14 -3
View File
@@ -6,8 +6,9 @@ from datetime import datetime
import frontmatter import frontmatter
from markdown import markdown from markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import render_template, Response, Blueprint from flask import render_template, Response, Blueprint, request, redirect
from .storage import LocalStorage from .storage import LocalStorage
from .comments import PostComments, Approval
class ContentArea(Blueprint): class ContentArea(Blueprint):
def __init__(self, directory: LocalStorage, *args, **kwargs): def __init__(self, directory: LocalStorage, *args, **kwargs):
@@ -24,7 +25,7 @@ class ContentArea(Blueprint):
self.add_url_rule('/', view_func=self.projects) self.add_url_rule('/', view_func=self.projects)
self.add_url_rule('/category/<category_id>/', view_func=self.category) self.add_url_rule('/category/<category_id>/', view_func=self.category)
self.add_url_rule('/<article_id>', view_func=self.article) self.add_url_rule('/<article_id>', view_func=self.article)
#self.add_url_rule('/image/<image_name>', view_func=self.image) self.add_url_rule('/<article_id>/comment', view_func=self.comment, methods=['POST'])
def processor(self) -> dict: def processor(self) -> dict:
''' Jninja processors ''' ''' Jninja processors '''
@@ -140,6 +141,16 @@ class ContentArea(Blueprint):
return Response(status=500) return Response(status=500)
the_article = articles[0] the_article = articles[0]
comments = PostComments(the_article.metadata['id'], path.join(self.md_directory.uri, 'comments.db')).comments
return render_template('article.html', post=markdown(the_article.content), return render_template('article.html', post=markdown(the_article.content),
metadata=the_article.metadata, metadata=the_article.metadata,
page_title=f'{the_article.metadata["title"]} - ') comments = comments,
page_title=f'{the_article.metadata["title"]} - ')
def comment(self, article_id: str):
PostComments(article_id, path.join(self.md_directory.uri, 'comments.db')).make_comment(
request.form['name'],
request.form['comment']
)
return redirect(f'/projects/{article_id}?comment=true#comments')
+20 -26
View File
@@ -1,5 +1,5 @@
{% extends 'main.html' %} {% extends 'main.html' %}
{% block description %}{{ metadata.description }}{% endblock %}
{% block head %} {% block head %}
{% if metadata.gallery %} {% if metadata.gallery %}
<script> <script>
@@ -11,31 +11,6 @@
}); });
</script> </script>
{% endif %} {% endif %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ metadata.title | tojson }},
"datePublished": "{{ metadata.date }}",
"image": "https://{{ url }}/image/{{ metadata.image }}",
"author": {
"@type": "Person",
"name": "Jake Charman",
"url": "https://{{ url }}/"
},
"mainEntityOfPage": "https://{{ url }}/projects/{{ metadata.id }}"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Projects", "item": "https://{{url}}/projects/"},
{"@type": "ListItem", "position": 2, "name": {{ metadata.title | tojson }}, "item": "https://{{url}}/projects/{{ metadata.id }}"}
]
}
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -47,4 +22,23 @@
{{post|safe}} {{post|safe}}
</section> </section>
</main> </main>
<section id="comments">
<h2>{{comments | length}} Comment{% if comments | length != 1 %}s{% endif %}</h2>
{% for comment in comments %}
<div class="comment">
<strong>{{ comment[0] }} - {{ comment[2] | human_date }}</strong>
<p>{{ comment[1] }}</p>
</div>
{% endfor %}
<h3>Leave a comment</h3>
{% if request.args.get('comment') is none %}
<form action="./{{metadata.id}}/comment" method="post">
<input type="text", name="name" placeholder="Name" required>
<textarea name="comment" placeholder="Comment" required></textarea>
<input type="submit" value="Submit">
</form>
{% else %}
<p>Thank you! Your comment will appear once it is approved</p>
{% endif %}
</section>
{% endblock %} {% endblock %}
+24 -26
View File
@@ -1,7 +1,5 @@
{% extends 'main.html' %} {% extends 'main.html' %}
{% block logo %}<a href='/'><h1>{{branding|upper}}</h1></a>{% endblock %}
{% block content %} {% block content %}
<main> <main>
<section id="technology"> <section id="technology">
@@ -20,31 +18,31 @@
My personal projects can also be found over on the <a href="/projects/">projects</a> page. Be aware though that if I'm able to write about a project here, it most likely wasn't done professionally so some may be a little rough around the edges. My personal projects can also be found over on the <a href="/projects/">projects</a> page. Be aware though that if I'm able to write about a project here, it most likely wasn't done professionally so some may be a little rough around the edges.
</p> </p>
<div id="techlogos"> <div id="techlogos">
<a href="https://ansible.com"><img alt="Ansible Logo" src="/static/images/technology/ansible.png" /></a> <a href="https://ansible.com"><img src="/static/images/technology/ansible.png" /></a>
<a href="https://aws.amazon.com/"><img alt="AWS Logo" src="/static/images/technology/aws.png" /></a> <a href="https://aws.amazon.com/"><img src="/static/images/technology/aws.png" /></a>
<a href="https://azure.microsoft.com/en-gb"><img alt="Azure Logo" src="/static/images/technology/azure.png" /></a> <a href="https://azure.microsoft.com/en-gb"><img src="/static/images/technology/azure.png" /></a>
<a href="https://dotnet.microsoft.com/en-us/languages/csharp"><img alt="C# Logo" src="/static/images/technology/csharp.png" /></a> <a href="https://dotnet.microsoft.com/en-us/languages/csharp"><img src="/static/images/technology/csharp.png" /></a>
<a href="https://www.debian.org/"><img alt="Debian Logo" src="/static/images/technology/debian.png" /></a> <a href="https://www.debian.org/"><img src="/static/images/technology/debian.png" /></a>
<a href="https://www.digitalocean.com/"><img alt="DigitalOcean Logo" src="/static/images/technology/digitalocean.png" /></a> <a href="https://www.digitalocean.com/"><img src="/static/images/technology/digitalocean.png" /></a>
<a href="https://www.docker.com/"><img alt="Docker Logo" src="/static/images/technology/docker.png" /></a> <a href="https://www.docker.com/"><img src="/static/images/technology/docker.png" /></a>
<a href="https://www.freeipa.org/"><img alt="FreeIPA Logo" src="/static/images/technology/freeipa.png" /></a> <a href="https://www.freeipa.org/"><img src="/static/images/technology/freeipa.png" /></a>
<a href="https://cloud.google.com/"><img alt="Google Cloud Logo" src="/static/images/technology/gcloud.png" /></a> <a href="https://cloud.google.com/"><img src="/static/images/technology/gcloud.png" /></a>
<a href="https://git-scm.com/"><img alt="Git Logo" src="/static/images/technology/git.png" /></a> <a href="https://git-scm.com/"><img src="/static/images/technology/git.png" /></a>
<a href="https://www.grafana.com/"><img alt="Grafana Logo" src="/static/images/technology/grafana.png" /></a> <a href="https://www.grafana.com/"><img src="/static/images/technology/grafana.png" /></a>
<a href="https://hadoop.apache.org/"><img alt="Hadoop Logo" src="/static/images/technology/hadoop.svg" /></a> <a href="https://hadoop.apache.org/"><img src="/static/images/technology/hadoop.svg" /></a>
<a href="https://hive.apache.org/"><img alt="Hive Logo" src="/static/images/technology/hive.svg" /></a> <a href="https://hive.apache.org/"><img src="/static/images/technology/hive.svg" /></a>
<a href="https://www.java.com/"><img alt="Java Logo" src="/static/images/technology/java.png" /></a> <a href="https://www.java.com/"><img src="/static/images/technology/java.png" /></a>
<a href="https://www.jenkins.io/"><img alt="Jenkins Logo" src="/static/images/technology/jenkins.png" /></a> <a href="https://www.jenkins.io/"><img src="/static/images/technology/jenkins.png" /></a>
<a href="https://mariadb.org/"><img alt="MariaDB Logo" src="/static/images/technology/mariadb.png" /></a> <a href="https://mariadb.org/"><img src="/static/images/technology/mariadb.png" /></a>
<a href="https://nginx.org/"><img alt="NGINX Logo" src="/static/images/technology/nginx.svg" /></a> <a href="https://nginx.org/"><img src="/static/images/technology/nginx.svg" /></a>
<a href="https://www.proxmox.com/"><img alt="Proxmox Logo" src="/static/images/technology/proxmox.png" /></a> <a href="https://www.proxmox.com/"><img src="/static/images/technology/proxmox.png" /></a>
<a href="https://www.python.org/"><img alt="Python Logo" src="/static/images/technology/python.png" /></a> <a href="https://www.python.org/"><img src="/static/images/technology/python.png" /></a>
<a href="https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux"><img alt="RedHat Logo" src="/static/images/technology/redhat.png" /></a> <a href="https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux"><img src="/static/images/technology/redhat.png" /></a>
<a href="https://rockylinux.org/"><img alt="Rocky Linux Logo" src="/static/images/technology/rocky.png" /></a> <a href="https://rockylinux.org/"><img src="/static/images/technology/rocky.png" /></a>
<a href="https://subversion.apache.org/"><img alt="Subversion Logo" src="/static/images/technology/svn.svg" /></a> <a href="https://subversion.apache.org/"><img src="/static/images/technology/svn.svg" /></a>
</div> </div>
<div id="certs"> <div id="certs">
<a href="https://catalog-education.oracle.com/ords/certview/sharebadge?id=A6C9B3D3D21627EB9C2504395194FAF772E01412911D4ADC78063133D54579ED"><img alt="Oracle Cloud certification badge" src="/static/images/certs/OCIF2023CA.png" /></a> <a href="https://catalog-education.oracle.com/ords/certview/sharebadge?id=A6C9B3D3D21627EB9C2504395194FAF772E01412911D4ADC78063133D54579ED"><img src="/static/images/certs/OCIF2023CA.png" /></a>
</div> </div>
<div class="social"> <div class="social">
<a class="button" href="https://www.linkedin.com/in/jakecharman/"><i class="fa-brands fa-linkedin-in"></i></a> <a class="button" href="https://www.linkedin.com/in/jakecharman/"><i class="fa-brands fa-linkedin-in"></i></a>
@@ -66,7 +64,7 @@
<p>I've also had the opportunity to work on a 10,000 horsepower Top Fuel Dragster. I <a href="https://jakecharman.co.uk/projects/topfuel_dragster">wrote about this on the Nitro Junkie website, and later extended that post here.</a></p> <p>I've also had the opportunity to work on a 10,000 horsepower Top Fuel Dragster. I <a href="https://jakecharman.co.uk/projects/topfuel_dragster">wrote about this on the Nitro Junkie website, and later extended that post here.</a></p>
<div class="gallery"> <div class="gallery">
<iframe class="yt" src="https://www.youtube.com/embed/Wa-V8mQTXFk?si=W-LzfR3Qj_jgAdkY&amp;clip=UgkxV9lBj4pP1cvnR5seb82RQpWeE7RdnOXB&amp;clipt=EOrWtgsYyqu6Cw&amp;autoplay=1&amp;mute=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> <iframe class="yt" src="https://www.youtube.com/embed/Wa-V8mQTXFk?si=W-LzfR3Qj_jgAdkY&amp;clip=UgkxV9lBj4pP1cvnR5seb82RQpWeE7RdnOXB&amp;clipt=EOrWtgsYyqu6Cw&amp;autoplay=1&amp;mute=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<img alt="Jake working on a Top Fuel Dragster in the pits at Santa Pod Raceway" src="/static/images/jake_tf_1.jpg"> <img src="/static/images/jake_tf_1.jpg">
</div> </div>
<div class="social"> <div class="social">
<a class="button" href="https://nitrojunkie.uk"><i class="fa-solid fa-globe"></i></a> <a class="button" href="https://nitrojunkie.uk"><i class="fa-solid fa-globe"></i></a>
-1
View File
@@ -12,7 +12,6 @@
{% if link.get('img') is not none %} {% if link.get('img') is not none %}
<a href="{{link.src}}" target="_blank"></a> <a href="{{link.src}}" target="_blank"></a>
<img class="link-thumb" <img class="link-thumb"
alt="{{ link.title }}"
srcset=" srcset="
{% for i in range(200, 5100, 100) %} {% for i in range(200, 5100, 100) %}
/image/{{ link.img }}?w={{i}} {{i}}w{{"," if not loop.last}} /image/{{ link.img }}?w={{i}} {{i}}w{{"," if not loop.last}}
+2 -23
View File
@@ -2,7 +2,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{% block description %}Personal website of Jake Charman. A technology professional based in the UK.{% endblock %}"> <meta name="description" content="Personal website of Jake Charman. A technology professional based in the UK.">
<title>{{ page_title }}{{branding}}</title> <title>{{ page_title }}{{branding}}</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Orbitron:[email protected]&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
@@ -31,27 +31,6 @@
gtag('config', 'G-6WMXXY0RL0'); gtag('config', 'G-6WMXXY0RL0');
</script> </script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Jake Charman",
"url": "https://jakecharman.co.uk/",
"jobTitle": "Technology Professional",
"sameAs": [
"https://www.linkedin.com/in/jakecharman/",
"https://github.com/jcharman/"
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "{{ branding }}",
"url": "https://{{ url }}/"
}
</script>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
@@ -64,7 +43,7 @@
</nav> </nav>
<div id="logo-container"> <div id="logo-container">
<div id="logo"> <div id="logo">
{% block logo %}<a href='/'>{{branding|upper}}</a>{% endblock %} <a href='/'><h1>{{branding|upper}}</h1></a>
</div> </div>
</div> </div>
</header> </header>
-1
View File
@@ -24,7 +24,6 @@
<a href="/projects/{{ row.id }}"> <a href="/projects/{{ row.id }}">
{% endif %} {% endif %}
<img class="project-thumb" <img class="project-thumb"
alt="{{ row.alt }}"
srcset=" srcset="
{% for i in range(200, 5100, 100) %} {% for i in range(200, 5100, 100) %}
/image/{{ row.image }}?w={{i}} {{i}}w{{"," if not loop.last}} /image/{{ row.image }}?w={{i}} {{i}}w{{"," if not loop.last}}
-10
View File
@@ -1,10 +0,0 @@
# Jake Charman
> Personal website of Jake Charman, a technology professional based in the UK,
> covering distributed systems work and motorsport (Nitro Junkie Top Fuel drag racing).
## Pages
- [About](https://jakecharman.co.uk/): Background, technology stack, and motorsport involvement.
- [Projects](https://jakecharman.co.uk/projects/): Write-ups of personal and professional projects.
- [Links](https://jakecharman.co.uk/links/): Curated external links.
- [Contact](https://jakecharman.co.uk/contact/): Contact form.
-3
View File
@@ -1,3 +0,0 @@
Contact: mailto:[email protected]
Expires: 2036-01-01T00:00:00.000Z
Preferred-Languages: en
-1
View File
@@ -99,7 +99,6 @@
.yt { .yt {
width: calc(66% - 40px); width: calc(66% - 40px);
min-height: 100%; min-height: 100%;
height: auto;
} }
.gallery { .gallery {
+11 -9
View File
@@ -45,16 +45,7 @@ footer{
#logo>a{ #logo>a{
text-decoration: none; text-decoration: none;
font-family: "Orbitron", sans-serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-size: 2em;
} }
#logo>a>h1{
font-size: 1em;
}
footer h2, section h2{ footer h2, section h2{
margin: 0; margin: 0;
@@ -337,4 +328,15 @@ pre{
padding: 5px; padding: 5px;
height: 31vw; height: 31vw;
object-fit: cover; object-fit: cover;
}
#comments {
border-top: 1px solid #e5e5e5;
}
.comment {
background-color: #4c4c4c;
margin: 10px;
padding: 5px;
border-radius: 10px;
} }