Compare commits

..

37 Commits

Author SHA1 Message Date
m
7fda9dde36 fix 2026-03-26 16:13:57 +01:00
m
194b1e04cc fix 2026-03-26 16:11:51 +01:00
m
f3ff920e38 render 2026-03-26 16:09:38 +01:00
m
a47ad949bd preview 2026-03-26 16:07:05 +01:00
m
2df2f5f3a8 import 2026-03-25 13:02:41 +01:00
m
62445a1812 check comp 2026-03-25 12:56:06 +01:00
m
6f046b3743 fix typo 2026-03-25 12:45:43 +01:00
m
1c23c05cc7 render fn 2026-03-25 12:42:26 +01:00
m
0272ec3d07 fix 2026-03-25 08:14:00 +01:00
m
81c093aa99 fix 2026-03-25 08:12:34 +01:00
m
2118d9fbc0 code template 2026-03-25 08:08:07 +01:00
m
20fce270a5 inject css 2026-03-25 05:03:29 +01:00
m
9a83309d36 preview 2026-03-25 01:51:21 +01:00
m
a90f06d913 combination 2026-03-25 01:32:03 +01:00
m
67ec5753bf testline 2026-03-24 21:54:07 +01:00
m
a38aad6576 add test 2026-03-24 21:47:48 +01:00
m
11aac567ec syntax fix 2026-03-24 21:44:12 +01:00
m
6c5649d721 image html 2026-03-24 21:41:05 +01:00
m
86588c0080 image templates 2026-03-24 21:33:10 +01:00
m
664900cbd1 image template 2026-03-24 21:27:59 +01:00
m
80c66c1802 blog title fix 2026-03-22 20:59:58 +01:00
m
8ad581658b title fix 2026-03-22 20:56:09 +01:00
m
8193bdd7f2 add favicon 2026-03-22 20:20:30 +01:00
m
acc9710991 logic path 2026-03-22 09:21:50 +01:00
m
034d9bc884 Merge branch 'main' of https://git.etwasse.de/Mira/flask_blog 2026-03-22 08:06:50 +01:00
m
fae653a2b9 about template 2026-03-22 08:01:38 +01:00
396ad2de38 Update readme.md 2026-03-17 10:03:24 +00:00
m
5ef0dca53b md 2026-03-17 09:20:55 +01:00
m
cd0c11dec3 fix.. 2026-03-17 09:16:51 +01:00
m
f7feb18de3 logic move out 2026-03-17 09:14:50 +01:00
m
43bec07510 ignore 2026-03-17 05:40:36 +01:00
m
4b06f03753 ignore 2026-03-17 05:39:27 +01:00
m
247d35f46a use_uid 2026-03-16 04:30:50 +01:00
m
ef3ecd3f8f content up 2026-03-15 20:10:41 +01:00
m
ad0840d4ed Merge branch 'main' of gitea:Mira/flask_blog 2026-03-15 20:05:56 +01:00
m
3551286aca content up 2026-03-15 20:05:05 +01:00
mira
85e387f99a gitignore 2026-03-15 16:02:41 +01:00
26 changed files with 590 additions and 269 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
*.pyc
docker-compose.yml
Dockerfile
Caddyfile
deploy.sh
.vscode/
.ruff_cache/

View File

@@ -1,3 +0,0 @@
http://100.64.0.17 {
reverse_proxy flask_blog:5000
}

View File

@@ -1,20 +0,0 @@
FROM python:3.11-slim
# Create a non-root user for security
RUN groupadd -g 1000 flaskuser && useradd -u 1000 -g flaskuser flaskuser
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn
# Copy the rest of the code
COPY . .
# Change ownership to our non-root user
RUN chown -R flaskuser:flaskuser /app
USER flaskuser
# Run with Gunicorn (replaces uwsgi)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--workers", "4"]

140
app.py
View File

