mirror of
https://github.com/MathiasDPX/blog.git
synced 2025-05-09 15:13:09 +00:00
publish v1
This commit is contained in:
commit
761e6d810a
21 changed files with 526 additions and 0 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
URL="http://127.0.0.1:5000" # URL for rss/atom feed
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
.env
|
||||
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
.venv
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Blog
|
||||
A simple blog
|
7
articles.json
Normal file
7
articles.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"title": "Lorem Ipsum",
|
||||
"template": "loremipsum",
|
||||
"category": "Uncategorized"
|
||||
}
|
||||
]
|
8
classes.py
Normal file
8
classes.py
Normal 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
82
main.py
Normal 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
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
flask
|
||||
feedgen
|
||||
python-dotenv
|
18
routes/editor.py
Normal file
18
routes/editor.py
Normal 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
71
static/base.css
Normal 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
41
static/editor.css
Normal 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
52
static/editor.js
Normal 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
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
89
static/typography.css
Normal file
89
static/typography.css
Normal 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;
|
||||
}
|
15
templates/articles/loremipsum.html
Normal file
15
templates/articles/loremipsum.html
Normal 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
38
templates/base.html
Normal 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
13
templates/contact.html
Normal 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 %}
|
10
templates/editor/demo.html
Normal file
10
templates/editor/demo.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Demo</title>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{{ written }}
|
||||
{% endblock %}
|
18
templates/editor/editor.html
Normal file
18
templates/editor/editor.html
Normal 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>
|
15
templates/editor/preview.html
Normal file
15
templates/editor/preview.html
Normal 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
37
templates/home.html
Normal 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 %}
|
Loading…
Add table
Add a link
Reference in a new issue