Browse Source

Caching and such

imwald
Nuša Pukšič 4 months ago
parent
commit
598bdc6f3a
  1. 321
      assets/controllers/service-worker_controller.js
  2. 16
      config/packages/asset_caching.yaml
  3. 2
      config/packages/framework.yaml
  4. 1
      config/services.yaml
  5. 18
      public/robots.txt
  6. 306
      public/service-worker.js
  7. 26
      src/Controller/ArticleController.php
  8. 6
      src/Controller/DefaultController.php
  9. 24
      src/Controller/StaticController.php
  10. 154
      templates/admin/cache.html.twig
  11. 2
      templates/admin/transactions.html.twig
  12. 4
      templates/components/Organisms/ZineList.html.twig
  13. 0
      templates/pages/journals.html.twig

321
assets/controllers/service-worker_controller.js

@ -1,11 +1,326 @@ @@ -1,11 +1,326 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['status']
static values = {
staticUrls: Array,
cacheName: String
}
connect() {
// Check if we've shown an update prompt recently (within 1 hour)
const lastPrompt = localStorage.getItem('sw-update-prompt-shown');
const oneHourAgo = Date.now() - (60 * 60 * 1000);
if (lastPrompt && parseInt(lastPrompt) > oneHourAgo) {
this.updatePromptShown = true;
} else {
this.updatePromptShown = false;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('SW registered:', reg))
.catch(err => console.error('SW failed:', err));
this.loadStaticRoutes().then(() => {
this.registerServiceWorker();
this.setupServiceWorkerEventListeners();
});
} else {
this.updateStatus('Service Worker not supported');
}
}
async loadStaticRoutes() {
try {
const response = await fetch('/api/static-routes');
if (response.ok) {
const data = await response.json();
this.staticUrlsValue = data.routes;
this.cacheNameValue = data.cacheName;
console.log('Loaded static routes for controller:', this.staticUrlsValue);
} else {
console.warn('Failed to load static routes from API');
}
} catch (error) {
console.error('Error loading static routes:', error);
}
}
async registerServiceWorker() {
try {
const registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('SW registered:', registration);
// Check if there's actually a waiting service worker with new content
if (registration.waiting && !this.updatePromptShown) {
this.showUpdateAvailable(registration);
}
// Listen for service worker updates, but be more selective
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
// Only show update if:
// 1. The worker is installed
// 2. We have an active controller (not first install)
// 3. Haven't shown prompt recently
if (newWorker.state === 'installed' &&
navigator.serviceWorker.controller &&
!this.updatePromptShown) {
// Double-check we haven't prompted recently
const lastPrompt = localStorage.getItem('sw-update-prompt-shown');
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
if (!lastPrompt || parseInt(lastPrompt) < thirtyMinutesAgo) {
// Add a delay to avoid immediate prompts during development
setTimeout(() => {
if (!this.updatePromptShown && registration.waiting) {
this.showUpdateAvailable(registration);
}
}, 3000);
}
}
});
});
this.updateStatus('Service Worker registered successfully');
await this.checkCacheStatus();
} catch (error) {
console.error('SW failed:', error);
this.updateStatus('Service Worker registration failed');
}
}
setupServiceWorkerEventListeners() {
// Listen for messages from the service worker
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data && event.data.type) {
switch (event.data.type) {
case 'CACHE_UPDATED':
this.updateStatus('Cache updated successfully');
break;
case 'CACHE_ERROR':
this.updateStatus('Cache update failed');
break;
}
}
});
}
async checkCacheStatus() {
if ('caches' in window) {
try {
const cacheNames = await caches.keys();
const cacheName = this.cacheNameValue || 'newsroom-static';
const hasStaticCache = cacheNames.some(name => name.includes(cacheName.split('-')[0] + '-' + cacheName.split('-')[1]));
if (hasStaticCache) {
const cache = await caches.open(cacheName);
const cachedRequests = await cache.keys();
this.updateStatus(`${cachedRequests.length} static pages cached`);
} else {
this.updateStatus('Static pages not yet cached');
}
} catch (error) {
console.error('Cache status check failed:', error);
}
}
}
showUpdateAvailable(registration) {
// Prevent multiple prompts
if (this.updatePromptShown) {
return;
}
this.updatePromptShown = true;
// Store the timestamp to avoid showing again too soon
localStorage.setItem('sw-update-prompt-shown', Date.now().toString());
// Use a more user-friendly notification
const shouldUpdate = confirm('A new version of the app is available. Would you like to update now?');
if (shouldUpdate) {
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
registration.waiting.addEventListener('statechange', (e) => {
if (e.target.state === 'activated') {
window.location.reload();
}
});
}
} else {
// If user declines, don't ask again for 2 hours
localStorage.setItem('sw-update-prompt-shown', (Date.now() + (2 * 60 * 60 * 1000)).toString());
// Reset the flag after 2 hours
setTimeout(() => {
this.updatePromptShown = false;
}, 2 * 60 * 60 * 1000);
}
}
async clearCache() {
try {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
this.updateStatus('All caches cleared');
console.log('All caches cleared');
} catch (error) {
console.error('Failed to clear caches:', error);
this.updateStatus('Failed to clear caches');
}
}
async refreshCache() {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'REFRESH_CACHE'
});
this.updateStatus('Cache refresh requested');
}
}
async getCacheStatus() {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
return new Promise((resolve) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.type === 'CACHE_STATUS') {
resolve(event.data.status);
} else {
resolve(null);
}
};
navigator.serviceWorker.controller.postMessage(
{ type: 'GET_CACHE_STATUS' },
[messageChannel.port2]
);
});
}
return null;
}
async displayCacheInfo() {
const status = await this.getCacheStatus();
if (status) {
console.log('Cache Status:', status);
let message = 'Cache Status:\n';
for (const [cacheName, count] of Object.entries(status)) {
message += `${cacheName}: ${count} items\n`;
}
this.updateStatus(message.trim());
}
}
async preloadCriticalAssets() {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
// Get the manifest to find critical assets
try {
const manifestResponse = await fetch('/assets/manifest.json');
if (manifestResponse.ok) {
const manifest = await manifestResponse.json();
// Critical assets that should be preloaded
const criticalAssets = [
'app.js',
'bootstrap.js',
'styles/app.css',
'styles/theme.css',
'styles/layout.css',
'styles/fonts.css'
];
const assetsToPreload = criticalAssets
.map(asset => manifest[asset])
.filter(Boolean);
// Trigger preloading by making requests
const preloadPromises = assetsToPreload.map(assetUrl =>
fetch(assetUrl, { mode: 'no-cors' }).catch(() => {
console.warn('Failed to preload:', assetUrl);
})
);
await Promise.all(preloadPromises);
this.updateStatus(`Preloaded ${assetsToPreload.length} critical assets`);
console.log('Preloaded assets:', assetsToPreload);
}
} catch (error) {
console.error('Failed to preload critical assets:', error);
}
}
}
// Enhanced cache management methods
async clearAssetsCache() {
if ('caches' in window) {
try {
const deleted = await caches.delete('newsroom-assets-v1');
if (deleted) {
this.updateStatus('Assets cache cleared');
console.log('Assets cache cleared');
} else {
this.updateStatus('Assets cache not found');
}
} catch (error) {
console.error('Failed to clear assets cache:', error);
this.updateStatus('Failed to clear assets cache');
}
}
}
async clearStaticCache() {
if ('caches' in window) {
try {
const deleted = await caches.delete('newsroom-static-v1');
if (deleted) {
this.updateStatus('Static cache cleared');
console.log('Static cache cleared');
} else {
this.updateStatus('Static cache not found');
}
} catch (error) {
console.error('Failed to clear static cache:', error);
this.updateStatus('Failed to clear static cache');
}
}
}
updateStatus(message) {
if (this.hasStatusTarget) {
this.statusTarget.textContent = message;
console.log('SW Status:', message);
}
}
// Action methods that can be called from the template
clearCacheAction() {
this.clearCache();
}
refreshCacheAction() {
this.refreshCache();
}
// Enhanced action methods
clearAssetsCacheAction() {
this.clearAssetsCache();
}
clearStaticCacheAction() {
this.clearStaticCache();
}
displayCacheInfoAction() {
this.displayCacheInfo();
}
preloadCriticalAssetsAction() {
this.preloadCriticalAssets();
}
}

