Compare commits

...

10 Commits

Author SHA1 Message Date
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
8 changed files with 242 additions and 91 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"]

86
app.py
View File

@@ -1,6 +1,6 @@
from flask import Flask, render_template, request, redirect, url_for from flask import Flask, render_template, request, redirect, url_for
from content.posts import BLOG_POSTS 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
# 🌟 IMPORT THE content from separate files. # 🌟 IMPORT THE content from separate files.
from content.posts import BLOG_POSTS from content.posts import BLOG_POSTS
@@ -16,15 +16,6 @@ COMMENT_FILE = 'content/comments.csv'
MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5 Megabytes MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5 Megabytes
POSTS_PER_PAGE = 5 # posts per page limit here 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']) @app.route('/post/<int:post_id>/comment', methods=['POST'])
def post_comment(post_id): def post_comment(post_id):
@@ -43,35 +34,66 @@ def post_comment(post_id):
# --- Routes --- # --- 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('/') @app.route('/')
def home(): def home():
"""Home page with paginated blog posts."""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
blog_title = TITLE 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 pagination = calculate_pagination(BLOG_POSTS, POSTS_PER_PAGE, 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
return render_template('index.html', return render_template('index.html',
posts=posts_to_show, posts=pagination['posts_to_show'],
prev_url=prev_url, prev_url=pagination['prev_url'],
next_url=next_url, next_url=pagination['next_url'],
current_page=page, current_page=pagination['current_page'],
blog_title=blog_title, total_pages=pagination['total_pages'],
total_pages=total_pages) blog_title=blog_title,
total_posts=pagination['total_posts'])

Submodule content updated: 182725bce2...d0e3035242

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:

93
flask_logic/logic.py Normal file
View File

@@ -0,0 +1,93 @@
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)
if not post:
return None
# Mapping config: keeps logic.py tiny
configs = {
10: {
"template": "components/timeline.html",
"timeline_file": "post_10_timeline.html"
},
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

87
readme.md Normal file
View File

@@ -0,0 +1,87 @@
# Flask Blog Templates
A minimal **Flask template set** for my blog.
This project adapts and evolves the original [simple-blog-template](https://github.com/earlbread/simple-blog-template) by [Seunghun Lee](https://github.com/earlbread).
## 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).