Compare commits

..

2 Commits

Author SHA1 Message Date
mi
203b17be0e opengraph 2025-11-06 22:54:43 +10:00
mi
66eefa9349 :lightning: client-side nav 2025-11-06 22:48:39 +10:00
5 changed files with 188 additions and 2 deletions

View File

@@ -5,7 +5,11 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
## Features
- Comic viewer with navigation (First, Previous, Next, Latest)
- Client-side navigation using JSON API (no page reloads)
- Archive page with thumbnail grid
- RSS feed support
- JSON API for programmatic access
- Open Graph and Twitter Card metadata for social sharing
- Responsive design
- Server-side rendering with Jinja2
- Clean, comic-focused layout
@@ -33,6 +37,8 @@ sunday/
└── static/ # Static files
├── css/
│ └── style.css # Main stylesheet
├── js/
│ └── comic-nav.js # Client-side navigation
├── images/ # Comic images
│ └── thumbs/ # Thumbnail images for archive
└── feed.rss # RSS feed (generated)

151
static/js/comic-nav.js Normal file
View File

@@ -0,0 +1,151 @@
// Client-side comic navigation using the API
(function() {
'use strict';
let totalComics = 0;
// Fetch and display a comic
async function loadComic(comicId) {
try {
const response = await fetch(`/api/comics/${comicId}`);
if (!response.ok) {
if (response.status === 404) {
window.location.href = '/404';
return;
}
throw new Error('Failed to load comic');
}
const comic = response.json ? await response.json() : comic;
displayComic(comic);
} catch (error) {
console.error('Error loading comic:', error);
}
}
// Update the page with comic data
function displayComic(comic) {
// Update title
const title = comic.title || `#${comic.number}`;
document.querySelector('.comic-header h1').textContent = title;
// Update date
document.querySelector('.comic-date').textContent = comic.date;
// Update image
const img = document.querySelector('.comic-image img');
img.src = `/static/images/${comic.filename}`;
img.alt = title;
img.title = comic.alt_text;
// Update author note
const transcriptDiv = document.querySelector('.comic-transcript');
if (comic.author_note) {
if (!transcriptDiv) {
const container = document.querySelector('.comic-container');
const newDiv = document.createElement('div');
newDiv.className = 'comic-transcript';
newDiv.innerHTML = '<h3>Author Note</h3><p></p>';
container.appendChild(newDiv);
}
document.querySelector('.comic-transcript p').textContent = comic.author_note;
document.querySelector('.comic-transcript').style.display = 'block';
} else if (transcriptDiv) {
transcriptDiv.style.display = 'none';
}
// Update navigation buttons
updateNavButtons(comic.number);
// Update page title
document.title = `${title} - Sunday Comics`;
// Update URL without reload
history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`);
}
// Update navigation button states
function updateNavButtons(currentNumber) {
const navButtons = document.querySelector('.nav-buttons');
// First button
const firstBtn = navButtons.children[0];
if (currentNumber > 1) {
firstBtn.className = 'btn btn-nav';
firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); };
} else {
firstBtn.className = 'btn btn-nav btn-disabled';
firstBtn.onclick = null;
}
// Previous button
const prevBtn = navButtons.children[1];
if (currentNumber > 1) {
prevBtn.className = 'btn btn-nav';
prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); };
} else {
prevBtn.className = 'btn btn-nav btn-disabled';
prevBtn.onclick = null;
}
// Comic number display
navButtons.children[2].textContent = `Comic #${currentNumber}`;
// Next button
const nextBtn = navButtons.children[3];
if (currentNumber < totalComics) {
nextBtn.className = 'btn btn-nav';
nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); };
} else {
nextBtn.className = 'btn btn-nav btn-disabled';
nextBtn.onclick = null;
}
// Latest button
const latestBtn = navButtons.children[4];
if (currentNumber < totalComics) {
latestBtn.className = 'btn btn-nav';
latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); };
} else {
latestBtn.className = 'btn btn-nav btn-disabled';
latestBtn.onclick = null;
}
}
// Initialize on page load
async function init() {
// Only run on comic pages
if (!document.querySelector('.comic-container')) {
return;
}
// Get total comics count from the page
totalComics = parseInt(document.querySelector('.comic-container').dataset.totalComics || 0);
// Get current comic number
const currentNumber = parseInt(document.querySelector('.comic-container').dataset.comicNumber || 0);
if (currentNumber && totalComics) {
updateNavButtons(currentNumber);
}
// Handle browser back/forward
window.addEventListener('popstate', (event) => {
if (event.state && event.state.comicId) {
loadComic(event.state.comicId);
}
});
// Set initial state
history.replaceState({ comicId: currentNumber }, '', window.location.pathname);
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -4,6 +4,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ title }}{% endblock %} - Sunday Comics</title>
<!-- SEO Meta Tags -->
<meta name="description" content="{% block meta_description %}A webcomic about life, the universe, and everything{% endblock %}">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="{% block meta_url %}{{ request.url }}{% endblock %}">
<meta property="og:title" content="{% block og_title %}{{ self.title() }} - Sunday Comics{% endblock %}">
<meta property="og:description" content="{% block og_description %}{{ self.meta_description() }}{% endblock %}">
<meta property="og:image" content="{% block og_image %}{{ request.url_root }}static/images/default-preview.png{% endblock %}">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="{% block twitter_url %}{{ self.meta_url() }}{% endblock %}">
<meta property="twitter:title" content="{% block twitter_title %}{{ self.og_title() }}{% endblock %}">
<meta property="twitter:description" content="{% block twitter_description %}{{ self.og_description() }}{% endblock %}">
<meta property="twitter:image" content="{% block twitter_image %}{{ self.og_image() }}{% endblock %}">
<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 %}
@@ -61,6 +79,7 @@
</div>
</footer>
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,7 +1,11 @@
{% extends "base.html" %}
{% block meta_description %}{{ comic.alt_text }}{% if comic.author_note %} - {{ comic.author_note }}{% endif %}{% endblock %}
{% block og_image %}{{ request.url_root }}static/images/thumbs/{{ comic.filename }}{% endblock %}
{% block content %}
<div class="comic-container">
<div class="comic-container" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
<div class="comic-header">
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
<p class="comic-date">{{ comic.date }}</p>

View File

@@ -1,7 +1,13 @@
{% extends "base.html" %}
{% if comic %}
{% block meta_description %}{{ comic.alt_text }}{% if comic.author_note %} - {{ comic.author_note }}{% endif %}{% endblock %}
{% block og_image %}{{ request.url_root }}static/images/thumbs/{{ comic.filename }}{% endblock %}
{% endif %}
{% block content %}
<div class="comic-container">
<div class="comic-container" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
<div class="comic-header">
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
<p class="comic-date">{{ comic.date }}</p>