publish v1

This commit is contained in:
Mathias DUPEUX 2025-03-05 15:41:26 +01:00
commit 761e6d810a
21 changed files with 526 additions and 0 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
URL="http://127.0.0.1:5000" # URL for rss/atom feed

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.env
__pycache__
*.pyc
.venv

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# Blog
A simple blog

7
articles.json Normal file
View file

@ -0,0 +1,7 @@
[
{
"title": "Lorem Ipsum",
"template": "loremipsum",
"category": "Uncategorized"
}
]

8
classes.py Normal file
View file

@ -0,0 +1,8 @@
class Article:
def __init__(self, template, title, category="uncategorized"):
self.template = template
self.title = title
self.category = "uncategorized" if category == None else category
def __repr__(self):
return f'Article(title="{self.title}")'

82
main.py Normal file
View file

@ -0,0 +1,82 @@
from flask import Flask, render_template, request, send_file, make_response
from routes.editor import editor_routes
from feedgen.feed import FeedGenerator
from dotenv import load_dotenv
from classes import *
import json, re, os
load_dotenv()
app = Flask(__name__)
app.register_blueprint(editor_routes)
articles_data = json.load(open("articles.json", "r", encoding="utf-8"))
categories = {} # Category name:str -> [Article]
articles = {} # ID:str = Article
fg = FeedGenerator()
fg.title("Mathias")
fg.id("Mathias")
fg.author({'name': "Mathias DPX", "email": "mathias@dupeux.net"})
fg.language("en")
fg.link(href=os.getenv("URL"))
fg.description("Blog RSS feed")
for article_data in articles_data:
category_name = article_data.get("category", "Uncategorized")
category_list = categories.get(category_name, [])
article = Article(
template=article_data.get("template"),
title=article_data.get("title"),
category=category_name
)
if article.template in list(articles.keys()):
raise KeyError(f"Two articles linking to the same template ({article.template})")
articles[article.template] = article
category_list.append(article)
categories[category_name] = category_list
fe = fg.add_entry()
fe.id(article.template)
fe.link(href=os.getenv("URL")+"/p/"+article.template)
fe.title(article.title)
@app.route("/")
@app.route("/p")
def index():
return render_template("home.html", categories=categories.items())
@app.route("/rss")
def rss_feed():
pretty = request.args.get("pretty", False, type=bool)
response = make_response(fg.rss_str(pretty=pretty), 200)
response.mimetype = "application/xml"
return response
@app.route("/atom")
def atom_feed():
pretty = request.args.get("pretty", False, type=bool)
response = make_response(fg.atom_str(pretty=pretty), 200)
response.mimetype = "application/xml"
return response
@app.route("/favicon.ico")
def favicon():
return send_file("static/favicon.ico")
@app.route("/contact")
def contact():
return render_template("contact.html")
@app.route("/p/<article_id>/")
def article(article_id:str):
if not re.match(r'[a-zA-Z0-9-]+', article_id):
return ">:("
article = articles[article_id]
return render_template(f"articles/{article.template}.html")
if __name__ == "__main__":
app.run()

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
flask
feedgen
python-dotenv

18
routes/editor.py Normal file
View file

@ -0,0 +1,18 @@
from flask import Blueprint, request, render_template
import base64
editor_routes = Blueprint('simple_page', __name__, template_folder='templates')
@editor_routes.route("/editor")
def editor():
return render_template("editor/editor.html")
@editor_routes.route("/preview/<b64>")
@editor_routes.route("/preview/")
def preview(b64:str=""):
is_real_preview = request.args.get("website", False, type=bool)
content = base64.b64decode(b64).decode("utf-8")
if is_real_preview:
return render_template("editor/demo.html", written=content)
else:
return render_template("editor/preview.html", content=content)

71
static/base.css Normal file
View file

@ -0,0 +1,71 @@
@import url("typography.css");
/* Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Base Layout */
html, body {
overflow-x: hidden;
}
body {
background-image: url("https://jvns.ca/stylesheets/noise.png");
background-color: #234892;
display: flex;
flex-direction: column;
}
/* Main Container */
#wrap {
width: 70%;
max-width: 45em;
margin: 0 auto;
background-color: #fff;
flex: 1;
}
/* Header Styles */
header {
padding-left: 35px;
padding-right: 35px;
min-height: 130px;
position: relative;
}
header h1 {
font-size: 100px;
position: absolute;
top: 50%;
transform: translate(0, -50%);
}
/* Navigation Bar */
#navbar {
min-width: 100%;
background-color: #03396C;
padding: 5px 3em 5px 0;
text-align: right;
width: 70%;
max-width: 45em;
margin: 0 auto;
}
/* Main Content */
#main {
padding: 35px;
}
/* Footer */
#footer {
padding-bottom: 0.5em;
}
#footer-title {
min-width: 100%;
background-color: #03396C;
text-align: center;
}

41
static/editor.css Normal file
View file

@ -0,0 +1,41 @@
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#code {
width: 50%;
height: 100%;
margin: 0;
padding: 10px;
background-color: #1F1F1F;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
color: #CCCCCC;
position: absolute;
border: none;
resize: none;
outline: none;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
}
#preview {
width: 50%;
margin: 0;
padding: 0;
position: absolute;
left: 50%;
height: 100%;
background-color: white;
border: none;
}
.switch-container {
position: absolute;
bottom: 10px;
right: 10px;
background-color: #1F1F1F;
color: #CCCCCC;
padding: 5px 10px;
border-radius: 5px;
}

52
static/editor.js Normal file
View file

