Compare commits
2 Commits
fcb38e593c
...
0eccc4b12e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eccc4b12e | |||
| b23f2399c4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
|
|
||||||
# This should be generated on deploy
|
# This should be generated on deploy
|
||||||
static/feed.rss
|
static/feed.rss
|
||||||
|
static/sitemap.xml
|
||||||
@@ -32,6 +32,12 @@ python scripts/generate_rss.py
|
|||||||
```
|
```
|
||||||
Run this after adding/updating comics to regenerate `static/feed.rss`.
|
Run this after adding/updating comics to regenerate `static/feed.rss`.
|
||||||
|
|
||||||
|
**Generate sitemap:**
|
||||||
|
```bash
|
||||||
|
python scripts/generate_sitemap.py
|
||||||
|
```
|
||||||
|
Run this after adding/updating comics to regenerate `static/sitemap.xml` for search engines.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Data Layer: comics_data.py
|
### Data Layer: comics_data.py
|
||||||
@@ -134,6 +140,7 @@ Global context variables injected into all templates:
|
|||||||
- `static/images/icons/`: Navigation icons (first.png, previous.png, next.png, latest.png) used when `USE_ICON_NAV` is True
|
- `static/images/icons/`: Navigation icons (first.png, previous.png, next.png, latest.png) used when `USE_ICON_NAV` is True
|
||||||
- `static/images/`: Header images and other site graphics
|
- `static/images/`: Header images and other site graphics
|
||||||
- `static/feed.rss`: Generated RSS feed (run `scripts/generate_rss.py`)
|
- `static/feed.rss`: Generated RSS feed (run `scripts/generate_rss.py`)
|
||||||
|
- `static/sitemap.xml`: Generated sitemap (run `scripts/generate_sitemap.py`)
|
||||||
|
|
||||||
## Important Implementation Details
|
## Important Implementation Details
|
||||||
|
|
||||||
|
|||||||
24
app.py
24
app.py
@@ -232,6 +232,30 @@ def api_comic(comic_id):
|
|||||||
return jsonify(comic)
|
return jsonify(comic)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/sitemap.xml')
|
||||||
|
def sitemap():
|
||||||
|
"""Serve the static sitemap.xml file"""
|
||||||
|
from flask import send_from_directory
|
||||||
|
return send_from_directory('static', 'sitemap.xml', mimetype='application/xml')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/robots.txt')
|
||||||
|
def robots():
|
||||||
|
"""Generate robots.txt dynamically with correct SITE_URL"""
|
||||||
|
from flask import Response
|
||||||
|
robots_txt = f"""# Sunday Comics - Robots.txt
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap location
|
||||||
|
Sitemap: {SITE_URL}/sitemap.xml
|
||||||
|
|
||||||
|
# Disallow API endpoints from indexing
|
||||||
|
Disallow: /api/
|
||||||
|
"""
|
||||||
|
return Response(robots_txt, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
"""404 error handler"""
|
"""404 error handler"""
|
||||||
|
|||||||
88
scripts/generate_sitemap.py
Normal file
88
scripts/generate_sitemap.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Sunday Comics - Sitemap generator
|
||||||
|
# Copyright (c) 2025 Tomasita Cabrera
|
||||||
|
# Licensed under the MIT License - see LICENSE file for details
|
||||||
|
|
||||||
|
"""
|
||||||
|
Script to generate a sitemap.xml file for the comic
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||||
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
# Add parent directory to path so we can import comics_data
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from comics_data import COMICS, SITE_URL
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sitemap():
|
||||||
|
"""Generate sitemap.xml from COMICS data"""
|
||||||
|
# Create sitemap root
|
||||||
|
urlset = Element('urlset', xmlns='http://www.sitemaps.org/schemas/sitemap/0.9')
|
||||||
|
|
||||||
|
# Add homepage
|
||||||
|
if COMICS:
|
||||||
|
latest_date = COMICS[-1]['date']
|
||||||
|
url = SubElement(urlset, 'url')
|
||||||
|
SubElement(url, 'loc').text = f'{SITE_URL}/'
|
||||||
|
SubElement(url, 'lastmod').text = latest_date
|
||||||
|
SubElement(url, 'changefreq').text = 'weekly'
|
||||||
|
SubElement(url, 'priority').text = '1.0'
|
||||||
|
|
||||||
|
# Add archive page
|
||||||
|
if COMICS:
|
||||||
|
latest_date = COMICS[-1]['date']
|
||||||
|
url = SubElement(urlset, 'url')
|
||||||
|
SubElement(url, 'loc').text = f'{SITE_URL}/archive'
|
||||||
|
SubElement(url, 'lastmod').text = latest_date
|
||||||
|
SubElement(url, 'changefreq').text = 'weekly'
|
||||||
|
SubElement(url, 'priority').text = '0.9'
|
||||||
|
|
||||||
|
# Add about page
|
||||||
|
url = SubElement(urlset, 'url')
|
||||||
|
SubElement(url, 'loc').text = f'{SITE_URL}/about'
|
||||||
|
SubElement(url, 'changefreq').text = 'monthly'
|
||||||
|
SubElement(url, 'priority').text = '0.7'
|
||||||
|
|
||||||
|
# Add all individual comic pages
|
||||||
|
for comic in COMICS:
|
||||||
|
url = SubElement(urlset, 'url')
|
||||||
|
SubElement(url, 'loc').text = f"{SITE_URL}/comic/{comic['number']}"
|
||||||
|
SubElement(url, 'lastmod').text = comic['date']
|
||||||
|
SubElement(url, 'changefreq').text = 'never'
|
||||||
|
SubElement(url, 'priority').text = '0.8'
|
||||||
|
|
||||||
|
# Convert to pretty XML
|
||||||
|
xml_str = minidom.parseString(tostring(urlset)).toprettyxml(indent=' ')
|
||||||
|
|
||||||
|
# Remove extra blank lines
|
||||||
|
xml_str = '\n'.join([line for line in xml_str.split('\n') if line.strip()])
|
||||||
|
|
||||||
|
return xml_str
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Generate and save sitemap"""
|
||||||
|
# Get path to static folder
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
parent_dir = os.path.dirname(script_dir)
|
||||||
|
static_dir = os.path.join(parent_dir, 'static')
|
||||||
|
|
||||||
|
# Create static directory if it doesn't exist
|
||||||
|
os.makedirs(static_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Generate sitemap
|
||||||
|
sitemap_content = generate_sitemap()
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
sitemap_file = os.path.join(static_dir, 'sitemap.xml')
|
||||||
|
with open(sitemap_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(sitemap_content)
|
||||||
|
|
||||||
|
print(f"Sitemap generated: {sitemap_file}")
|
||||||
|
print(f"Total URLs: {len(COMICS) + 3}") # comics + homepage + archive + about
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
<a href="{{ url_for('comic', comic_id=comic.number) }}">
|
<a href="{{ url_for('comic', comic_id=comic.number) }}">
|
||||||
<img src="{{ url_for('static', filename='images/thumbs/' + comic.filename) }}"
|
<img src="{{ url_for('static', filename='images/thumbs/' + comic.filename) }}"
|
||||||
onerror="this.onerror=null; this.src='{{ url_for('static', filename='images/thumbs/default.jpg') }}';"
|
onerror="this.onerror=null; this.src='{{ url_for('static', filename='images/thumbs/default.jpg') }}';"
|
||||||
alt="{{ comic.title if comic.title else '#' ~ comic.number }}">
|
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
||||||
|
loading="lazy">
|
||||||
<div class="archive-info">
|
<div class="archive-info">
|
||||||
{% if not archive_full_width %}
|
{% if not archive_full_width %}
|
||||||
<h3>#{{ comic.number }}{% if comic.title %}: {{ comic.title }}{% endif %}</h3>
|
<h3>#{{ comic.number }}{% if comic.title %}: {{ comic.title }}{% endif %}</h3>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<!-- SEO Meta Tags -->
|
<!-- SEO Meta Tags -->
|
||||||
<meta name="description" content="{% block meta_description %}A webcomic about life, the universe, and everything{% endblock %}">
|
<meta name="description" content="{% block meta_description %}A webcomic about life, the universe, and everything{% endblock %}">
|
||||||
|
<link rel="canonical" href="{% block canonical %}{{ site_url }}{{ request.path }}{% endblock %}">
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
|
|||||||
@@ -4,6 +4,27 @@
|
|||||||
|
|
||||||
{% block og_image %}{{ site_url }}/static/images/thumbs/{{ comic.filename }}{% endblock %}
|
{% block og_image %}{{ site_url }}/static/images/thumbs/{{ comic.filename }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "ComicStory",
|
||||||
|
"name": "{{ comic.title if comic.title else '#' ~ comic.number }}",
|
||||||
|
"datePublished": "{{ comic.date }}",
|
||||||
|
"image": "{{ site_url }}/static/images/comics/{{ comic.filename }}",
|
||||||
|
"thumbnailUrl": "{{ site_url }}/static/images/thumbs/{{ comic.filename }}",
|
||||||
|
"description": "{{ comic.alt_text }}",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "ComicSeries",
|
||||||
|
"name": "{{ comic_name }}",
|
||||||
|
"url": "{{ site_url }}"
|
||||||
|
},
|
||||||
|
"position": "{{ comic.number }}",
|
||||||
|
"url": "{{ site_url }}/comic/{{ comic.number }}"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- ARIA live region for screen reader announcements -->
|
<!-- ARIA live region for screen reader announcements -->
|
||||||
<div aria-live="polite" aria-atomic="true" class="sr-only" id="comic-announcer"></div>
|
<div aria-live="polite" aria-atomic="true" class="sr-only" id="comic-announcer"></div>
|
||||||
@@ -29,7 +50,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
||||||
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
||||||
title="{{ comic.alt_text }}">
|
title="{{ comic.alt_text }}"
|
||||||
|
loading="eager">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -43,7 +65,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
||||||
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
||||||
title="{{ comic.alt_text }}">
|
title="{{ comic.alt_text }}"
|
||||||
|
loading="eager">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user