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:
2026-05-02 16:23:57 +03:00
parent 4eee261107
commit b1878e470f
13 changed files with 747 additions and 42 deletions

View File

@@ -9,7 +9,7 @@ from typing import Any
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from app.infrastructure.auth import TokenInfo
@@ -22,6 +22,7 @@ from app.presentation.web.deps import (
can_see_draft,
get_user_role,
)
from app.presentation.web.flash import flash
router = APIRouter(prefix="/web", tags=["web"])
templates = Jinja2Templates(directory="app/presentation/templates")
@@ -259,11 +260,11 @@ async def new_post_form(
)
@router.post("/posts/new", response_class=HTMLResponse)
@router.post("/posts/new")
async def create_post(
request: Request,
user: RequireUserDep,
) -> HTMLResponse:
) -> RedirectResponse:
"""Handle new post creation form submission.
Args:
@@ -271,29 +272,17 @@ async def create_post(
user: Current user (required).
Returns:
HTMLResponse redirecting to the home page.
RedirectResponse to the new post or home page.
"""
context = get_base_context(user)
visible_posts = filter_visible_posts(MOCK_POSTS, user)
return templates.TemplateResponse(
request,
"pages/index.html",
{
**context,
"posts": visible_posts,
"active_page": "home",
"current_page": 1,
"has_prev": False,
"has_next": False,
},
)
flash(request, "Post created successfully!", "success")
response = RedirectResponse(url="/web/", status_code=303)
return response
@router.get("/posts/{post_id}", response_class=HTMLResponse)
@router.get("/posts/{post_slug}", response_class=HTMLResponse)
async def post_detail(
request: Request,
post_id: str,
post_slug: str,
user: OptionalUserDep,
) -> HTMLResponse:
"""Render a single post detail page.
@@ -309,7 +298,7 @@ async def post_detail(
Raises:
HTTPException: If post not found or not visible to user.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None)
post = next((p for p in MOCK_POSTS if p.slug.value == post_slug), None)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
@@ -332,10 +321,10 @@ async def post_detail(
)
@router.get("/posts/{post_id}/edit", response_class=HTMLResponse)
@router.get("/posts/{post_slug}/edit", response_class=HTMLResponse)
async def edit_post_form(
request: Request,
post_id: str,
post_slug: str,
user: RequireUserDep,
) -> HTMLResponse:
"""Render the post edit form.
@@ -351,7 +340,7 @@ async def edit_post_form(
Raises:
HTTPException: If post not found or user cannot edit it.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None)
post = next((p for p in MOCK_POSTS if p.slug.value == post_slug), None)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
@@ -373,10 +362,10 @@ async def edit_post_form(
)
@router.post("/posts/{post_id}/edit", response_class=HTMLResponse)
@router.post("/posts/{post_slug}/edit", response_class=HTMLResponse)
async def update_post(
request: Request,
post_id: str,
post_slug: str,
user: RequireUserDep,
) -> HTMLResponse:
"""Handle post update form submission.
@@ -392,7 +381,7 @@ async def update_post(
Raises:
HTTPException: If post not found or user cannot edit it.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None)
post = next((p for p in MOCK_POSTS if p.slug.value == post_slug), None)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
@@ -415,10 +404,10 @@ async def update_post(
)
@router.post("/posts/{post_id}/delete", response_class=HTMLResponse)
@router.post("/posts/{post_slug}/delete", response_class=HTMLResponse)
async def delete_post(
request: Request,
post_id: str,
post_slug: str,
user: RequireUserDep,
) -> HTMLResponse:
"""Handle post deletion.
@@ -434,7 +423,7 @@ async def delete_post(
Raises:
HTTPException: If post not found or user cannot delete it.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None)
post = next((p for p in MOCK_POSTS if p.slug.value == post_slug), None)
if not post:
raise HTTPException(status_code=404, detail="Post not found")