@@ -1,7 +1,7 @@
from flask import Flask, render_template, request, redirect, url_for
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
from content.posts import BLOG_POSTS
from content.logic import get_enriched_post, get_comments_for_post, save_comment
from flask_logic.logic import get_enriched_post, get_comments_for_post, save_comment
from flask_logic.renderer import process_post_content, collect_css, generate_preview
# 🌟 IMPORT THE content from separate files.
from content.posts import BLOG_POSTS
from content.about_text import ABOUT, TITLE
@@ -16,15 +16,6 @@ COMMENT_FILE = 'content/comments.csv'
MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5 Megabytes
POSTS_PER_PAGE = 5 # posts per page limit here
"""
import os
from flask import send_from_directory
@app.route('/content/images/<path:filename>')
def custom_static(filename):
# This serves files from your private submodule folder
return send_from_directory('content/images', filename)
"""
@app.route('/post/<int:post_id>/comment', methods=['POST'])
def post_comment(post_id):
@@ -43,35 +34,79 @@ def post_comment(post_id):
# --- Routes ---
def calculate_pagination(posts, posts_per_page, page):
"""
Calculate pagination parameters for blog posts.
Args:
posts (list): Full list of blog posts
posts_per_page (int): Number of posts per page
page (int): Current page number (1-indexed)
Returns:
dict: Pagination data including:
- posts_to_show: Posts for current page
- prev_url: Previous page URL or None
- next_url: Next page URL or None
- current_page: Current page number
- total_pages: Total number of pages
- total_posts: Total number of posts
"""
# Sort posts by ID newest first
sorted_posts = sorted(posts, key=lambda x: x['id'], reverse=True)
total_posts = len(sorted_posts)
total_pages = math.ceil(total_posts / posts_per_page)
# Clamp page to valid range
page = max(1, min(page, total_pages))
start = (page - 1) * posts_per_page
end = start + posts_per_page
posts_to_show = sorted_posts[start:end]
prev_url = url_for('home', page=page - 1) if page > 1 else None
next_url = url_for('home', page=page + 1) if end < total_posts else None
return {
'posts_to_show': posts_to_show,
'prev_url': prev_url,
'next_url': next_url,
'current_page': page,
'total_pages': total_pages,
'total_posts': total_posts
}
@app.route('/')
def home():
posts_with_preview = []
for post in BLOG_POSTS:
preview = generate_preview(post.get("content", ""))
post_copy = dict(post)
post_copy["preview"] = preview
posts_with_preview.append(post_copy)
"""Home page with paginated blog posts."""
page = request.args.get('page', 1, type=int)
blog_title = TITLE
# 2. Sort posts by ID newest first
all_posts = sorted(BLOG_POSTS, key=lambda x: x['id'], reverse=True)
# Calculate totals
total_posts = len(all_posts)
total_pages = math.ceil(total_posts / POSTS_PER_PAGE)
# 3. Calculate start and end indices
start = (page - 1) * POSTS_PER_PAGE
end = start + POSTS_PER_PAGE
# 4. Slice the list for the current page
posts_to_show = all_posts[start:end]
# 5. Determine if there is a next or previous page
prev_url = url_for('home', page=page - 1) if page > 1 else None
next_url = url_for('home', page=page + 1) if end < len(all_posts) else None
pagination = calculate_pagination(posts_with_preview, POSTS_PER_PAGE, page)
return render_template('index.html',
posts=posts_to_show,
prev_url=prev_url,
next_url=next_url,
current_page=page,
blog_title=blog_title,
total_pages=total_pages)
posts=pagination['posts_to_show'],
prev_url=pagination['prev_url'],
next_url=pagination['next_url'],
current_page=pagination['current_page'],
total_pages=pagination['total_pages'],
blog_title=blog_title,
total_posts=pagination['total_posts'])
@@ -85,18 +120,43 @@ def about():
@app.route('/post/<int:post_id>')
def post_detail(post_id):
# One call to get the data + the extra logic (templates/timelines)
context = {"used_components": set()}
post = get_enriched_post(post_id, BLOG_POSTS)
if not post:
return "Post not found", 404
comments = get_comments_for_post(post_id)
return render_template('post_detail.html', post=post, comments=comments, blog_title = TITLE)
comments = get_comments_for_post(post_id)
processed_content = process_post_content(
post.get('content', ''), context=context
)
css_files = collect_css(context)
return render_template(
"post_detail.html",
post=post,
content=processed_content,
comments=comments,
blog_title=TITLE,
component_css=css_files
)
@app.route('/content/image/<path:filename>')
def content_image_files(filename):
directory = 'content/image'
full_path = os.path.join(os.getcwd(), directory, filename) # Your actual file location
print(f"Requested: {filename}")
print(f"Directory: {os.getcwd()}/{directory}")
print(f"Full path: {full_path}")
print(f"Exists: {os.path.exists(full_path)}")
if not os.path.exists(full_path):
return f"File not found: {full_path}", 404
return send_from_directory(directory, filename)
if __name__ == '__main__':
app.run(debug=True)

Submodule content updated: 182725bce2...cb53a98cca

View File

@@ -1,35 +0,0 @@
services:
flask_app:
build: .
container_name: flask_blog
restart: always
volumes:
- .:/app
- ./data:/app/data # Persist your CSV files
networks:
- web_net
caddy:
image: caddy:latest
container_name: caddy
restart: always
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web_net
depends_on:
- flask_app
networks:
web_net:
driver: bridge
volumes:
caddy_data:
caddy_config:

0
flask_logic/__init__.py Normal file
View File

60
flask_logic/components.py Normal file
View File

@@ -0,0 +1,60 @@
from flask_logic.registry import register_component
@register_component("image", css="css/components/image.css")
def render_image_component(src, caption=None, css_class=None, context=None):
if context:
context["used_components"].add("image")
base_class = "image-container"
full_class = f"{base_class} {css_class}" if css_class else base_class
caption_html = f'<div class="caption">{caption}</div>' if caption else ""
return f"""
<div class="{full_class}">
<img src="/content/image/{src}" alt="{caption or ''}">
{caption_html}
</div>
"""
@register_component("code", css="css/components/code.css")
def render_code_block(value=None, code=None, context=None):
if context:
context["used_components"].add("code")
code = code or value or ""
return f"""
<pre class="code-block"><code>{code}</code></pre>
"""
@register_component("tree", css="css/components/tree.css")
def render_tree_component(context=None):
if context:
context["used_components"].add("tree")
return f"""
<div class="background-svg">
<img src="/content/image/animation.svg" alt="" />
<br />
<p>tree source: codepen @uchardon</p>
</div>"""
@register_component("timeline", css="css/components/timeline.css")
def render_timeline_component(timeline=None, value=None, context=None):
if context:
context["used_components"].add("timeline")
timeline = timeline or value or ""
return f"""
<div class="tw-w-full tw-mx-auto tw-px-0 tw-py-12">
<ol
class="tw-relative tw-border-l-2 tw-border-gray-300 tw-list-none tw-p-0 tw-m-0 tw-ml-4"
>
{timeline}
</ol>
</div>
<li class="tw-mb-12 tw-ml-6 tw-list-none tw-relative"></li>
"""

99
flask_logic/logic.py Normal file
View File

