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
20 changed files with 152 additions and 316 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
__pycache__ __pycache__
.vscode .vscode
.buildinfo.json .buildinfo.json
*.md
Vendored
-1
View File
@@ -148,7 +148,6 @@ pipeline {
stage('Clear cache') { stage('Clear cache') {
steps{ steps{
sh "/var/lib/jenkins/clearCFCache/clearCache.py a514fb61e1413b88aabbb19df16b8508" sh "/var/lib/jenkins/clearCFCache/clearCache.py a514fb61e1413b88aabbb19df16b8508"
sh "/var/lib/jenkins/clearCFCache/clearCache.py 2e68842d0d5f5512f319e16d73899b9d"
} }
} }
} }
-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
+6 -26
View File
@@ -16,35 +16,21 @@ 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__)
md_path = path.join(path.realpath(path.dirname(__file__)), path.normpath('../projects/')) md_path = path.join(path.realpath(path.dirname(__file__)), path.normpath('../projects/'))
projects = ContentArea( projects = ContentArea(
directory=LocalStorage(path.join(md_path, 'projects')), directory=LocalStorage(md_path),
root_templ='projects.html',
post_templ='article.html',
name='projects', name='projects',
title='Projects', import_name=__name__)
desc='A selection of projects I\'ve been involved in',
import_name=__name__
)
blog = ContentArea(
directory=LocalStorage(path.join(md_path, 'blog')),
root_templ='blog.html',
post_templ='post.html',
name='blog',
title='Blog',
desc='Less formal writing, usually something I\'ve been working on that isn\'t worthy of a project write up',
import_name=__name__
)
app.register_blueprint(projects, url_prefix='/projects') app.register_blueprint(projects, url_prefix='/projects')
app.register_blueprint(blog, url_prefix='/blog')
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 '''
@@ -88,13 +74,7 @@ def inject_branding() -> dict:
case _: case _:
brand = req_domain brand = req_domain
return {'branding': brand, 'url': req_domain} return {'branding': brand}
@app.context_processor
def inject_globals():
return {
'canonical_url': f"https://jakec.tech{request.path}"
}
@app.route('/') @app.route('/')
def index() -> str: def index() -> str:
@@ -162,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)
+27 -20
View File
@@ -5,17 +5,18 @@ import json
from datetime import datetime from datetime import datetime
import frontmatter import frontmatter
from markdown import markdown from markdown import markdown
import readtime
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, root_templ: str, post_templ: str, title: str, desc: str, *args, **kwargs): def __init__(self, directory: LocalStorage, *args, **kwargs):
self.md_directory = directory self.md_directory = directory
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.add_app_template_filter(self.category_title, 'category_title')
self.add_app_template_filter(self.human_date, 'human_date') self.add_app_template_filter(self.human_date, 'human_date')
self.add_app_template_filter(self.to_html, 'to_html') self.add_app_template_filter(self.to_html, 'to_html')
@@ -24,11 +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('/<article_id>/comment', view_func=self.comment, methods=['POST'])
self.root_templ = root_templ
self.post_templ = post_templ
self.title = title
self.desc = desc
def processor(self) -> dict: def processor(self) -> dict:
''' Jninja processors ''' ''' Jninja processors '''
@@ -37,14 +34,14 @@ class ContentArea(Blueprint):
post_soup = BeautifulSoup(html, 'html.parser') post_soup = BeautifulSoup(html, 'html.parser')
all_text = ' '.join([x.get_text() for x in post_soup.findAll('p')]) all_text = ' '.join([x.get_text() for x in post_soup.findAll('p')])
return ' '.join(all_text.split()[:200]) return ' '.join(all_text.split()[:200])
return dict(get_excerpt=get_excerpt)
def get_readtime(post: str) -> str: def category_title(self, category_id: str) -> str:
return readtime.of_html(post).text ''' Jninja filter to get a category title by its ID '''
with self.md_directory.open('categories.json') as categories_file:
def get_readtime_mins(post: str) -> int: categories = json.load(categories_file)
return readtime.of_html(post).minutes
return dict(get_excerpt=get_excerpt, get_readtime=get_readtime, get_readtime_mins=get_readtime_mins) return categories.get(category_id).get('title', '')
def human_date(self, iso_date: str) -> str: def human_date(self, iso_date: str) -> str:
''' Jninja filter to convert an ISO date to human readable. ''' ''' Jninja filter to convert an ISO date to human readable. '''
@@ -94,12 +91,12 @@ class ContentArea(Blueprint):
error='There\'s nothing here... yet.', error='There\'s nothing here... yet.',
description='I\'m still working on this page. Check back soon for some content.') description='I\'m still working on this page. Check back soon for some content.')
return render_template(self.root_templ, return render_template('projects.html',
articles=articles_to_return, articles=articles_to_return,
all_categories=categories, all_categories=categories,
title=self.title, title='Projects',
page_title='Projects - ', page_title='Projects - ',
description=self.desc) description='A selection of projects I\'ve been involved in')
def category(self, category_id: str) -> str: def category(self, category_id: str) -> str:
''' Load the page for a given category ''' ''' Load the page for a given category '''
@@ -127,7 +124,7 @@ class ContentArea(Blueprint):
error='There\'s nothing here... yet.', error='There\'s nothing here... yet.',
description='I\'m still working on this page. Check back soon for some content.') description='I\'m still working on this page. Check back soon for some content.')
return render_template(self.root_templ, articles=articles_to_return, return render_template('projects.html', articles=articles_to_return,
title=the_category['title'], title=the_category['title'],
description=the_category['long_description'], description=the_category['long_description'],
page_title=f'{the_category["title"]} - ', page_title=f'{the_category["title"]} - ',
@@ -144,6 +141,16 @@ class ContentArea(Blueprint):
return Response(status=500) return Response(status=500)
the_article = articles[0] the_article = articles[0]
return render_template(self.post_templ, post=markdown(the_article.content),
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),
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 -28
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,42 +11,34 @@
}); });
</script> </script>
{% endif %} {% endif %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ metadata.title | tojson }},
"datePublished": "{{ metadata.date }}",
"image": "https://jakec.tech/image/{{ metadata.image }}",
"timeRequired": "PT{{ get_readtime_mins(post) }}M",
"author": {
"@type": "Person",
"name": "Jake Charman",
"url": "https://jakec.tech/"
},
"mainEntityOfPage": "https://jakec.tech/projects/{{ metadata.id }}"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Projects", "item": "https://jakec.tech/projects/"},
{"@type": "ListItem", "position": 2, "name": {{ metadata.title | tojson }}, "item": "https://jakec.tech/projects/{{ metadata.id }}"}
]
}
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main> <main>
<section id="article"> <section id="article">
<h1>{{ metadata.title}} </h1> <h1>{{ metadata.title}} </h1>
<p>{{ get_readtime(post) }} read</p>
<p>{{ metadata.date | human_date }}</p> <p>{{ metadata.date | human_date }}</p>
<hr /> <hr />
{{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 %}
-58
View File
@@ -1,58 +0,0 @@
{% extends 'main.html' %}
{% block content %}
<main id="blog-main">
<nav id="filter">
<label for="filter_category">Filter:</label>
<select id="filter_category" name="filter_category" onchange="update_filter()">
<option value="all">All Categories</option>
{% for category in all_categories %}
<option {{ 'selected' if current_category==category }} value="{{ category }}">
{{all_categories.get(category).get('title')}}</option>
{% endfor %}
</select>
</nav>
<h2>{{ title }}</h2>
<p>{{ description }}</p>
{% for row in articles %}
<div class="blog-post">
{% if row.get('link') is not none %}
<a href="{{ row.link }}">
{% else %}
<a href="/blog/{{ row.id }}">
{% endif %}
<img class="blogpost-thumb"
alt="{{ row.alt }}"
srcset="
{% for i in range(200, 5100, 100) %}
/image/{{ row.image }}?w={{i}} {{i}}w{{"," if not loop.last}}
{% endfor %}
"
sizes="
(max-width: 999px) 80vw,
(min-width: 1000px) 20vw
"
src="/image/{{ row.image }}">
</a>
<div class="blogpost-text">
{% if row.get('link') is not none %}
<a href="{{ row.link }}">
{% else %}
<a href="/blog/{{ row.id }}">
{% endif %}
<h3>{{ row.title }}</h3>
</a>
<p class="blogpost-date">{{ row.date | human_date }}</p>
<p>{{ get_readtime(row.content) }} read</p>
<hr />
<p class="blogpost-description">{{ get_excerpt(row) }}... <a href="/blog/{{ row.id }}">Read more</a></p>
<hr />
{% for category in row.categories %}
<a class="blogpost-category" href="/blog/category/{{ category }}"> {{ all_categories.get(category, {}).get('title', '') }}{{ ", " if not loop.last }}</loop></a>
{% endfor %}
</div>
</div>
{% endfor %}
</section>
</main>
{% endblock %}
+25 -30
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,38 +18,35 @@
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>
<a class="button" href="https://github.com/jcharman/"><i class="fa-brands fa-github"></i></a> <a class="button" href="https://github.com/jcharman/"><i class="fa-brands fa-github"></i></a>
{% if url == 'jakec.tech' %}
<a class="button" href="https://instagr.am/jakec.tech"><i class="fa-brands fa-instagram"></i></a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -66,10 +61,10 @@
<hr /> <hr />
<p>When I'm not at work, I can often be found at <a href="https://santapod.com">Santa Pod Raceway</a> with <a href="https://nitrojunkie.uk">Nitro Junkie Racing</a></p> <p>When I'm not at work, I can often be found at <a href="https://santapod.com">Santa Pod Raceway</a> with <a href="https://nitrojunkie.uk">Nitro Junkie Racing</a></p>
<p>We run a Top Fuel motorcycle purpose built for drag racing. The engine is loosely based on a Kawasaki Z 1000, but supercharged, and nitromethane injected to produce around 1,000 horsepower.</p> <p>We run a Top Fuel motorcycle purpose built for drag racing. The engine is loosely based on a Kawasaki Z 1000, but supercharged, and nitromethane injected to produce around 1,000 horsepower.</p>
<p>I've also had the opportunity to work on a 10,000 horsepower Top Fuel Dragster. I <a href="/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 -30
View File
@@ -1,11 +1,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta property="og:url" content="{{ canonical_url }}">
<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 rel="canonical" href="{{ canonical_url }}">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&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:wght@400..900&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:wght@400..900&family=Tourney:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style/mobile.css" /> <link rel="stylesheet" href="/static/style/mobile.css" />
@@ -33,29 +31,6 @@
gtag('config', 'G-6WMXXY0RL0'); gtag('config', 'G-6WMXXY0RL0');
</script> </script>
{% if url == 'jakecharman.co.uk' %}
<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>
{% endif %}
<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>
@@ -63,15 +38,12 @@
<nav id="top-nav"> <nav id="top-nav">
<a href="/">About</a> <a href="/">About</a>
<a href="/projects/">Projects</a> <a href="/projects/">Projects</a>
{% if url != 'jakecharman.co.uk' %}
<a href="/blog">Blog</a>
{% endif %}
<a href="/contact/">Contact</a> <a href="/contact/">Contact</a>
<a href="/links/">Links</a> <a href="/links/">Links</a>
</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>
-69
View File
@@ -1,69 +0,0 @@
{% extends 'main.html' %}
{% block description %}{{ metadata.description }}{% endblock %}
{% block head %}
{% if metadata.gallery %}
<script>
document.addEventListener("DOMContentLoaded",function(){
post_gallery = new Viewer(document.getElementById('article'), {
'url': 'realsrc',
'title': false
});
});
</script>
{% endif %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ metadata.title | tojson }},
"description": {{ metadata.description | tojson }},
"timeRequired": "PT{{ get_readtime_mins(post) }}M",
"datePublished": "{{ metadata.date }}",
{% if metadata.updated %}
"dateModified": "{{ metadata.updated }}",
{% endif %}
"image": "https://jakec.tech/image/{{ metadata.image }}",
"author": {
"@type": "Person",
"name": "Jake Charman",
"url": "https://jakec.tech/"
},
"publisher": {
"@type": "Person",
"name": "Jake Charman",
"url": "https://jakec.tech/"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://jakec.tech/blog/{{ metadata.id }}"
},
"url": "https://jakec.tech/blog/{{ metadata.id }}",
"inLanguage": "en-GB"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Projects", "item": "https://jakec.tech/projects/"},
{"@type": "ListItem", "position": 2, "name": {{ metadata.title | tojson }}, "item": "https://jakec.tech/projects/{{ metadata.id }}"}
]
}
</script>
{% endblock %}
{% block content %}
<main>
<section id="article">
<h1>{{ metadata.title}} </h1>
<p>{{ get_readtime(post) }} read</p>
<p>{{ metadata.date | human_date }}</p>
{% if metadata.updated %}
<p>Updated: {{ metadata.updated | human_date }}</p>
{% endif %}
<hr />
{{post|safe}}
</section>
</main>
{% endblock %}
+1 -2
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}}
@@ -47,7 +46,7 @@
<p class="article-description">{{ row.description }}</p> <p class="article-description">{{ row.description }}</p>
<p class="article-date">{{ row.date | human_date }}</p> <p class="article-date">{{ row.date | human_date }}</p>
{% for category in row.categories %} {% for category in row.categories %}
<a class="article-category" href="/projects/category/{{ category }}"> {{ all_categories.get(category, {}).get('title', '') }}{{ ", " if not loop.last }}</a> <a class="article-category" href="/projects/category/{{ category }}">{{ category | category_title }}{{ ", " if not loop.last }}</loop></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
-1
View File
@@ -7,4 +7,3 @@ python-frontmatter>=1.1.0
requests>=2.32.3 requests>=2.32.3
pillow>=11.0.0 pillow>=11.0.0
mod_wsgi>=5.0.2 mod_wsgi>=5.0.2
readtime>=3.0.0
+2 -3
View File
@@ -1,11 +1,10 @@
function update_filter() { function update_filter() {
var section = window.location.pathname.split('/')[1]
var project_filter = document.getElementById("filter_category"); var project_filter = document.getElementById("filter_category");
console.log(project_filter.value) console.log(project_filter.value)
if (project_filter.value == 'all') { if (project_filter.value == 'all') {
window.location.href = '/'+section; window.location.href = '/projects';
} }
else { else {
window.location.href = '/'+section+'/category/' + project_filter.value; window.location.href = '/projects/category/' + project_filter.value;
} }
} }
-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:security@jakecharman.co.uk
Expires: 2036-01-01T00:00:00.000Z
Preferred-Languages: en
-5
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 {
@@ -129,8 +128,4 @@
padding: 10px; padding: 10px;
height: 14vw; height: 14vw;
} }
.blogpost-thumb {
width: 12.5vw;
}
} }
+6 -22
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;
@@ -339,20 +330,13 @@ pre{
object-fit: cover; object-fit: cover;
} }
#blog-main { #comments {
padding: 0 10px 0 10px; border-top: 1px solid #e5e5e5;
} }
.blog-post { .comment {
border: 5px solid #e5e5e5; background-color: #4c4c4c;
padding: 10px;
margin: 10px; margin: 10px;
border-radius: 15px; padding: 5px;
overflow: hidden; border-radius: 10px;
}
.blogpost-thumb {
float: left;
padding-right: 20px;
width: 25vw;
} }