diff --git a/public/service-worker.js b/public/service-worker.js index b255411..8cbd578 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -48,10 +48,24 @@ const CACHE_STRATEGIES = { cacheName: ASSETS_CACHE, maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days }, - // Static pages - network first with cache fallback + // Nostr event pages - cache first with background update + nostrEvents: { + pattern: /^https?.*\/e\/(note|nevent)1.*/, + strategy: 'staleWhileRevalidate', + cacheName: RUNTIME_CACHE, + maxAge: 10 * 60 * 1000 // 10 minutes + }, + // Nostr articles, profiles - cache first with background update + nostrArticles: { + pattern: /^https?.*\/(article|p)\/*/, + strategy: 'staleWhileRevalidate', + cacheName: RUNTIME_CACHE, + maxAge: 10 * 60 * 1000 // 10 minutes + }, + // Static pages pages: { pattern: /^https?.*\/(about|roadmap|tos|landing|unfold)$/, - strategy: 'networkFirst', + strategy: 'staleWhileRevalidate', cacheName: STATIC_CACHE, maxAge: 24 * 60 * 60 * 1000 // 1 day }, diff --git a/src/Controller/EventController.php b/src/Controller/EventController.php index 7c5aadd..4569804 100644 --- a/src/Controller/EventController.php +++ b/src/Controller/EventController.php @@ -9,6 +9,7 @@ use App\Service\NostrClient; use App\Service\NostrLinkParser; use App\Service\RedisCacheService; use Exception; +use http\Client\Request; use nostriphant\NIP19\Bech32; use nostriphant\NIP19\Data; use nostriphant\NIP19\Data\Note; @@ -25,7 +26,8 @@ class EventController extends AbstractController * @throws Exception */ #[Route('/e/{nevent}', name: 'nevent', requirements: ['nevent' => '^(nevent|note)1.*'])] - public function index($nevent, NostrClient $nostrClient, RedisCacheService $redisCacheService, NostrLinkParser $nostrLinkParser, LoggerInterface $logger): Response + public function index($nevent, \Symfony\Component\HttpFoundation\Request $request, NostrClient $nostrClient, + RedisCacheService $redisCacheService, NostrLinkParser $nostrLinkParser, LoggerInterface $logger): Response { $logger->info('Accessing event page', ['nevent' => $nevent]); @@ -99,12 +101,27 @@ class EventController extends AbstractController } // Render template with the event data and extracted Nostr links - return $this->render('event/index.html.twig', [ + $response = $this->render('event/index.html.twig', [ 'event' => $event, 'author' => $authorMetadata, 'nostrLinks' => $nostrLinks ]); + // Add HTTP caching headers for request-level caching + $response->setPublic(); // Allow public caching (browsers, CDNs) + $response->setMaxAge(300); // Cache for 5 minutes + $response->setSharedMaxAge(300); // Same for shared caches (CDNs) + + // Add ETag for conditional requests + $etag = md5($nevent . ($event->created_at ?? '') . ($event->content ?? '')); + $response->setEtag($etag); + $response->setLastModified(new \DateTime('@' . ($event->created_at ?? time()))); + + // Check if client has current version + $response->isNotModified($request); + + return $response; + } catch (Exception $e) { $logger->error('Error processing event', ['error' => $e->getMessage()]); throw $e; diff --git a/src/Service/RedisCacheService.php b/src/Service/RedisCacheService.php index a0d8e06..a673295 100644 --- a/src/Service/RedisCacheService.php +++ b/src/Service/RedisCacheService.php @@ -398,4 +398,61 @@ readonly class RedisCacheService ]; } } + + /** + * Get a single event by ID with caching + * + * @param string $eventId The event ID + * @param array|null $relays Optional relays to query + * @return object|null The event object or null if not found + */ + public function getEvent(string $eventId, ?array $relays = null): ?object + { + $cacheKey = 'event_' . $eventId . ($relays ? '_' . md5(json_encode($relays)) : ''); + + try { + return $this->redisCache->get($cacheKey, function (ItemInterface $item) use ($eventId, $relays) { + $item->expiresAfter(1800); // 30 minutes cache for events + + try { + $event = $this->nostrClient->getEventById($eventId, $relays); + return $event; + } catch (\Exception $e) { + $this->logger->error('Error getting event.', ['exception' => $e, 'eventId' => $eventId]); + return null; + } + }); + } catch (InvalidArgumentException $e) { + $this->logger->error('Cache error getting event.', ['exception' => $e, 'eventId' => $eventId]); + return null; + } + } + + /** + * Get a parameterized replaceable event (naddr) with caching + * + * @param array $decodedData The decoded naddr data + * @return object|null The event object or null if not found + */ + public function getNaddrEvent(array $decodedData): ?object + { + $cacheKey = 'naddr_' . $decodedData['kind'] . '_' . $decodedData['pubkey'] . '_' . $decodedData['identifier'] . '_' . md5(json_encode($decodedData['relays'] ?? [])); + + try { + return $this->redisCache->get($cacheKey, function (ItemInterface $item) use ($decodedData) { + $item->expiresAfter(1800); // 30 minutes cache for naddr events + + try { + $event = $this->nostrClient->getEventByNaddr($decodedData); + return $event; + } catch (\Exception $e) { + $this->logger->error('Error getting naddr event.', ['exception' => $e, 'decodedData' => $decodedData]); + return null; + } + }); + } catch (InvalidArgumentException $e) { + $this->logger->error('Cache error getting naddr event.', ['exception' => $e, 'decodedData' => $decodedData]); + return null; + } + } } diff --git a/templates/event/index.html.twig b/templates/event/index.html.twig index 1e1d800..f88e906 100644 --- a/templates/event/index.html.twig +++ b/templates/event/index.html.twig @@ -1,6 +1,84 @@ {% extends 'layout.html.twig' %} -{% block title %}Nostr Event{% endblock %} +{% block title %} + {%- if author and author.name is defined -%} + {{ author.name }} - Nostr Event + {%- else -%} + Nostr Event + {%- endif -%} +{% endblock %} + +{% block ogtags %} + {# Set og:type dynamically based on event kind #} + {% set ogType = 'article' %} + {% if event.kind == 21 or event.kind == 22 %} + {% set ogType = 'video.other' %} + {% elseif event.kind == 20 %} + {% set ogType = 'article' %} {# Could use 'image' but 'article' is more widely supported #} + {% endif %} + + + + {# OG Description - use event content or fallback #} + {% set ogDescription = event.content|default('View this Nostr event')|striptags|slice(0, 200) %} + + + {# URL #} + + + {# Image - try to extract from event or use author image #} + {% set ogImage = null %} + + {# For picture events (kind 20), try to get image from url tag #} + {% if event.kind == 20 %} + {% for tag in event.tags %} + {% if tag[0] == 'url' and ogImage is null %} + {% set ogImage = tag[1] %} + {% endif %} + {% endfor %} + {% endif %} + + {# For video events (kind 21/22), try to get thumbnail #} + {% if (event.kind == 21 or event.kind == 22) and ogImage is null %} + {% for tag in event.tags %} + {% if tag[0] == 'thumb' and ogImage is null %} + {% set ogImage = tag[1] %} + {% elseif tag[0] == 'image' and ogImage is null %} + {% set ogImage = tag[1] %} + {% endif %} + {% endfor %} + {% endif %} + + {# Fallback to author image if available #} + {% if ogImage is null and author and author.image is defined %} + {% set ogImage = author.image %} + {% endif %} + + {# Use default icon if no image found #} + {% if ogImage is null %} + {% set ogImage = app.request.schemeAndHttpHost ~ asset('icons/apple-touch-icon.png') %} + {% endif %} + + + + {# Twitter Card tags #} + + + + + + {# Article metadata #} + {% if event.created_at is defined %} + + {% endif %} + + {% if author and author.name is defined %} + + {% endif %} + + {# Site name #} + +{% endblock %} {% block body %}