@ -0,0 +1,52 @@
const codeArea = document.getElementById('code')
const previewFrame = document.getElementById('preview')
const previewSwitch = document.getElementById('previewSwitch')
let lastCode = ''
let previewMode = false
function forceUpdatePreview() {
const currentCode = codeArea.value
const encodedCode = btoa(currentCode)
let url = `http://127.0.0.1:5000/preview/${encodedCode}`
if (previewMode) {
url += "?website=True"
}
previewFrame.src = url
}
function updatePreview() {
const currentCode = codeArea.value
if (currentCode !== lastCode) {
forceUpdatePreview();
lastCode = currentCode
}
}
function saveCode() {
const currentCode = codeArea.value
const blob = new Blob([currentCode], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'code.html'
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
setInterval(updatePreview, 1000)
updatePreview()
document.addEventListener('keydown', function(event) {
if (event.ctrlKey && event.key === 's') {
event.preventDefault()
saveCode()
}
})
previewSwitch.addEventListener('change', function(event) {
previewMode = event.target.checked
forceUpdatePreview()
})

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

89
static/typography.css Normal file
View file

@ -0,0 +1,89 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
/* Base Typography */
body {
font-family: "PT Serif", Georgia, Times, "Times New Roman", serif;
color: #000000;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0;
font-family: "Montserrat", sans-serif;
font-weight: bold;
}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
color: #000000;
position: relative;
}
/* Heading Hover Effects */
h2 a:hover::before,
h3 a:hover::before,
h4 a:hover::before,
h5 a:hover::before,
h6 a:hover::before {
content: "#";
position: absolute;
left: -0.75em;
top: 0;
color: #0073E6;
}
/* Text Elements */
#main p {
margin-bottom: 0.75em;
}
time {
font-family: "Montserrat", sans-serif;
font-weight: bold;
font-size: 75%;
}
hr {
margin-bottom: 1em;
}
/* Links */
a {
text-decoration: none;
color: #005B96;
}
/* Navigation */
#navbar {
min-width: 100%;
color: #ffffff;
text-transform: uppercase;
font-family: "Montserrat", sans-serif;
}
#navbar a {
color: inherit;
}
#navbar a + a::before {
content: "// ";
padding: 0 0.1em;
}
/* Footer */
#footer-title {
padding: 5px 0;
margin-bottom: 0.5em;
min-width: 100%;
color: #ffffff;
font-family: "Montserrat", sans-serif;
}
#footer {
text-align: center;
}
#footer p {
padding-top: 0.5em;
padding-bottom: 0.5em;
line-height: 1.25;
}

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block head %}
<title>Lorem Ipsum</title>
{% endblock %}
{% block content %}
<h1>Lorem Ipsum</h1>
<h4><time datetime="2025-01-23T18:41:03Z">January 23th 2025</time></h4>
<hr>
<p><a href="https://www.lipsum.com" target="_blank">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit. Nulla mattis, est et laoreet efficitur, nisi leo volutpat nibh, et rutrum erat urna vel nisl. Aenean sodales lorem quis dolor sodales, at convallis purus convallis. Nam interdum tincidunt nunc, sed molestie lectus. Integer a rhoncus enim, eu cursus lacus.</p>
<p>Donec dignissim consequat augue mattis ornare. Vivamus feugiat odio in sagittis consectetur. Proin nec suscipit dolor, ut consectetur nisi. Aliquam ut ex dapibus, volutpat justo sit amet, dignissim tellus. Curabitur placerat tempor neque congue sollicitudin. Praesent elementum in ligula id semper.</p>
{% endblock %}

38
templates/base.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" />
{% block head %}{% endblock %}
</head>
<body>
<div id="wrap">
<div id="head">
<header>
<h1>Mathias</h1>
</header>
<nav id="navbar">
<a href="/rss">rss</a>
<a href="{{ url_for('contact') }}">contact</a>
<a href="/">home</a>
</nav>
</div>
<main id="main">
<article>
{% block content %}{% endblock %}
</article>
</main>
<footer id="footer">
<div id="footer-title">0x03</div>
<p>
© Mathias DUPEUX<br>
<i><b>very</b></i> inspired by <a href="https://jvns.ca" target="_blank">jvns.ca</a><br>
Opensource at <a href="https://github.com/MathiasDPX/blog" target="_blank">MathiasDPX/blog</a>
</p>
</footer>
</div>
</body>
</html>

13
templates/contact.html Normal file
View file

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block head %}
<title>Contact</title>
{% endblock %}
{% block content %}
<h1>Contact</h1>
<hr>
Mail: <a href="mailto:mathias@dupeux.net">mathias@dupeux.net</a>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% block head %}
<title>Demo</title>
{% endblock %}
{% block content %}
{{ written }}
{% endblock %}

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Editor</title>
<link rel="stylesheet" href="{{ url_for('static', filename='editor.css') }}">
</head>
<body>
<textarea id="code" spellcheck="false" placeholder="<!DOCTYPE html>"></textarea>
<iframe id="preview" width="100%" height="100%"></iframe>
<div class="switch-container">
<label for="previewSwitch">Preview</label>
<input type="checkbox" id="previewSwitch">
</div>
<script src="{{ url_for('static', filename='editor.js') }}"></script>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='typography.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" />
<title>Preview</title>
</head>
<body>
<main id="main">
{{ content }}
</main>
</body>
</html>

37
templates/home.html Normal file
View file

@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% block head %}
<title>Home</title>
<style>
ul {
list-style-type: none;
}
li {
padding-left: 1.5em;
padding-bottom: 0.2em;
}
h3 {
padding-bottom: 0.1em;
}
a {
color: inherit;
}
</style>
{% endblock %}
{% block content %}
👋, Welcome here, you can see it's a mess
<br />
<br />
{% for category_name, articles in categories %}
<h3>{{ category_name }}</h3>
<ul>
{% for article in articles %}
<li><a href="{{ url_for('article', article_id=article.template) }}">{{article.title}}</a></li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}