@@ -0,0 +1,99 @@
import os
import csv
COMMENT_FILE = os.path.join(os.path.dirname(__file__), '../content/comments.csv')
MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5MB example
import datetime
def save_comment(post_id, author, content):
"""Handles the validation and saving of comments to the private CSV."""
# 1. Validation logic moved here
if not content or len(content) > 1000:
return False
author = (author or "Anonymous").strip()[:50]
content = content.strip()
# 2. Path & Size Checks
file_exists = os.path.isfile(COMMENT_FILE)
if file_exists and os.path.getsize(COMMENT_FILE) > MAX_FILE_SIZE_BYTES:
return False # Or raise a specific error
# 3. Write to CSV
with open(COMMENT_FILE, 'a', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['post_id', 'author', 'content', 'date'])
if not file_exists:
writer.writeheader()
writer.writerow({
'post_id': post_id,
'author': author,
'content': content,
'date': datetime.datetime.now().strftime("%B %d, %Y at %I:%M %p")
})
return True
def load_snippet(filename):
"""Helper to read HTML snippets from the data folder."""
base_path = os.path.join(os.path.dirname(__file__), 'data')
file_path = os.path.join(base_path, filename)
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
return ""
def get_enriched_post(post_id, BLOG_POSTS):
post = next((p for p in BLOG_POSTS if p.get('id') == post_id), None)
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / "content" / "data"
file_path_10 = DATA_DIR / "post_10_timeline.html"
# Mapping config: keeps logic.py tiny
configs = {
10: {
"template": "components/timeline.html",
"timeline_file": file_path_10
},
8: {
"template": "components/christmas_post.html",
"timeline": None
},
}
if post_id in configs:
conf = configs[post_id]
post_template = conf.get("template")
if post_template:
post["template"] = post_template
# Only load the file if a filename is provided
t_file = conf.get("timeline_file")
if t_file:
post["timeline"] = load_snippet(t_file)
return post
def get_comments_for_post(post_id):
comments = []
if not os.path.exists(COMMENT_FILE):
return comments
if os.path.getsize(COMMENT_FILE) > MAX_FILE_SIZE_BYTES:
return ["Comment section full."] # Handle error in app.py
with open(COMMENT_FILE, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
if row['post_id'] == str(post_id):
comments.append(row)
return comments

12
flask_logic/registry.py Normal file
View File

@@ -0,0 +1,12 @@
# component_registry.py
COMPONENTS = {}
def register_component(name, css=None):
def decorator(func):
COMPONENTS[name] = {
"render": func,
"css": css
}
return func
return decorator

128
flask_logic/renderer.py Normal file
View File

@@ -0,0 +1,128 @@
import re
# FORCE component registration
import flask_logic.components
from flask_logic.registry import COMPONENTS
# ------------------------
# Content renderer (blocks)
# ------------------------
def render_blocks(blocks, context=None):
html_output = []
print(COMPONENTS)
for block in blocks:
block_type = block.get("type")
if block_type == "text":
html_output.append(block.get("value", ""))
elif block_type in COMPONENTS:
render_func = COMPONENTS[block_type]["render"]
# remove "type" before passing kwargs
kwargs = {k: v for k, v in block.items() if k != "type"}
html_output.append(
render_func(**kwargs, context=context)
)
return "\n".join(html_output)
# ------------------------
# Old string renderer (tokens)
# ------------------------
def parse_options(option_string):
options = {}
if not option_string:
return options
parts = option_string.split("|")
for part in parts:
if "=" in part:
key, value = part.split("=", 1)
options[key.strip()] = value.strip()
return options
def render_content(content, context=None):
if not content:
return ""
def replace_image(match):
src = match.group("src").strip()
options = parse_options(match.group("options"))
render_func = COMPONENTS["image"]["render"]
return render_func(
src=src,
caption=options.get("caption"),
css_class=options.get("class"),
context=context
)
pattern = r"\[image:(?P<src>[^\|\]]+)(?:\|(?P<options>[^\]]+))?\]"
return re.sub(pattern, replace_image, content)
# ------------------------
# Unified entry
# ------------------------
def process_post_content(content, context=None):
if isinstance(content, str):
return render_content(content, context=context)
elif isinstance(content, list):
return render_blocks(content, context=context)
return ""
# ------------------------
# CSS collector
# ------------------------
def collect_css(context):
css_files = []
for comp in context["used_components"]:
css = COMPONENTS.get(comp, {}).get("css")
if css:
css_files.append(css)
return css_files
def generate_preview(content, max_length=200):
if isinstance(content, str):
# old system
text = re.sub(r"<[^>]+>", "", content) # strip HTML
return text[:max_length]
elif isinstance(content, list):
# new system
for block in content:
if block.get("type") == "text":
text = re.sub(r"<[^>]+>", "", block.get("value", ""))
return text[:max_length]
return ""
# optinal, for images
def generate_preview_html(content, context=None):
if isinstance(content, list):
for block in content:
if block.get("type") == "text":
return f"<div class='blog-preview'>{block.get('value')}</div>"
elif block.get("type") in COMPONENTS:
# optional: allow image preview
render_func = COMPONENTS[block["type"]]["render"]
kwargs = {k: v for k, v in block.items() if k != "type"}
return render_func(**kwargs, context=context)
elif isinstance(content, str):
return content[:200]
return ""

89
readme.md Normal file
View File

@@ -0,0 +1,89 @@
# Flask Blog Templates
A minimal **Flask template set** for my blog.
This project adapts and the original [simple-blog-template](https://github.com/earlbread/simple-blog-template) by [Seunghun Lee](https://github.com/earlbread).
Content is **static**; selected templates (with **fixed** names/paths) are loaded dynamically.
Initially deployed using uWSGI and Nginx; later migrated to Gunicorn, Caddy, and Docker. Most versions should work.
## Git Tag
Current version:
```bash
git tag -a v1.0 -m "Initial release of Flask Blog Templates"
(it worked on my machine)
```
---
## Overview
It provides a growing collection of styling templates and is designed to run with two additional files (see below).
In the current setup, blog content is defined using Python dictionaries (example below), while comments are stored in a CSV file at `content/comments.csv`.
A basic blog structure is provided by the existing templates. Additional per-article styling can be applied either by embedding raw HTML directly in the post content or by using template expansion via Jinja:
```jinja2
{% if post.template %}
{% include post.template %}
{% endif %}
```
For example, `templates/components/timeline.html` demonstrates a timeline component.
An example data structure is provided below.
---
## Required Files
This two files should be included in program folder to run the app.
### `content/about.py`
provides text for about page and blog title
```python
ABOUT = {
"content": """
<p>Hello here is the about page</p>
"""
}
TITLE = {
"title": "Hello World"
}
```
### `content/posts.py`
provides articles in blog:
```python
BLOG_POSTS = [
{
'id': 7,
'title': "title",
'subtitle': "subtitle",
'date': "December 10, 2024",
'content': """can use raw html string for style, will be rendered as html
""",
'displayall': False
},
]
```
## Template Evolution
- Originally based on the Simple Blog Template project.
- Adapted for Jinja2 rendering (Flask compatibility).
- CSS rewritten for flexibility and modular styling.
- Template design will continue accumulating for themes and post customization.
## License
- This project is distributed under the [MIT License](https://github.com/earlbread/simple-blog-template/blob/master/LICENSE).

View File

@@ -1,124 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 131 140">
<style>
svg {
max-height: 60vh;
overflow: visible;
}
path {
fill: #379157;
stroke: #379157;
stroke-width: 0.2;
transform: scale(0);
transform-origin: 50% 50%;
animation: star 8s ease-in-out infinite;
animation-delay: calc(var(--no) * 0.025s);
transform-box: fill-box;
}
@keyframes star {
0% {
transform: scale(0);
animation-timing-function: cubic-bezier(0.74, 1.72, 0.57, 1.01);
}
10% {
transform: scale(1);
}
65% {
transform: translateY(0px) scale(1);
}
75% {
transform: translateY(50px) scale(0);
}
100% {
transform: translateY(0px) scale(0);
}
}
</style>
<path d="m49.35 6.37-1.88-.93-1.85 1.01.32-2.07-1.54-1.43 2.08-.34.9-1.9.96 1.86 2.1.25-1.48 1.5z" style="--no:100;fill:#d8e540;stroke:#d8e540;"/>
<path d="m129.54 125.15-.82-.26-.68.53-.01-.86-.71-.48.81-.28.23-.83.52.7.86-.04-.5.7z" style="--no: 1; transform: scale(222);"/>
<path d="m126.73 127.78-1.39-.2-.95 1.03-.24-1.38-1.27-.59 1.24-.65.16-1.39 1 .98 1.38-.28-.62 1.26z" style="--no:2"/>
<path d="m120.86 129.62-1.36-.62-1.28.78.16-1.49-1.14-.97 1.46-.31.58-1.38.75 1.3 1.49.12-1 1.1z" style="--no:3"/>
<path d="m115.25 131.06-2.48-1.02-2.25 1.47.2-2.68-2.1-1.68 2.62-.64.95-2.5 1.41 2.28 2.68.12-1.73 2.05z" style="--no:4"/>
<path d="m106.57 132.05-1.32-.75-1.37.67.3-1.49-1.05-1.09 1.5-.17.71-1.34.64 1.37 1.49.27-1.12 1.02z" style="--no:5"/>
<path d="m107.36 126.39-1.26-.48-1.11.77.06-1.35-1.07-.82 1.3-.35.44-1.28.74 1.13 1.36.03-.85 1.05z" style="--no:6"/>
<path d="m101.6 129.66-3.35-1.58-3.2 1.87.47-3.68-2.77-2.46 3.64-.7 1.5-3.39 1.78 3.25 3.69.37-2.54 2.7z" style="--no:7"/>
<path d="m91.68 125.45-2.42-1.71-2.78 1.03.87-2.83-1.83-2.33 2.96-.05 1.64-2.46.96 2.8 2.85.8-2.37 1.78z" style="--no:8"/>
<path d="m88.75 115.32-1.92.87-.37 2.08-1.42-1.55-2.1.29 1.05-1.84-.93-1.9 2.07.43 1.52-1.47.24 2.1z" style="--no:9"/>
<path d="m80.86 119.34-1.99-.75-1.74 1.22.1-2.12-1.7-1.28 2.05-.57.7-2 1.17 1.77 2.12.04-1.33 1.66z" style="--no:10"/>
<path d="m80.76 112.4-1.91-.46-1.46 1.32-.16-1.96-1.7-.98 1.8-.76.4-1.92 1.3 1.49 1.95-.21-1.02 1.68z" style="--no:11"/>
<path d="m73.42 115.87-2.31-1.18-2.28 1.23.4-2.55-1.87-1.79 2.56-.4 1.11-2.34 1.18 2.3 2.57.35-1.83 1.83z" style="--no:12"/>
<path d="m73.07 106.79-.93.2-.36.87-.48-.82-.95-.07.63-.7-.22-.93.87.38.8-.5-.09.95z" style="--no:13"/>
<path d="m66.62 105.3-.92-.58-.99.42.27-1.05-.7-.81 1.07-.07.55-.92.4 1 1.05.24-.83.7z" style="--no:14"/>
<path d="m64.58 114.97-3.79-2.46-4.18 1.72 1.17-4.36-2.93-3.45 4.52-.23 2.37-3.85 1.61 4.22 4.4 1.06-3.52 2.85z" style="--no:15"/>
<path d="m57.23 104.7-1.17-.97-1.46.43.56-1.42-.86-1.26 1.52.1.94-1.2.38 1.47 1.43.51-1.28.82z" style="--no:16"/>
<path d="m51.83 107.18-2.77-.7-2.14 1.9-.2-2.85-2.46-1.45 2.65-1.07.62-2.78 1.84 2.18 2.83-.27-1.5 2.42z" style="--no:17"/>
<path d="m54.36 113.53-2.02.23-.96 1.8-.84-1.86-2-.36 1.5-1.37-.28-2.01 1.77 1 1.83-.88-.41 1.99z" style="--no:18"/>
<path d="m44.68 113.98-1.72-1.4-2.12.63.8-2.06-1.25-1.83 2.2.12 1.35-1.75.57 2.14 2.09.74-1.86 1.2z" style="--no:19"/>
<path d="m44.48 117.46-.94-.5-.94.5.18-1.05-.76-.74 1.05-.15.46-.95.48.95 1.05.15-.76.74z" style="--no:20"/>
<path d="m38.72 116.76-2.49-.72-2 1.64-.08-2.59-2.18-1.4 2.43-.87.66-2.51 1.59 2.04 2.59-.15-1.46 2.15z" style="--no:21"/>
<path d="m42.9 104.65-1.42-.85-1.5.68.37-1.6-1.1-1.21 1.63-.15.81-1.43.65 1.52 1.61.33-1.24 1.08z" style="--no:22"/>
<path d="m31.82 119.39-1.35-.43-1.12.87-.01-1.43-1.17-.8 1.34-.44.4-1.37.85 1.15 1.42-.04-.83 1.15z" style="--no:23"/>
<path d="M30.14 114.38h-1l-.54.83-.32-.94-.95-.27.8-.59-.04-.99.8.58.93-.34-.3.94z" style="--no:24"/>
<path d="m25.22 120.18-2.06-.89-1.9 1.19.21-2.24-1.72-1.44 2.2-.49.84-2.08 1.14 1.93 2.24.16-1.49 1.69z" style="--no:25"/>
<path d="m18.67 122.17-1.53-.5-1.27.99v-1.61l-1.33-.91 1.53-.5.45-1.54.95 1.3 1.6-.05-.94 1.3z" style="--no:26"/>
<path d="m12.72 123.8-1.1-.26-.84.74-.07-1.12-.97-.57 1.04-.42.24-1.1.73.86 1.11-.1-.6.95z" style="--no:27"/>
<path d="m8.25 126.29-1.18-.52-1.1.67.14-1.28-.98-.84 1.26-.26.49-1.2.65 1.12 1.28.1-.86.96z" style="--no:28"/>
<path d="m3.43 128.67-1.11-.26-.85.78-.1-1.14-1-.57 1.06-.45.22-1.12.76.86 1.14-.13-.6.99z" style="--no:29"/>
<path d="m36.74 105.6-3.33-1.68-3.27 1.79.58-3.69L28 99.46l3.68-.6 1.6-3.36 1.7 3.32 3.7.48-2.64 2.64z" style="--no:30"/>
<path d="m28.14 106.59-1.84-.42-1.39 1.28-.17-1.87-1.65-.93 1.74-.75.37-1.85 1.24 1.42 1.88-.22-.97 1.63z" style="--no:31"/>
<path d="m23.8 102.17-.71-.49-.8.31.24-.82-.54-.66.85-.03.47-.71.29.8.82.22-.68.52z" style="--no:32"/>
<path d="m20.1 108.3-1.52-1.19-1.85.58.66-1.82-1.12-1.58 1.94.07 1.16-1.55.53 1.86 1.84.62-1.61 1.08z" style="--no:33"/>
<path d="m14.73 109.75-1.37-.47-1.16.87.02-1.45-1.18-.84 1.38-.43.43-1.38.84 1.18 1.45-.02-.87 1.17z" style="--no:34"/>
<path d="m8.87 112.07-.96-.54-1 .5.22-1.1-.77-.78 1.1-.14.5-.98.47 1 1.1.19-.82.75z" style="--no:35"/>
<path d="m37.72 97.29-.82-.4-.8.45.13-.9-.67-.62.9-.16.38-.83.43.8.9.1-.63.67z" style="--no:36"/>
<path d="m43.69 97.64-1.87-1.15-2.01.88.52-2.14-1.47-1.64 2.2-.16 1.1-1.9.84 2.04 2.15.46-1.68 1.42z" style="--no:37"/>
<path d="m51.23 94.34-1.54.2-.72 1.37-.65-1.4-1.54-.27 1.14-1.05-.23-1.54 1.36.75 1.39-.7-.3 1.53z" style="--no:38"/>
<path d="m50.46 90.7-1.14-.82-1.29.58.42-1.35-.94-1.04 1.4-.02.7-1.22.45 1.33 1.38.3-1.13.84z" style="--no:39"/>
<path d="m60.25 91.43-3.43-1.99-3.46 1.93.83-3.88-2.9-2.7 3.94-.4 1.67-3.6 1.6 3.63 3.94.47-2.95 2.65z" style="--no:40"/>
<path d="m64.71 84.62-.84-.82-1.13.3.52-1.04-.64-.99 1.16.17.74-.91.2 1.15 1.09.42-1.04.55z" style="--no:41"/>
<path d="m71.62 83.43-.96-.53-.95.57.2-1.09-.82-.72 1.09-.15.43-1.01.48 1 1.1.1-.8.75z" style="--no:42"/>
<path d="m69.06 91.57-2.25-1.58-2.52 1.12.81-2.63-1.85-2.04 2.76-.05L67.38 84l.9 2.6 2.69.57-2.2 1.66z" style="--no:43"/>
<path d="m76.64 88.66-1.87-.98-1.8 1.11.35-2.08-1.61-1.36 2.09-.32.8-1.95.94 1.89 2.1.15-1.5 1.49z" style="--no:44"/>
<path d="m83.66 86.9-2.15-1.06-2 1.3.34-2.36L78 83.27l2.36-.4.86-2.24 1.11 2.12 2.4.13-1.68 1.71z" style="--no:45"/>
<path d="m89.28 83.7-1.25-.96-1.47.57.53-1.48-1-1.23 1.58.06.86-1.34.44 1.52 1.52.4-1.3.89z" style="--no:47"/>
<path d="m94.37 80.9-1.12-.38-.9.76.03-1.18-1-.62 1.12-.34.28-1.14.68.96 1.17-.09-.7.94z" style="--no:48"/>
<path d="m53.76 83.53-.99-.56-.99.55.24-1.11-.84-.78 1.13-.12.48-1.03.46 1.04 1.13.13-.84.77z" style="--no:49"/>
<path d="m49.34 84.58-2.28-1.18-2.18 1.36.41-2.53-1.96-1.66 2.54-.39.97-2.37 1.15 2.3 2.56.18-1.82 1.8z" style="--no:50"/>
<path d="m40.4 85.88-1.61-1.35-1.99.68.8-1.94-1.27-1.68 2.1.15 1.2-1.72.5 2.04 2.01.62-1.79 1.1z" style="--no:51"/>
<path d="m34.53 85.6-3.5-1.38-2.97 2.3.23-3.76-3.1-2.13 3.64-.93 1.07-3.6 2 3.17 3.77-.1-2.4 2.9z" style="--no:52"/>
<path d="m23.9 86.9-1.68-1.5-2.15.66L21 84l-1.3-1.84 2.24.24 1.35-1.8.47 2.2 2.13.72-1.95 1.13z" style="--no:53"/>
<path d="m18.21 88.34-1.62-.6-1.34 1.1.06-1.73-1.45-.94 1.66-.47.44-1.67.97 1.43 1.72-.1-1.06 1.37z" style="--no:54"/>
<path d="m35.48 77.6-1.07-.58-1.05.62.22-1.2-.92-.8 1.21-.16.48-1.12.53 1.1 1.21.11-.88.84z" style="--no:55"/>
<path d="M41.72 78.62 40.12 77l-2.2.54 1.05-2.02-1.2-1.93 2.25.37 1.47-1.73.33 2.25 2.1.85-2.03 1.02z" style="--no:56"/>
<path d="m49.62 74.51-1.93-.58-1.5 1.36-.03-2.02-1.76-1 1.9-.66.41-1.97 1.22 1.6 2-.22-1.14 1.66z" style="--no:57"/>
<path d="m55.27 71.78-1.65-.53-1.3 1.14v-1.73l-1.49-.88 1.64-.54.38-1.69 1.02 1.4 1.73-.16-1.02 1.4z" style="--no:58"/>
<path d="m63 71.81-2.43-1.38-2.42 1.38.56-2.73-2.06-1.88 2.77-.3 1.15-2.54 1.15 2.53 2.77.31-2.06 1.88z" style="--no:59"/>
<path d="m69.27 69.46-1.16-1.2-1.62.4.77-1.48-.87-1.41 1.64.27 1.08-1.27.24 1.65 1.54.64-1.49.74z" style="--no:60"/>
<path d="m76.01 70.02-1.24-.55-1.11.78.14-1.36-1.09-.82 1.33-.28.45-1.28.68 1.18 1.35.02-.9 1.01z" style="--no:61"/>
<path d="m81.2 67.28-1.02-.37-.84.7.03-1.1-.92-.58 1.05-.3.27-1.06.62.9 1.08-.06-.66.86z" style="--no:62"/>
<path d="m56.57 65.7-.93-.61-1.01.48.3-1.08-.77-.81 1.11-.05.54-.98.39 1.05 1.1.2-.88.7z" style="--no:63"/>
<path d="m50.57 67.67-1.71-1.65-2.3.63 1.04-2.14-1.3-1.98 2.35.32L50.14 61l.41 2.34 2.23.84-2.1 1.12z" style="--no:64"/>
<path d="m43.73 67.04-1.33-1.07-1.59.58.61-1.58-1.05-1.33 1.7.09.94-1.42.44 1.65 1.63.46-1.42.92z" style="--no:65"/>
<path d="m38.39 65.28-2.84-1.44-2.7 1.7.5-3.15-2.46-2.04 3.15-.5 1.18-2.97 1.45 2.84 3.19.21-2.25 2.26z" style="--no:66"/>
<path d="m29.58 65.7-1.85-.51-1.39 1.32-.08-1.91-1.69-.92 1.8-.67.35-1.89 1.2 1.51 1.9-.26-1.07 1.6z" style="--no:67"/>
<path d="m22.67 67.35-.95-.82-1.19.4.5-1.16-.75-1.01 1.25.1.73-1 .28 1.21 1.2.39-1.08.64z" style="--no:68"/>
<path d="m46.67 60.54-2.1-1.18-2.09 1.2.48-2.36-1.79-1.62 2.4-.28.99-2.2 1 2.2 2.4.26-1.78 1.62z" style="--no:69"/>
<path d="m52.22 59.39-1.15-.58-1.08.7.2-1.27-1-.82 1.27-.2.46-1.2.6 1.14 1.27.08-.9.9z" style="--no:70"/>
<path d="m57.73 56.34-.74-.95-1.19.16.67-1-.52-1.08 1.15.33.88-.82.04 1.2 1.05.57-1.12.4z" style="--no:71"/>
<path d="m66.78 48.61-1.14-.34-.88.81-.03-1.19-1.04-.59 1.12-.4.24-1.17.73.95 1.18-.13-.68.98z" style="--no:72"/>
<path d="m60.82 51.53-1.55-1.18-1.8.73.63-1.83-1.25-1.49 1.95.04 1.03-1.65.56 1.86 1.88.47-1.6 1.1z" style="--no:73"/>
<path d="m53.55 54.76-2.32-1.74-2.68 1.11.93-2.74-1.88-2.21 2.9.04 1.5-2.48.86 2.77 2.82.68-2.37 1.67z" style="--no:74"/>
<path d="M45.34 50.09 44 49.44l-1.23.82.2-1.47-1.16-.93 1.46-.26.52-1.38.7 1.3 1.48.07-1.03 1.07z" style="--no:75"/>
<path d="m40.18 48.79-1.77-1.15-1.9.93.55-2.04-1.47-1.51 2.1-.11.99-1.87.76 1.97 2.08.36-1.64 1.33z" style="--no:76"/>
<path d="m33.37 48.93-1.14-.4-.92.79.02-1.21-1.03-.64 1.16-.35.28-1.18.7 1 1.2-.1-.73.97z" style="--no:77"/>
<path d="m44 43.45-1.1-.09-.66.9-.25-1.08-1.06-.34.95-.58v-1.11l.84.73 1.06-.35-.44 1.03z" style="--no:78"/>
<path d="m47.65 45.49-1.2-1.17-1.63.44.74-1.51-.92-1.41 1.67.23 1.06-1.3.28 1.65 1.58.6-1.49.79z" style="--no:79"/>
<path d="m55.3 43.35-2.53-.88-2.06 1.72.06-2.68-2.28-1.42 2.57-.78.65-2.6 1.53 2.2 2.68-.19-1.62 2.14z" style="--no:80"/>
<path d="m59.03 37.84-1.01-.56-1 .58.22-1.13-.86-.77 1.15-.14.46-1.06.49 1.05 1.14.12-.84.78z" style="--no:81"/>
<path d="m53.55 34.33-1.91-1.11-1.93 1.07.46-2.16-1.62-1.51 2.2-.22.94-2.01.9 2.02 2.19.27-1.65 1.48z" style="--no:82"/>
<path d="m46.29 36.65-.8-.77-1.05.3.48-1-.6-.91 1.09.15.68-.85.2 1.08 1.02.39-.97.51z" style="--no:83"/>
<path d="m39.44 36.68-1.15-1.11-1.56.42.7-1.45-.88-1.34 1.6.22 1-1.25.29 1.58 1.5.57-1.42.75z" style="--no:84"/>
<path d="m46.78 32.72-2.41-1.47-2.49 1.33.65-2.75-2.03-1.96 2.8-.23 1.25-2.53 1.09 2.6 2.78.4-2.13 1.83z" style="--no:85"/>
<path d="m50.22 23.8-2.4-.6-1.74 1.73-.16-2.46-2.19-1.13 2.3-.9.39-2.44 1.57 1.9 2.43-.38-1.32 2.08z" style="--no:86"/>
<path d="m48.64 16.71-1.1-.7-1.17.59.33-1.26-.92-.93 1.3-.08.6-1.16.48 1.21 1.29.22-1.01.82z" style="--no:87"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,16 @@
.code-block {
display: block;
white-space: pre-wrap; /* Allows the code to wrap within the container */
word-wrap: break-word; /* Ensures long words break to fit the container */
max-width: 100%; /* Ensures the code block doesn't exceed the container's width */
padding: 10px; /* Adds padding for better readability */
background-color: #f5f5f5; /* Light gray background for contrast */
border: 1px solid #ddd; /* Subtle border to distinguish the code block */
border-radius: 5px; /* Rounded corners for aesthetics */
overflow-x: auto; /* Adds horizontal scroll if necessary */
}
.code-block .comment {
text-indent: 8em;
color: #408080;
}

View File

@@ -0,0 +1,16 @@
.image-container {
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
height: 20vh; /* Show 20% of the viewport height */
overflow: hidden; /* Hide parts of the image outside the container */
position: relative; /* Position context for the image */
}
.image-container img {
width: 100%; /* Make the image fit the container width */
height: auto; /* Maintain aspect ratio */
position: absolute; /* Position image relative to container */
top: 50%; /* Move image down by 50% of its height */
transform: translateY(-50%); /* Pull it back up by 50% of its own height */
}

View File

@@ -0,0 +1,10 @@
/* --- Decoration --- */
.background-svg {
position: fixed;
bottom: 100px;
right: 200px;
width: 200px;
z-index: -1;
pointer-events: none;
opacity: 0.6;
}

View File

@@ -221,17 +221,6 @@ nav {
border-color: #ccc;
}
/* --- Decoration --- */
.background-svg {
position: fixed;
bottom: 100px;
right: 200px;
width: 200px;
z-index: -1;
pointer-events: none;
opacity: 0.6;
}
/* Login, Sign up, New post */
.login,
.signup {

39
static/favicon.svg Normal file
View File

@@ -0,0 +1,39 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2415 4020 c-645 -57 -1178 -445 -1378 -1004 l-32 -90 -105 -31
c-370 -111 -623 -269 -706 -441 -23 -49 -28 -72 -28 -139 -1 -71 3 -88 31
-145 92 -187 365 -349 785 -466 70 -19 174 -45 232 -57 l105 -22 86 -80 c228
-211 505 -354 806 -415 288 -59 609 -41 891 50 126 41 334 146 441 223 48 35
126 99 173 143 67 62 94 81 127 87 246 47 519 132 696 218 202 97 325 199 384
319 28 57 32 74 31 145 0 67 -5 90 -28 139 -82 170 -337 330 -700 440 l-109
34 -29 78 c-190 524 -651 895 -1233 994 -134 22 -319 31 -440 20z m274 -160
c366 -33 677 -170 922 -407 155 -150 266 -323 328 -508 43 -130 47 -161 24
-173 -74 -39 -304 -100 -508 -136 -295 -51 -436 -61 -900 -61 -461 0 -575 8
-881 60 -271 46 -534 124 -534 159 0 34 55 199 94 282 65 137 137 237 260 360
271 272 621 417 1046 433 19 0 86 -4 149 -9z m-1719 -1128 c0 -34 33 -65 106
-98 226 -103 605 -180 1047 -214 178 -14 697 -14 874 0 455 35 817 109 1048
215 72 32 105 63 105 97 0 10 2 18 4 18 19 0 232 -81 312 -120 232 -110 351
-233 330 -343 -18 -95 -118 -187 -311 -282 -156 -77 -283 -122 -499 -175
-1067 -264 -2632 -182 -3351 174 -159 79 -251 152 -289 227 -14 29 -26 66 -26
81 0 131 198 281 525 398 129 46 125 45 125 22z m923 -1187 c448 -44 988 -40
1445 11 79 9 146 14 148 11 3 -3 -32 -30 -78 -60 -506 -335 -1182 -336 -1693
-3 -47 31 -85 59 -85 62 0 4 26 4 58 0 31 -4 124 -14 205 -21z"/>
<path d="M735 2335 c-29 -28 -32 -70 -10 -103 23 -32 129 -104 220 -150 182
-91 426 -157 715 -192 280 -33 851 -32 904 3 33 22 44 63 25 96 -25 45 -48 49
-249 40 -208 -10 -521 2 -705 26 -330 43 -609 137 -756 255 -72 58 -106 64
-144 25z"/>
<path d="M3505 2110 c-116 -17 -132 -21 -152 -45 -29 -33 -29 -64 -2 -99 25
-32 58 -33 241 -8 134 19 161 31 172 78 7 28 -16 79 -39 88 -25 9 -76 6 -220
-14z"/>
<path d="M2735 2028 c-35 -30 -37 -79 -5 -113 23 -25 26 -25 190 -25 154 0
169 2 194 21 35 28 36 80 1 114 -24 25 -27 25 -189 25 -157 0 -166 -1 -191
-22z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,13 +1,9 @@
{% extends "base.html" %} {% block title %}About - Simple Blog Template{%
endblock %} {% block content %}
{% extends "base.html" %} {% block title %}{{blog_title.title}}{% endblock %}{%
block content %}
<div class="row">
<div class="col-lg-12">
<h1>About</h1>
<hr />
<!-- Post Content -->
<div>{{ about_txt.content | safe }}</div>
<p>Last Update : October 2024</p>
<hr />
</div>
</div>
{% endblock %}

View File

@@ -8,6 +8,10 @@
<meta name="author" content="" />
<title>{% block title %}Default Title{% endblock %}</title>
<link
rel="shortcut icon"
href="{{ url_for('static', filename='favicon.svg') }}"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@@ -27,10 +31,6 @@
href="{{ url_for('static', filename='css/blog.css') }}"
rel="stylesheet"
/>
<link
href="{{ url_for('static', filename='css/components/timeline.css') }}"
rel="stylesheet"
/>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
@@ -41,6 +41,7 @@
};
</script>
<script src="{{ url_for('static', filename='js/timeline.js') }}"></script>
<style></style>
</head>
<body>

View File

@@ -1,5 +0,0 @@
<div class="background-svg">
<img src="{{ url_for('static', filename='animation.svg') }}" alt="" />
<br />
<p>tree source: codepen @uchardon</p>
</div>

View File

@@ -6,7 +6,7 @@
{% else %}
<div class="blog-preview">{{ post.content | striptags | truncate(200) }}</div>
<div class="blog-preview">{{ post.preview }}</div>
<a class="btn-read-more" href="{{ url_for('post_detail', post_id=post.id) }}">
Read More

View File

@@ -1,3 +0,0 @@
{% macro render_post(post) %}
<article class="blog-content">{{ post.content | safe }}</article>
{% if post.template %} {% include post.template %} {% endif %} {% endmacro %}

View File

@@ -1,9 +0,0 @@
<div class="tw-w-full tw-mx-auto tw-px-0 tw-py-12">
<ol
class="tw-relative tw-border-l-2 tw-border-gray-300 tw-list-none tw-p-0 tw-m-0 tw-ml-4"
>
{{ post.timeline | safe }}
</ol>
</div>
<li class="tw-mb-12 tw-ml-6 tw-list-none tw-relative"></li>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% block title %}{{ post.title }} - Simple Blog
Template{% endblock %} {% block content %} {% from
"components/post_renderer.html" import render_post %} {% from
{% for css in component_css %}
<link rel="stylesheet" href="{{ url_for('static', filename=css) }}" />
{% endfor %} {% extends "base.html" %} {% block title %}{{ post.title }} -
Simple Blog Template{% endblock %} {% block content %} {% from
"components/comment_renderer.html" import render_comment %}
<div class="row">
<div class="col-lg-12">
@@ -9,7 +10,7 @@ Template{% endblock %} {% block content %} {% from
<p>
<span class="glyphicon glyphicon-time"></span> Posted on {{ post.date }}
</p>
{{ render_post(post) }}
{{ content | safe }}
<div class="post-actions">
<a href="{{ url_for('home') }}" class="btn btn-default btn-custom"
>← Back to Posts</a

View File

@@ -1,3 +0,0 @@
<h1 class="text-3xl font-bold mb-8">Project Timeline</h1>
{% include "components/timeline.html" %}