feat(ui): add error handling, flash messages and SEO optimization
- Add custom error pages (404, 403, 500) with user-friendly messages
- Add flash message system with signed cookies for security
- Add toast notifications with auto-dismiss and manual close
- Add comprehensive SEO meta tags (description, keywords, OG, Twitter)
- Add canonical URLs for SEO
- Update routes to use slug-based URLs (/posts/{slug} instead of /posts/{id})
- Add Open Graph and Twitter Card meta tags for social sharing
- Add favicon SVG
- Update all templates with proper meta tags and URLs
- Add error handlers registration in main.py
- Add flash middleware for request handling
- Install itsdangerous dependency
This commit is contained in:
111
app/presentation/templates/pages/error.html
Normal file
111
app/presentation/templates/pages/error.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ error_code }} - {{ error_title }}{% endblock %}
|
||||
{% block meta_description %}{{ error_message }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-page" data-testid="error-page">
|
||||
<div class="error-content" data-testid="error-content">
|
||||
<div class="error-icon" data-testid="error-icon">
|
||||
{% if error_code == 404 %}
|
||||
🔍
|
||||
{% elif error_code == 403 %}
|
||||
🚫
|
||||
{% elif error_code == 500 %}
|
||||
⚠️
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1 class="error-code" data-testid="error-code">{{ error_code }}</h1>
|
||||
<h2 class="error-title" data-testid="error-title">{{ error_title }}</h2>
|
||||
<p class="error-message" data-testid="error-message">{{ error_message }}</p>
|
||||
|
||||
<div class="error-actions" data-testid="error-actions">
|
||||
<a href="/" class="btn btn-primary" data-testid="btn-error-home">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M2 8L8 2L14 8M4 6V13H12V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Go Home
|
||||
</a>
|
||||
|
||||
{% if error_code == 403 %}
|
||||
<a href="/auth/login" class="btn" data-testid="btn-error-login">
|
||||
Sign In
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button onclick="window.history.back()" class="btn btn-ghost" data-testid="btn-error-back">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-dark);
|
||||
margin: 1rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--color-text-light);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-code {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Blog - Home{% endblock %}
|
||||
{% block meta_description %}Discover stories, thinking, and expertise from writers on any topic. A modern blog built with FastAPI.{% endblock %}
|
||||
{% block meta_keywords %}blog, articles, posts, writing, fastapi, python{% endblock %}
|
||||
|
||||
{% block og_type %}website{% endblock %}
|
||||
{% block og_title %}Blog - Home{% endblock %}
|
||||
{% block og_description %}Discover stories, thinking, and expertise from writers on any topic.{% endblock %}
|
||||
|
||||
{% block twitter_title %}Blog - Home{% endblock %}
|
||||
{% block twitter_description %}Discover stories, thinking, and expertise from writers on any topic.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header" data-testid="page-header-home">
|
||||
@@ -26,7 +35,7 @@
|
||||
<article class="card post-card" data-testid="post-card-{{ post.id }}">
|
||||
<div class="post-card-header" data-testid="post-card-header-{{ post.id }}">
|
||||
<h2 class="post-card-title" data-testid="post-title-{{ post.id }}">
|
||||
<a href="/posts/{{ post.id }}" data-testid="post-title-link-{{ post.id }}">{{ post.title }}</a>
|
||||
<a href="/web/posts/{{ post.slug.value }}" data-testid="post-title-link-{{ post.id }}">{{ post.title }}</a>
|
||||
</h2>
|
||||
{% if post.published %}
|
||||
<span class="badge badge-success" data-testid="post-status-{{ post.id }}">Published</span>
|
||||
@@ -55,7 +64,7 @@
|
||||
<span class="tag" data-testid="post-tag-{{ post.id }}-{{ loop.index }}">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/posts/{{ post.id }}" class="btn btn-sm" data-testid="btn-read-more-{{ post.id }}">
|
||||
<a href="/web/posts/{{ post.slug.value }}" class="btn btn-sm" data-testid="btn-read-more-{{ post.id }}">
|
||||
Read more
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 0.25rem;">
|
||||
<path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
{% block title %}{{ post.title }} - Blog{% endblock %}
|
||||
{% block meta_description %}{{ post.content.value[:160] }}{% endblock %}
|
||||
{% block meta_keywords %}{{ post.tags|join(', ') }}{% endblock %}
|
||||
{% block meta_author %}{{ post.author_id }}{% endblock %}
|
||||
|
||||
{% block canonical_url %}{{ request.base_url }}web/posts/{{ post.slug.value }}{% endblock %}
|
||||
|
||||
{% block og_type %}article{% endblock %}
|
||||
{% block og_url %}{{ request.base_url }}web/posts/{{ post.slug.value }}{% endblock %}
|
||||
{% block og_title %}{{ post.title }}{% endblock %}
|
||||
{% block og_description %}{{ post.content.value[:160] }}{% endblock %}
|
||||
|
||||
{% block twitter_title %}{{ post.title }}{% endblock %}
|
||||
{% block twitter_description %}{{ post.content.value[:160] }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="post-detail" data-testid="post-detail">
|
||||
@@ -48,7 +60,7 @@
|
||||
{% if can_edit or can_delete %}
|
||||
<div class="flex gap-2" data-testid="post-detail-edit-actions">
|
||||
{% if can_edit %}
|
||||
<a href="/posts/{{ post.id }}/edit" class="btn" data-testid="btn-edit-post">
|
||||
<a href="/web/posts/{{ post.slug.value }}/edit" class="btn" data-testid="btn-edit-post">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M11 2L14 5M2 14L3 10L12 1L15 4L6 13L2 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -56,7 +68,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_delete %}
|
||||
<form action="/posts/{{ post.id }}/delete" method="POST" style="display: inline;" data-testid="form-delete-post">
|
||||
<form action="/web/posts/{{ post.slug.value }}/delete" method="POST" style="display: inline;" data-testid="form-delete-post">
|
||||
<button type="submit" class="btn btn-danger" data-testid="btn-delete-post" onclick="return confirm('Are you sure you want to delete this post?');">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M2 4h12M6 4V2a2 2 0 012-2h0a2 2 0 012 2v2m3 0v10a2 2 0 01-2 2H5a2 2 0 01-2-2V4h9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="{% if is_edit %}/posts/{{ post.id }}/edit{% else %}/posts/new{% endif %}"
|
||||
<form
|
||||
method="POST"
|
||||
action="{% if is_edit %}/web/posts/{{ post.slug.value }}/edit{% else %}/web/posts/new{% endif %}"
|
||||
class="card"
|
||||
data-testid="form-post"
|
||||
>
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<div class="card-footer" data-testid="form-post-footer">
|
||||
<div class="flex justify-between items-center" data-testid="form-actions">
|
||||
<a href="{% if is_edit %}/posts/{{ post.id }}{% else %}/{% endif %}" class="btn" data-testid="btn-cancel">
|
||||
<a href="{% if is_edit %}/web/posts/{{ post.slug.value }}{% else %}/web/{% endif %}" class="btn" data-testid="btn-cancel">
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user