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 <cursoragent@cursor.com>
This commit is contained in:
@@ -21,6 +21,10 @@ PORT=3001
|
|||||||
API_URL=http://localhost:3001
|
API_URL=http://localhost:3001
|
||||||
FRONTEND_URL=http://localhost:3002
|
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)
|
# Payment Providers (optional)
|
||||||
STRIPE_SECRET_KEY=
|
STRIPE_SECRET_KEY=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|||||||
@@ -15,6 +15,28 @@ interface UserContext {
|
|||||||
|
|
||||||
const eventsRouter = new Hono<{ Variables: { user: 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
|
// Helper to normalize event data for API response
|
||||||
// PostgreSQL decimal returns strings, booleans are stored as integers
|
// PostgreSQL decimal returns strings, booleans are stored as integers
|
||||||
function normalizeEvent(event: any) {
|
function normalizeEvent(event: any) {
|
||||||
@@ -337,6 +359,9 @@ eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', c
|
|||||||
|
|
||||||
await (db as any).insert(events).values(newEvent);
|
await (db as any).insert(events).values(newEvent);
|
||||||
|
|
||||||
|
// Revalidate sitemap when a new event is created
|
||||||
|
revalidateSitemap();
|
||||||
|
|
||||||
// Return normalized event data
|
// Return normalized event data
|
||||||
return c.json({ event: normalizeEvent(newEvent) }, 201);
|
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))
|
(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) });
|
return c.json({ event: normalizeEvent(updated) });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -429,6 +457,9 @@ eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
|
|||||||
// Finally delete the event
|
// Finally delete the event
|
||||||
await (db as any).delete(events).where(eq((events as any).id, id));
|
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' });
|
return c.json({ message: 'Event deleted successfully' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ NEXT_PUBLIC_EMAIL=hola@spanglish.com.py
|
|||||||
NEXT_PUBLIC_TELEGRAM=spanglish_py
|
NEXT_PUBLIC_TELEGRAM=spanglish_py
|
||||||
NEXT_PUBLIC_TIKTOK=spanglishsocialpy
|
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)
|
# Plausible Analytics (optional - leave empty to disable tracking)
|
||||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://analytics.azzamo.net
|
NEXT_PUBLIC_PLAUSIBLE_URL=https://analytics.azzamo.net
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=spanglishcommunity.com
|
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=spanglishcommunity.com
|
||||||
|
|||||||
27
frontend/src/app/api/revalidate/route.ts
Normal file
27
frontend/src/app/api/revalidate/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ interface Event {
|
|||||||
async function getPublishedEvents(): Promise<Event[]> {
|
async function getPublishedEvents(): Promise<Event[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiUrl}/api/events?status=published`, {
|
const response = await fetch(`${apiUrl}/api/events?status=published`, {
|
||||||
next: { revalidate: 3600 }, // Cache for 1 hour
|
next: { tags: ['events-sitemap'] },
|
||||||
});
|
});
|
||||||
if (!response.ok) return [];
|
if (!response.ok) return [];
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user