From af94c99fd2501bc35fa06f6e9b94ea325bfc8810 Mon Sep 17 00:00:00 2001 From: Michilis Date: Thu, 12 Feb 2026 03:51:00 +0000 Subject: [PATCH] feat: auto-update sitemap when events are added/updated/removed - Use tag-based cache for sitemap event list (events-sitemap) - Add POST /api/revalidate endpoint (secret-protected) to trigger revalidation - Backend calls revalidation after event create/update/delete - Add REVALIDATE_SECRET to .env.example (frontend + backend) Co-authored-by: Cursor --- backend/.env.example | 4 +++ backend/src/routes/events.ts | 31 ++++++++++++++++++++++++ frontend/.env.example | 4 +++ frontend/src/app/api/revalidate/route.ts | 27 +++++++++++++++++++++ frontend/src/app/sitemap.ts | 2 +- 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/api/revalidate/route.ts diff --git a/backend/.env.example b/backend/.env.example index 614ac56..19dfe4e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,6 +21,10 @@ PORT=3001 API_URL=http://localhost:3001 FRONTEND_URL=http://localhost:3002 +# Revalidation secret (shared with frontend for on-demand cache revalidation) +# Must match the REVALIDATE_SECRET in frontend/.env +REVALIDATE_SECRET=change-me-to-a-random-secret + # Payment Providers (optional) STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 3eb64fc..ce5b396 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -15,6 +15,28 @@ interface UserContext { const eventsRouter = new Hono<{ Variables: { user: UserContext } }>(); +// Trigger frontend sitemap revalidation (fire-and-forget) +function revalidateSitemap() { + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3002'; + const secret = process.env.REVALIDATE_SECRET; + if (!secret) { + console.warn('REVALIDATE_SECRET not set, skipping sitemap revalidation'); + return; + } + fetch(`${frontendUrl}/api/revalidate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ secret, tag: 'events-sitemap' }), + }) + .then((res) => { + if (!res.ok) console.error('Sitemap revalidation failed:', res.status); + else console.log('Sitemap revalidation triggered'); + }) + .catch((err) => { + console.error('Sitemap revalidation error:', err.message); + }); +} + // Helper to normalize event data for API response // PostgreSQL decimal returns strings, booleans are stored as integers function normalizeEvent(event: any) { @@ -337,6 +359,9 @@ eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', c await (db as any).insert(events).values(newEvent); + // Revalidate sitemap when a new event is created + revalidateSitemap(); + // Return normalized event data return c.json({ event: normalizeEvent(newEvent) }, 201); }); @@ -373,6 +398,9 @@ eventsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json', (db as any).select().from(events).where(eq((events as any).id, id)) ); + // Revalidate sitemap when an event is updated (status/dates may have changed) + revalidateSitemap(); + return c.json({ event: normalizeEvent(updated) }); }); @@ -429,6 +457,9 @@ eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => { // Finally delete the event await (db as any).delete(events).where(eq((events as any).id, id)); + // Revalidate sitemap when an event is deleted + revalidateSitemap(); + return c.json({ message: 'Event deleted successfully' }); }); diff --git a/frontend/.env.example b/frontend/.env.example index 1e240b6..73dc506 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -21,6 +21,10 @@ NEXT_PUBLIC_EMAIL=hola@spanglish.com.py NEXT_PUBLIC_TELEGRAM=spanglish_py NEXT_PUBLIC_TIKTOK=spanglishsocialpy +# Revalidation secret (shared between frontend and backend for on-demand cache revalidation) +# Must match the REVALIDATE_SECRET in backend/.env +REVALIDATE_SECRET=change-me-to-a-random-secret + # Plausible Analytics (optional - leave empty to disable tracking) NEXT_PUBLIC_PLAUSIBLE_URL=https://analytics.azzamo.net NEXT_PUBLIC_PLAUSIBLE_DOMAIN=spanglishcommunity.com diff --git a/frontend/src/app/api/revalidate/route.ts b/frontend/src/app/api/revalidate/route.ts new file mode 100644 index 0000000..42ffdd3 --- /dev/null +++ b/frontend/src/app/api/revalidate/route.ts @@ -0,0 +1,27 @@ +import { revalidateTag } from 'next/cache'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { secret, tag } = body; + + // Validate the revalidation secret + const revalidateSecret = process.env.REVALIDATE_SECRET; + if (!revalidateSecret || secret !== revalidateSecret) { + return NextResponse.json({ error: 'Invalid secret' }, { status: 401 }); + } + + // Validate tag + const allowedTags = ['events-sitemap']; + if (!tag || !allowedTags.includes(tag)) { + return NextResponse.json({ error: 'Invalid tag' }, { status: 400 }); + } + + revalidateTag(tag); + + return NextResponse.json({ revalidated: true, tag, now: Date.now() }); + } catch { + return NextResponse.json({ error: 'Failed to revalidate' }, { status: 500 }); + } +} diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index 72c6a33..d6e5b49 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -12,7 +12,7 @@ interface Event { async function getPublishedEvents(): Promise { try { const response = await fetch(`${apiUrl}/api/events?status=published`, { - next: { revalidate: 3600 }, // Cache for 1 hour + next: { tags: ['events-sitemap'] }, }); if (!response.ok) return []; const data = await response.json();