Compare commits
7 Commits
c7cd40f5a9
...
6d36d43b9e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d36d43b9e | |||
| a3f0132c7e | |||
| 08bc888df0 | |||
| 5ae0d2ab39 | |||
| 5905e773b9 | |||
| 881d63dbbb | |||
| a74b9dc9e0 |
42
.dockerignore
Normal file
42
.dockerignore
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Production Dockerfile for Sunday Comics
|
||||||
|
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY --chown=appuser:appuser requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY --chown=appuser:appuser . .
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3000').read()"
|
||||||
|
|
||||||
|
# Run with Gunicorn
|
||||||
|
CMD gunicorn app:app \
|
||||||
|
--bind 0.0.0.0:${PORT} \
|
||||||
|
--workers 4 \
|
||||||
|
--threads 2 \
|
||||||
|
--worker-class gthread \
|
||||||
|
--access-logfile - \
|
||||||
|
--error-logfile - \
|
||||||
|
--log-level info
|
||||||
190
README.md
190
README.md
@@ -15,21 +15,27 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
|
|||||||
```
|
```
|
||||||
sunday/
|
sunday/
|
||||||
├── app.py # Main Flask application
|
├── app.py # Main Flask application
|
||||||
|
├── comics_data.py # Comic data (edit this to add comics)
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
|
├── Dockerfile # Production Docker image
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
├── .dockerignore # Docker build exclusions
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
│ ├── add_comic.py # Script to add new comic entries
|
||||||
|
│ └── generate_rss.py # Script to generate RSS feed
|
||||||
├── templates/ # Jinja2 templates
|
├── templates/ # Jinja2 templates
|
||||||
│ ├── base.html # Base template with navigation
|
│ ├── base.html # Base template with navigation
|
||||||
│ ├── index.html # Latest comic page
|
│ ├── index.html # Latest comic page
|
||||||
│ ├── comic.html # Individual comic viewer
|
│ ├── comic.html # Individual comic viewer
|
||||||
│ ├── archive.html # Archive grid
|
│ ├── archive.html # Archive grid
|
||||||
│ ├── about.html # About page
|
│ ├── about.html # About page
|
||||||
│ ├── contact.html # Contact page
|
|
||||||
│ └── 404.html # Error page
|
│ └── 404.html # Error page
|
||||||
└── static/ # Static files
|
└── static/ # Static files
|
||||||
├── css/
|
├── css/
|
||||||
│ └── style.css # Main stylesheet
|
│ └── style.css # Main stylesheet
|
||||||
├── js/
|
├── images/ # Comic images
|
||||||
└── images/ # Comic images
|
│ └── thumbs/ # Thumbnail images for archive
|
||||||
└── thumbs/ # Thumbnail images for archive
|
└── feed.rss # RSS feed (generated)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
@@ -44,32 +50,154 @@ pip install -r requirements.txt
|
|||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Visit http://127.0.0.1:5000 in your browser
|
3. Visit http://127.0.0.1:3000 in your browser
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The app can be configured via environment variables:
|
||||||
|
|
||||||
|
- `SECRET_KEY` - Flask secret key (defaults to 'your-secret-key')
|
||||||
|
- `PORT` - Port to run on (defaults to 3000)
|
||||||
|
- `DEBUG` - Enable debug mode (defaults to False)
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
```bash
|
||||||
|
export DEBUG=True
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
```bash
|
||||||
|
export SECRET_KEY="your-secure-random-secret-key"
|
||||||
|
export PORT=3000
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
## Adding Comics
|
## Adding Comics
|
||||||
|
|
||||||
Currently, comics are stored in the `COMICS` list in `app.py`. Each comic needs:
|
Comics are stored in the `COMICS` list in `comics_data.py`. Each comic entry:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
{
|
{
|
||||||
'number': 1, # Comic number (sequential)
|
'number': 1, # Comic number (required, sequential)
|
||||||
'title': 'Comic Title', # Title of the comic
|
'filename': 'comic-001.png', # Image filename (required)
|
||||||
'filename': 'comic-001.png', # Image filename
|
'date': '2025-01-01', # Publication date (required)
|
||||||
'date': '2025-01-01', # Publication date
|
'alt_text': 'Alt text for comic', # Accessibility text (required)
|
||||||
'alt_text': 'Alt text for comic', # Accessibility text
|
'title': 'Comic Title', # Title (optional, shows #X if absent)
|
||||||
'transcript': 'Optional transcript' # Optional transcript
|
'author_note': 'Optional note' # Author note (optional)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding a New Comic
|
### Adding a New Comic
|
||||||
|
|
||||||
|
**Option 1: Use the script (recommended)**
|
||||||
|
```bash
|
||||||
|
python scripts/add_comic.py
|
||||||
|
```
|
||||||
|
This will automatically add a new entry with defaults. Then edit `comics_data.py` to customize.
|
||||||
|
|
||||||
|
**Option 2: Manual**
|
||||||
1. Save your comic image in `static/images/` (e.g., `comic-001.png`)
|
1. Save your comic image in `static/images/` (e.g., `comic-001.png`)
|
||||||
2. Optionally, create a thumbnail in `static/images/thumbs/` with the same filename
|
2. Optionally, create a thumbnail in `static/images/thumbs/` with the same filename
|
||||||
3. Add the comic entry to the `COMICS` list in `app.py`
|
3. Add the comic entry to the `COMICS` list in `comics_data.py`
|
||||||
|
|
||||||
|
### Generating RSS Feed
|
||||||
|
|
||||||
|
After adding comics, regenerate the RSS feed:
|
||||||
|
```bash
|
||||||
|
python scripts/generate_rss.py
|
||||||
|
```
|
||||||
|
This creates/updates `static/feed.rss`
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
For production, you should **NOT** use Flask's built-in development server. Choose one of the following deployment methods:
|
||||||
|
|
||||||
|
### Option 1: Docker (Recommended)
|
||||||
|
|
||||||
|
**1. Generate a secure secret key:**
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Create a `.env` file:**
|
||||||
|
```bash
|
||||||
|
SECRET_KEY=your-generated-secret-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Build and run with Docker Compose:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or build and run manually:**
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build -t sunday-comics .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -d \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e SECRET_KEY="your-secret-key" \
|
||||||
|
-v $(pwd)/comics_data.py:/app/comics_data.py:ro \
|
||||||
|
-v $(pwd)/static/images:/app/static/images:ro \
|
||||||
|
--name sunday-comics \
|
||||||
|
sunday-comics
|
||||||
|
```
|
||||||
|
|
||||||
|
**View logs:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Manual Deployment with Gunicorn
|
||||||
|
|
||||||
|
**1. Generate a Secure Secret Key**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Set Environment Variables**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SECRET_KEY="generated-secret-key-from-above"
|
||||||
|
export DEBUG=False
|
||||||
|
export PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Use a Production WSGI Server**
|
||||||
|
|
||||||
|
**Install Gunicorn:**
|
||||||
|
```bash
|
||||||
|
pip install gunicorn
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run with Gunicorn:**
|
||||||
|
```bash
|
||||||
|
gunicorn app:app --bind 0.0.0.0:3000 --workers 4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a Reverse Proxy (Recommended)
|
||||||
|
|
||||||
|
Set up Nginx or another reverse proxy in front of your app for:
|
||||||
|
- HTTPS/SSL termination
|
||||||
|
- Static file serving
|
||||||
|
- Load balancing
|
||||||
|
- Better security
|
||||||
|
|
||||||
|
### Additional Production Considerations
|
||||||
|
|
||||||
|
- Use a process manager (systemd, supervisor) for non-Docker deployments
|
||||||
|
- Set appropriate file permissions
|
||||||
|
- Enable HTTPS with Let's Encrypt
|
||||||
|
- Consider using a CDN for static assets
|
||||||
|
- Monitor logs and performance
|
||||||
|
- Set up automated backups of `comics_data.py`
|
||||||
|
|
||||||
## Upgrading to a Database
|
## Upgrading to a Database
|
||||||
|
|
||||||
For production use, consider replacing the `COMICS` list with a database:
|
For larger comic archives, consider replacing the `COMICS` list with a database:
|
||||||
|
|
||||||
- SQLite for simple setups
|
- SQLite for simple setups
|
||||||
- PostgreSQL/MySQL for production
|
- PostgreSQL/MySQL for production
|
||||||
@@ -94,7 +222,39 @@ For production use, consider replacing the `COMICS` list with a database:
|
|||||||
- `/comic/<id>` - View a specific comic by number
|
- `/comic/<id>` - View a specific comic by number
|
||||||
- `/archive` - Browse all comics in a grid
|
- `/archive` - Browse all comics in a grid
|
||||||
- `/about` - About the comic and author
|
- `/about` - About the comic and author
|
||||||
- `/contact` - Contact form
|
- `/static/feed.rss` - RSS feed
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The app exposes a JSON API for programmatic access:
|
||||||
|
|
||||||
|
- **GET `/api/comics`** - Returns all comics as a JSON array
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"number": 1,
|
||||||
|
"title": "First Comic",
|
||||||
|
"filename": "comic-001.png",
|
||||||
|
"date": "2025-01-01",
|
||||||
|
"alt_text": "The very first comic",
|
||||||
|
"author_note": "This is where your comic journey begins!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **GET `/api/comics/<id>`** - Returns a specific comic as JSON
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"number": 1,
|
||||||
|
"title": "First Comic",
|
||||||
|
"filename": "comic-001.png",
|
||||||
|
"date": "2025-01-01",
|
||||||
|
"alt_text": "The very first comic",
|
||||||
|
"author_note": "This is where your comic journey begins!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns 404 with `{"error": "Comic not found"}` if the comic doesn't exist.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
28
app.py
28
app.py
@@ -1,10 +1,11 @@
|
|||||||
from flask import Flask, render_template, abort
|
import os
|
||||||
|
from flask import Flask, render_template, abort, jsonify, request
|
||||||
from comics_data import COMICS
|
from comics_data import COMICS
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-here' # Change this in production
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-secret-key')
|
||||||
|
|
||||||
|
|
||||||
def get_comic_by_number(number):
|
def get_comic_by_number(number):
|
||||||
@@ -58,11 +59,32 @@ def about():
|
|||||||
return render_template('about.html', title='About')
|
return render_template('about.html', title='About')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/comics')
|
||||||
|
def api_comics():
|
||||||
|
"""API endpoint - returns all comics as JSON"""
|
||||||
|
return jsonify(COMICS)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/comics/<int:comic_id>')
|
||||||
|
def api_comic(comic_id):
|
||||||
|
"""API endpoint - returns a specific comic as JSON"""
|
||||||
|
comic = get_comic_by_number(comic_id)
|
||||||
|
if not comic:
|
||||||
|
return jsonify({'error': 'Comic not found'}), 404
|
||||||
|
return jsonify(comic)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
"""404 error handler"""
|
"""404 error handler"""
|
||||||
|
# Return JSON for API requests
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({'error': 'Not found'}), 404
|
||||||
|
# Return HTML for regular pages
|
||||||
return render_template('404.html', title='Page Not Found'), 404
|
return render_template('404.html', title='Page Not Found'), 404
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
port = int(os.environ.get('PORT', 3000))
|
||||||
|
debug = os.environ.get('DEBUG', 'False').lower() in ('true', '1', 't')
|
||||||
|
app.run(debug=debug, port=port)
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-please-change-this-secret-key}
|
||||||
|
- PORT=3000
|
||||||
|
- DEBUG=False
|
||||||
|
volumes:
|
||||||
|
# Mount comics data for easy updates without rebuilding
|
||||||
|
- ./comics_data.py:/app/comics_data.py:ro
|
||||||
|
- ./static/images:/app/static/images:ro
|
||||||
|
- ./static/feed.rss:/app/static/feed.rss:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000').read()"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from comics_data import COMICS
|
from comics_data import COMICS
|
||||||
|
|
||||||
# Configuration - update these for your site
|
# Configuration - update these for your site
|
||||||
SITE_URL = "http://localhost:5000" # Change to your actual domain
|
SITE_URL = "http://localhost:3000" # Change to your actual domain
|
||||||
SITE_TITLE = "Sunday Comics"
|
SITE_TITLE = "Sunday Comics"
|
||||||
SITE_DESCRIPTION = "A webcomic about life, the universe, and everything"
|
SITE_DESCRIPTION = "A webcomic about life, the universe, and everything"
|
||||||
SITE_LANGUAGE = "en-us"
|
SITE_LANGUAGE = "en-us"
|
||||||
|
|||||||
@@ -342,12 +342,88 @@ main {
|
|||||||
/* Footer */
|
/* Footer */
|
||||||
footer {
|
footer {
|
||||||
border-top: 3px solid #000;
|
border-top: 3px solid #000;
|
||||||
text-align: center;
|
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer p {
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
color: #000;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form input[type="email"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 2px solid #000;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form button:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-placeholder {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{{ title }}{% endblock %} - Sunday Comics</title>
|
<title>{% block title %}{{ title }}{% endblock %} - Sunday Comics</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="Sunday Comics RSS Feed" href="{{ url_for('static', filename='feed.rss') }}">
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -31,8 +32,33 @@
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>Follow</h3>
|
||||||
|
<div class="social-links">
|
||||||
|
<!-- Uncomment and update with your social links -->
|
||||||
|
<!-- <a href="https://twitter.com/yourhandle" target="_blank">Twitter</a> -->
|
||||||
|
<!-- <a href="https://instagram.com/yourhandle" target="_blank">Instagram</a> -->
|
||||||
|
<!-- <a href="https://mastodon.social/@yourhandle" target="_blank">Mastodon</a> -->
|
||||||
|
<a href="{{ url_for('static', filename='feed.rss') }}">RSS Feed</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>Newsletter</h3>
|
||||||
|
<!-- Replace with your newsletter service form -->
|
||||||
|
<!-- <form class="newsletter-form" action="#" method="post">
|
||||||
|
<input type="email" name="email" placeholder="Enter your email" required>
|
||||||
|
<button type="submit">Subscribe</button>
|
||||||
|
</form> -->
|
||||||
|
<p class="newsletter-placeholder">Newsletter coming soon!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
<p>© 2025 Sunday Comics. All rights reserved.</p>
|
<p>© 2025 Sunday Comics. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user