16
config/packages/asset_caching.yaml

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
# Asset caching configuration for better browser caching
framework:
assets:
# AssetMapper already handles versioning via manifest.json
# No need to set version_strategy as it conflicts with json_manifest_path
json_manifest_path: '%kernel.project_dir%/public/assets/manifest.json'
# Configure HTTP cache headers for static assets
when@prod:
framework:
http_cache:
# Cache static assets for longer periods
default_ttl: 3600 # 1 hour default
private_headers: ['authorization', 'cookie']
allow_reload: false
allow_revalidate: false

2
config/packages/framework.yaml

@ -8,7 +8,7 @@ framework: @@ -8,7 +8,7 @@ framework:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
cookie_secure: auto
cookie_samesite: lax
cookie_lifetime: 0 # integer, lifetime in seconds, 0 means 'valid for the length of the browser session'
cookie_lifetime: 2678400 # integer, lifetime in seconds, 0 means 'valid for the length of the browser session'
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers: ['forwarded', 'x-forwarded-for', 'x-forwarded-proto']
# trusted_proxies: '%env(TRUSTED_PROXIES)%'

1
config/services.yaml

@ -5,7 +5,6 @@ @@ -5,7 +5,6 @@
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
encryption_key: '%env(APP_ENCRYPTION_KEY)%'
nsec: '%env(APP_NSEC)%'
services:
# default configuration for services in *this* file

