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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user