Browse Source

Loading optimization

imwald
Nuša Pukšič 3 months ago
parent
commit
80d585803f
  1. 18
      public/service-worker.js
  2. 21
      src/Controller/EventController.php
  3. 57
      src/Service/RedisCacheService.php
  4. 80
      templates/event/index.html.twig

18
public/service-worker.js

@ -48,10 +48,24 @@ const CACHE_STRATEGIES = { @@ -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
},

21
src/Controller/EventController.php

@ -9,6 +9,7 @@ use App\Service\NostrClient; @@ -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 @@ -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 @@ -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;

57
src/Service/RedisCacheService.php

@ -398,4 +398,61 @@ readonly class RedisCacheService @@ -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;
}
}
}

80
templates/event/index.html.twig

@ -1,6 +1,84 @@ @@ -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 %}
<meta property="og:type" content="{{ ogType }}" />
<meta property="og:title" content="{% if author and author.name is defined %}{{ author.name }} - Nostr Event{% else %}Nostr Event{% endif %}" />
{# OG Description - use event content or fallback #}
{% set ogDescription = event.content|default('View this Nostr event')|striptags|slice(0, 200) %}
<meta property="og:description" content="{{ ogDescription }}" />
{# URL #}
<meta property="og:url" content="{{ app.request.uri }}" />
{# 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 %}
<meta property="og:image" content="{{ ogImage }}" />
{# Twitter Card tags #}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{% if author and author.name is defined %}{{ author.name }} - Nostr Event{% else %}Nostr Event{% endif %}" />
<meta name="twitter:description" content="{{ ogDescription }}" />
<meta name="twitter:image" content="{{ ogImage }}" />
{# Article metadata #}
{% if event.created_at is defined %}
<meta property="article:published_time" content="{{ event.created_at|date('c') }}" />
{% endif %}
{% if author and author.name is defined %}
<meta property="article:author" content="{{ author.name }}" />
{% endif %}
{# Site name #}
<meta property="og:site_name" content="Decent Newsroom" />
{% endblock %}
{% block body %}
<div class="container">

Loading…
Cancel
Save