18
public/robots.txt

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
# Robots.txt
# Disallow all by default
User-agent: *
Disallow: /
# Allow only specific public content
Allow: /assets/
Allow: /icons/
Allow: /$
Allow: /unfold
Allow: /about
Allow: /mag/
Allow: /p/
Allow: /list/
# Crawl delay
Crawl-delay: 1

306
public/service-worker.js

@ -1,11 +1,309 @@ @@ -1,11 +1,309 @@
// Define cache names and versions
const CACHE_NAME = 'newsroom-v1';
const STATIC_CACHE = 'newsroom-static-v1';
const ASSETS_CACHE = 'newsroom-assets-v1';
const RUNTIME_CACHE = 'newsroom-runtime-v1';
// Assets to cache immediately on install
const PRECACHE_ASSETS = [
'/',
'/assets/app.js',
'/assets/bootstrap.js',
'/assets/styles/app.css',
'/assets/styles/theme.css',
'/assets/styles/layout.css',
'/assets/styles/fonts.css',
'/assets/icons/favicon.ico',
'/assets/icons/web-app-manifest-192x192.png',
'/assets/icons/web-app-manifest-512x512.png'
];
// Define what should be cached with different strategies
const CACHE_STRATEGIES = {
// CSS files - cache first (they change infrequently)
css: {
pattern: /\.css$/,
strategy: 'cacheFirst',
cacheName: ASSETS_CACHE,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
},
// JS files - cache first with network fallback
js: {
pattern: /\.js$/,
strategy: 'cacheFirst',
cacheName: ASSETS_CACHE,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
},
// Fonts - cache first (rarely change)
fonts: {
pattern: /\.(woff2?|ttf|eot)$/,
strategy: 'cacheFirst',
cacheName: ASSETS_CACHE,
maxAge: 365 * 24 * 60 * 60 * 1000 // 1 year
},
// Images and icons - cache first
images: {
pattern: /\.(png|jpg|jpeg|gif|svg|ico)$/,
strategy: 'cacheFirst',
cacheName: ASSETS_CACHE,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
},
// Static pages - network first with cache fallback
pages: {
pattern: /^https?.*\/(about|roadmap|tos|landing|unfold)$/,
strategy: 'networkFirst',
cacheName: STATIC_CACHE,
maxAge: 24 * 60 * 60 * 1000 // 1 day
},
// API calls - network first
api: {
pattern: /\/api\//,
strategy: 'networkFirst',
cacheName: RUNTIME_CACHE,
maxAge: 5 * 60 * 1000 // 5 minutes
}
};
self.addEventListener('install', async (event) => {
// Activate the service worker immediately after install
console.log('Service Worker installing...');
event.waitUntil(
(async () => {
try {
// Cache core assets immediately
const cache = await caches.open(ASSETS_CACHE);
console.log('Precaching core assets...');
// Get the current asset manifest to cache the actual versioned files
const manifestResponse = await fetch('/assets/manifest.json');
if (manifestResponse.ok) {
const manifest = await manifestResponse.json();
const versionedAssets = PRECACHE_ASSETS.map(asset => {
const logicalPath = asset.replace('/assets/', '');
return manifest[logicalPath] || asset;
});
await cache.addAll(versionedAssets);
console.log('Precached assets:', versionedAssets);
} else {
// Fallback to original assets if manifest not available
await cache.addAll(PRECACHE_ASSETS);
}
// Activate immediately
await self.skipWaiting();
} catch (error) {
console.error('Precaching failed:', error);
}
})()
);
});
self.addEventListener('activate', (event) => {
// Take control of all clients immediately
event.waitUntil(self.clients.claim());
console.log('Service Worker activating...');
event.waitUntil(
(async () => {
// Clean up old caches
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
name.startsWith('newsroom-') &&
![CACHE_NAME, STATIC_CACHE, ASSETS_CACHE, RUNTIME_CACHE].includes(name)
);
await Promise.all(oldCaches.map(name => caches.delete(name)));
console.log('Cleaned up old caches:', oldCaches);
// Take control of all clients
await self.clients.claim();
})()
);
});
self.addEventListener('fetch', (event) => {
const request = event.request;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') return;
// Skip chrome-extension and other non-http requests
if (!url.protocol.startsWith('http')) return;
// Find matching cache strategy
const strategy = findCacheStrategy(request.url);
if (strategy) {
event.respondWith(handleRequest(request, strategy));
}
});
// No fetch or cache handlers — fully fall-through
self.addEventListener('message', (event) => {
const { type, data } = event.data || {};
switch (type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'REFRESH_CACHE':
handleCacheRefresh();
break;
case 'GET_CACHE_STATUS':
handleCacheStatus(event);
break;
}
});
function findCacheStrategy(url) {
for (const [name, config] of Object.entries(CACHE_STRATEGIES)) {
if (config.pattern.test(url)) {
return config;
}
}
return null;
}
async function handleRequest(request, strategy) {
const cache = await caches.open(strategy.cacheName);
switch (strategy.strategy) {
case 'cacheFirst':
return cacheFirst(request, cache, strategy);
case 'networkFirst':
return networkFirst(request, cache, strategy);
case 'staleWhileRevalidate':
return staleWhileRevalidate(request, cache, strategy);
default:
return fetch(request);
}
}
async function cacheFirst(request, cache, strategy) {
try {
// Check cache first
const cachedResponse = await cache.match(request);
if (cachedResponse && !isExpired(cachedResponse, strategy.maxAge)) {
return cachedResponse;
}
// Fetch from network
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Clone and cache the response
const responseToCache = networkResponse.clone();
await cache.put(request, responseToCache);
}
return networkResponse;
} catch (error) {
console.error('Cache first strategy failed:', error);
// Return cached version even if expired as fallback
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
async function networkFirst(request, cache, strategy) {
try {
// Try network first
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful responses
const responseToCache = networkResponse.clone();
await cache.put(request, responseToCache);
}
return networkResponse;
} catch (error) {
console.log('Network failed, trying cache:', error.message);
// Fallback to cache
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
async function staleWhileRevalidate(request, cache, strategy) {
const cachedResponse = await cache.match(request);
// Always fetch in background to update cache
const fetchPromise = fetch(request).then(networkResponse => {
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
}).catch(() => {
// Ignore background fetch failures
});
// Return cached version immediately if available
if (cachedResponse && !isExpired(cachedResponse, strategy.maxAge)) {
return cachedResponse;
}
// Wait for network if no cache or expired
return fetchPromise;
}
function isExpired(response, maxAge) {
if (!maxAge) return false;
const dateHeader = response.headers.get('date');
if (!dateHeader) return false;
const responseTime = new Date(dateHeader).getTime();
return (Date.now() - responseTime) > maxAge;
}
async function handleCacheRefresh() {
try {
// Clear all caches
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
// Reinstall with fresh cache
const cache = await caches.open(ASSETS_CACHE);
await cache.addAll(PRECACHE_ASSETS);
// Notify all clients
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({ type: 'CACHE_UPDATED' });
});
console.log('Cache refreshed successfully');
} catch (error) {
console.error('Cache refresh failed:', error);
// Notify clients of error
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({ type: 'CACHE_ERROR', error: error.message });
});
}
}
async function handleCacheStatus(event) {
try {
const cacheNames = await caches.keys();
const status = {};
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
status[cacheName] = keys.length;
}
event.ports[0].postMessage({ type: 'CACHE_STATUS', status });
} catch (error) {
event.ports[0].postMessage({ type: 'CACHE_STATUS_ERROR', error: error.message });
}
}

