5 Commits

Author SHA1 Message Date
15655e3987 Merge pull request 'dev' (#12) from dev into main
Reviewed-on: #12
2026-02-16 23:11:52 +00:00
Michilis
5263fa6834 Make llms.txt always fetch fresh data from the backend
- Switch from tag-based caching to cache: no-store for all backend fetches
- Add dynamic = force-dynamic to prevent Next.js static caching
- Ensures llms.txt always reflects the current featured event and FAQ data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 23:10:33 +00:00
Michilis
923c86a3b3 Fix FRONTEND_URL pointing to wrong port, breaking cache revalidation
- Update FRONTEND_URL default from localhost:3002 to localhost:3019 (actual frontend port)
- Reorder systemd service so EnvironmentFile loads before Environment overrides

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 22:53:59 +00:00
d8b3864411 Merge pull request 'Fix stale featured event on homepage: revalidate cache when featured event changes' (#11) from dev into main
Reviewed-on: #11
2026-02-16 22:44:19 +00:00
Michilis
4aaffe99c7 Fix stale featured event on homepage: revalidate cache when featured event changes
- Extract revalidateFrontendCache() to backend/src/lib/revalidate.ts
- Call revalidation from site-settings when featuredEventId is set/cleared
- Ensures homepage shows updated featured event after admin changes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 22:42:55 +00:00
6 changed files with 42 additions and 28 deletions

View File

@@ -19,7 +19,7 @@ GOOGLE_CLIENT_ID=
# Server Configuration # Server Configuration
PORT=3001 PORT=3001
API_URL=http://localhost:3001 API_URL=http://localhost:3001
FRONTEND_URL=http://localhost:3002 FRONTEND_URL=http://localhost:3019
# Revalidation secret (shared with frontend for on-demand cache revalidation) # Revalidation secret (shared with frontend for on-demand cache revalidation)
# Must match the REVALIDATE_SECRET in frontend/.env # Must match the REVALIDATE_SECRET in frontend/.env

View File

@@ -0,0 +1,22 @@
// Trigger frontend cache revalidation (fire-and-forget)
// Revalidates both the sitemap and the next-event data (homepage, llms.txt)
export function revalidateFrontendCache() {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3002';
const secret = process.env.REVALIDATE_SECRET;
if (!secret) {
console.warn('REVALIDATE_SECRET not set, skipping frontend revalidation');
return;
}
fetch(`${frontendUrl}/api/revalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret, tag: ['events-sitemap', 'next-event'] }),
})
.then((res) => {
if (!res.ok) console.error('Frontend revalidation failed:', res.status);
else console.log('Frontend revalidation triggered (sitemap + next-event)');
})
.catch((err) => {
console.error('Frontend revalidation error:', err.message);
});
}

View File

@@ -5,6 +5,7 @@ import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, ema
import { eq, desc, and, gte, sql } from 'drizzle-orm'; import { eq, desc, and, gte, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js'; import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, getNow, convertBooleansForDb, toDbDate, calculateAvailableSeats } from '../lib/utils.js'; import { generateId, getNow, convertBooleansForDb, toDbDate, calculateAvailableSeats } from '../lib/utils.js';
import { revalidateFrontendCache } from '../lib/revalidate.js';
interface UserContext { interface UserContext {
id: string; id: string;
@@ -15,29 +16,6 @@ interface UserContext {
const eventsRouter = new Hono<{ Variables: { user: UserContext } }>(); const eventsRouter = new Hono<{ Variables: { user: UserContext } }>();
// Trigger frontend cache revalidation (fire-and-forget)
// Revalidates both the sitemap and the next-event data (homepage, llms.txt)
function revalidateFrontendCache() {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3002';
const secret = process.env.REVALIDATE_SECRET;
if (!secret) {
console.warn('REVALIDATE_SECRET not set, skipping frontend revalidation');
return;
}
fetch(`${frontendUrl}/api/revalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret, tag: ['events-sitemap', 'next-event'] }),
})
.then((res) => {
if (!res.ok) console.error('Frontend revalidation failed:', res.status);
else console.log('Frontend revalidation triggered (sitemap + next-event)');
})
.catch((err) => {
console.error('Frontend 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) {

View File

@@ -5,6 +5,7 @@ import { db, dbGet, siteSettings, events } from '../db/index.js';
import { eq, and, gte } from 'drizzle-orm'; import { eq, and, gte } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js'; import { requireAuth } from '../lib/auth.js';
import { generateId, getNow, toDbBool } from '../lib/utils.js'; import { generateId, getNow, toDbBool } from '../lib/utils.js';
import { revalidateFrontendCache } from '../lib/revalidate.js';
interface UserContext { interface UserContext {
id: string; id: string;
@@ -172,6 +173,11 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
(db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id)) (db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id))
); );
// Revalidate frontend cache if featured event changed
if (data.featuredEventId !== undefined) {
revalidateFrontendCache();
}
return c.json({ settings: updated, message: 'Settings updated successfully' }); return c.json({ settings: updated, message: 'Settings updated successfully' });
}); });
@@ -216,6 +222,9 @@ siteSettingsRouter.put('/featured-event', requireAuth(['admin']), zValidator('js
await (db as any).insert(siteSettings).values(newSettings); await (db as any).insert(siteSettings).values(newSettings);
// Revalidate frontend cache so homepage shows the updated featured event
revalidateFrontendCache();
return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' }); return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' });
} }
@@ -229,6 +238,9 @@ siteSettingsRouter.put('/featured-event', requireAuth(['admin']), zValidator('js
}) })
.where(eq((siteSettings as any).id, existing.id)); .where(eq((siteSettings as any).id, existing.id));
// Revalidate frontend cache so homepage shows the updated featured event
revalidateFrontendCache();
return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' }); return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' });
}); });