26
src/Controller/ArticleController.php

@ -324,32 +324,6 @@ class ArticleController extends AbstractController @@ -324,32 +324,6 @@ class ArticleController extends AbstractController
}
}
private function verifyNostrSignature(array $event): bool
{
try {
// Reconstruct the event ID
$serializedEvent = json_encode([
0,
$event['pubkey'],
$event['created_at'],
$event['kind'],
$event['tags'],
$event['content']
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$eventId = hash('sha256', $serializedEvent);
// Verify the event ID matches
if ($eventId !== $event['id']) {
return false;
}
return (new SchnorrSignature())->verify($event['pubkey'], $event['sig'], $event['id']);
} catch (\Exception $e) {
return false;
}
}
private function extractArticleDataFromEvent(array $event, array $formData): array
{
$data = [

6
src/Controller/DefaultController.php

@ -24,15 +24,9 @@ use Symfony\Component\HttpFoundation\Response; @@ -24,15 +24,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\ItemInterface;
class DefaultController extends AbstractController
{
public function __construct(
private readonly FinderInterface $finder,
private readonly CacheInterface $redisCache)
{
}
/**
* @throws Exception

24
src/Controller/StaticController.php

@ -5,6 +5,7 @@ declare(strict_types=1); @@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@ -52,9 +53,26 @@ class StaticController extends AbstractController @@ -52,9 +53,26 @@ class StaticController extends AbstractController
return $this->render('static/unfold.html.twig');
}
#[Route('/journals', name: 'journals_index')]
public function journalsIndex(): Response
#[Route('/api/static-routes', name: 'api_static_routes', methods: ['GET'])]
public function getStaticRoutes(): JsonResponse
{
return $this->render('pages/journals.html.twig');
$staticRoutes = [
'/about',
'/roadmap',
'/tos',
'/landing',
'/unfold',
];
return new JsonResponse([
'routes' => $staticRoutes,
'cacheName' => 'newsroom-static-v1'
]);
}
#[Route('/admin/cache', name: 'admin_cache', methods: ['GET'])]
public function cacheManagement(): Response
{
return $this->render('admin/cache.html.twig');
}
}

154
templates/admin/cache.html.twig

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
{% extends 'base.html.twig' %}
{% block title %}Cache Management - {{ parent() }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.cache-management {
max-width: 800px;
margin: 2rem auto;
padding: 1rem;
}
.cache-status {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
font-family: monospace;
white-space: pre-wrap;
}
.cache-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.cache-action-btn {
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
background: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.2s;
}
.cache-action-btn:hover {
background: #0056b3;
}
.cache-action-btn.danger {
background: #dc3545;
}
.cache-action-btn.danger:hover {
background: #c82333;
}
.cache-info {
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
}
</style>
{% endblock %}
{% block layout %}
<div class="cache-management" data-controller="service-worker">
<h1>Cache Management</h1>
<div class="cache-info">
<h3>Caching Strategy Overview</h3>
<p>Your newsroom application uses a multi-layered caching strategy:</p>
<ul>
<li><strong>Service Worker Cache</strong> - Caches JS, CSS, fonts, and images for offline access</li>
<li><strong>Browser Cache</strong> - HTTP cache headers for optimal browser caching</li>
<li><strong>Asset Versioning</strong> - Content-hashed filenames for cache busting</li>
</ul>
</div>
<div class="cache-status" data-service-worker-target="status">
Loading cache status...
</div>
<div class="cache-actions">
<button
class="cache-action-btn"
data-action="click->service-worker#displayCacheInfoAction"
>
Show Cache Status
</button>
<button
class="cache-action-btn"
data-action="click->service-worker#preloadCriticalAssetsAction"
>
Preload Critical Assets
</button>
<button
class="cache-action-btn"
data-action="click->service-worker#refreshCacheAction"
>
Refresh All Caches
</button>
<button
class="cache-action-btn"
data-action="click->service-worker#clearAssetsCacheAction"
>
Clear Assets Cache
</button>
<button
class="cache-action-btn"
data-action="click->service-worker#clearStaticCacheAction"
>
Clear Static Cache
</button>
<button
class="cache-action-btn danger"
data-action="click->service-worker#clearCacheAction"
>
Clear All Caches
</button>
</div>
<div class="cache-info">
<h3>Cache Types Explained</h3>
<dl>
<dt><strong>Assets Cache (newsroom-assets-v1)</strong></dt>
<dd>Stores JS files, CSS files, fonts, and images. Uses "cache-first" strategy for fast loading.</dd>
<dt><strong>Static Cache (newsroom-static-v1)</strong></dt>
<dd>Stores static pages like About, Roadmap, etc. Uses "network-first" strategy for fresh content.</dd>
<dt><strong>Runtime Cache (newsroom-runtime-v1)</strong></dt>
<dd>Stores API responses and dynamic content with short expiration times.</dd>
</dl>
</div>
<div class="cache-info">
<h3>Asset Caching Details</h3>
<p>The following asset types are automatically cached:</p>
<ul>
<li><strong>JavaScript files (.js)</strong> - Cached for 30 days</li>
<li><strong>CSS files (.css)</strong> - Cached for 30 days</li>
<li><strong>Font files (.woff2, .woff, .ttf)</strong> - Cached for 1 year</li>
<li><strong>Images (.png, .jpg, .svg, .ico)</strong> - Cached for 30 days</li>
<li><strong>Static pages</strong> - Cached for 1 day with network-first strategy</li>
</ul>
</div>
<div class="cache-info">
<h3>Performance Benefits</h3>
<ul>
<li>Faster page loads after first visit</li>
<li>Reduced bandwidth usage</li>
<li>Better offline experience</li>
<li>Automatic cache invalidation when assets change</li>
</ul>
</div>
</div>
{% endblock %}

2
templates/admin/transactions.html.twig

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
{% for tx in transactions %}
<tr>
<td>{{ tx.id }}</td>
<td><span title="{{ tx.npub }}">{{ tx.npub|shortenNpub }}</span></td>
<td><span title="{{ tx.npub }}"><twig:Molecules:UserFromNpub :ident="tx.npub" /></span></td>
<td>{{ tx.type }}</td>
<td>{{ tx.amount }}</td>
<td>{{ tx.reason ?: '—' }}</td>

4
templates/components/Organisms/ZineList.html.twig

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
{% for item in nzines %}
<div class="card">
<div class="card-body text-center">
<h3 class="card-title">
<h2 class="card-title">
<a href="{{ path('magazine-index', {mag: item.slug}) }}">
{% if item and item.title %}
{{ item.title }}
@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
Untitled Magazine
{% endif %}
</a>
</h3>
</h2>
{% if item and item.summary %}
<p class="lede">{{ item.summary }}</p>
{% endif %}

0
templates/pages/journals.html.twig

Loading…
Cancel
Save