View File

@@ -8,9 +8,9 @@ Type=simple
User=spanglish User=spanglish
Group=spanglish Group=spanglish
WorkingDirectory=/home/spanglish/Spanglish/backend WorkingDirectory=/home/spanglish/Spanglish/backend
EnvironmentFile=/home/spanglish/Spanglish/backend/.env
Environment=NODE_ENV=production Environment=NODE_ENV=production
Environment=PORT=3018 Environment=PORT=3018
EnvironmentFile=/home/spanglish/Spanglish/backend/.env
ExecStart=/usr/bin/node dist/index.js ExecStart=/usr/bin/node dist/index.js
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10

View File

@@ -28,7 +28,7 @@ interface LlmsEvent {
async function getNextUpcomingEvent(): Promise<LlmsEvent | null> { async function getNextUpcomingEvent(): Promise<LlmsEvent | null> {
try { try {
const response = await fetch(`${apiUrl}/api/events/next/upcoming`, { const response = await fetch(`${apiUrl}/api/events/next/upcoming`, {
next: { tags: ['next-event'] }, cache: 'no-store',
}); });
if (!response.ok) return null; if (!response.ok) return null;
const data = await response.json(); const data = await response.json();
@@ -41,7 +41,7 @@ async function getNextUpcomingEvent(): Promise<LlmsEvent | null> {
async function getUpcomingEvents(): Promise<LlmsEvent[]> { async function getUpcomingEvents(): Promise<LlmsEvent[]> {
try { try {
const response = await fetch(`${apiUrl}/api/events?status=published&upcoming=true`, { const response = await fetch(`${apiUrl}/api/events?status=published&upcoming=true`, {
next: { tags: ['next-event'] }, cache: 'no-store',
}); });
if (!response.ok) return []; if (!response.ok) return [];
const data = await response.json(); const data = await response.json();
@@ -115,7 +115,7 @@ function getEventStatus(event: LlmsEvent): string {
async function getHomepageFaqs(): Promise<LlmsFaq[]> { async function getHomepageFaqs(): Promise<LlmsFaq[]> {
try { try {
const response = await fetch(`${apiUrl}/api/faq?homepage=true`, { const response = await fetch(`${apiUrl}/api/faq?homepage=true`, {
next: { revalidate: 3600 }, cache: 'no-store',
}); });
if (!response.ok) return []; if (!response.ok) return [];
const data = await response.json(); const data = await response.json();
@@ -128,6 +128,8 @@ async function getHomepageFaqs(): Promise<LlmsFaq[]> {
} }
} }
export const dynamic = 'force-dynamic';
export async function GET() { export async function GET() {
const [nextEvent, upcomingEvents, faqs] = await Promise.all([ const [nextEvent, upcomingEvents, faqs] = await Promise.all([
getNextUpcomingEvent(), getNextUpcomingEvent(),