perf: 100% Lighthouse score — contrast, CLS, caching fixes

- Font preload eliminates layout shift (CLS 0)
- Dual accent colors: --accent for filled buttons, --accent-text for text on dark bg
- All WCAG AA contrast ratios met (buttons, links, badges, countdown)
- Language picker: position absolute instead of fixed
- CoinGecko rates proxied with 2min server-side cache (no CORS, no rate limit)
- English as default inline text (no empty→text shift)
This commit is contained in:
Alexander Schmidt
2026-03-25 17:18:41 +01:00
parent cf3c43ff67
commit 787168b248
3 changed files with 48 additions and 15 deletions

View File

@@ -1,9 +1,21 @@
<?php
header('Content-Type: application/json');
header('Cache-Control: public, max-age=60');
// Server-side cache: fetch from CoinGecko at most once per 2 minutes
$cacheFile = __DIR__ . '/../data/rates_cache.json';
$cacheTTL = 120; // seconds
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if ($cached && (time() - ($cached['_time'] ?? 0)) < $cacheTTL) {
unset($cached['_time']);
header('Cache-Control: public, max-age=60');
echo json_encode($cached);
exit;
}
}
$url = 'https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=eur,usd,chf';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
@@ -15,8 +27,27 @@ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response !== false && $httpCode === 200) {
echo $response;
} else {
http_response_code(502);
echo json_encode(['error' => 'Failed to fetch rates']);
$data = json_decode($response, true);
if ($data) {
$data['_time'] = time();
file_put_contents($cacheFile, json_encode($data));
unset($data['_time']);
header('Cache-Control: public, max-age=60');
echo json_encode($data);
exit;
}
}
// On error, serve stale cache if available
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if ($cached) {
unset($cached['_time']);
header('Cache-Control: public, max-age=30');
echo json_encode($cached);
exit;
}
}
http_response_code(502);
echo json_encode(['error' => 'Failed to fetch rates']);

View File

@@ -6,6 +6,7 @@
<title>xmrpay.link — Monero Invoice Generator</title>
<meta name="description" content="Create Monero payment requests in seconds. No account, no backend, no KYC.">
<link rel="icon" id="favicon" href="favicon.svg" type="image/svg+xml">
<link rel="preload" href="fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="style.css">
</head>
<body>

View File

@@ -23,8 +23,9 @@
--border: #333;
--text: #e0e0e0;
--text-muted: #888;
--accent: #ff6600;
--accent-hover: #ff8533;
--accent: #c74a00;
--accent-hover: #a83f00;
--accent-text: #e87830;
--success: #4caf50;
--error: #f44336;
--radius: 8px;
@@ -54,7 +55,7 @@ header {
}
.lang-picker {
position: fixed;
position: absolute;
top: 0.75rem;
right: 0.75rem;
z-index: 50;
@@ -119,7 +120,7 @@ header {
}
.lang-option.active {
color: var(--accent);
color: var(--accent-text);
}
header h1 {
@@ -138,7 +139,7 @@ header h1 a:hover {
}
header h1 span {
color: var(--accent);
color: var(--accent-text);
}
header p {
@@ -590,8 +591,8 @@ textarea {
.btn-new {
margin-top: 0.8rem;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
border: 1px solid var(--accent-text);
color: var(--accent-text);
}
.btn-new:hover {
@@ -628,7 +629,7 @@ footer {
}
footer a {
color: var(--accent);
color: var(--accent-text);
text-decoration: underline;
}
@@ -644,7 +645,7 @@ footer a {
}
.countdown.active {
color: var(--accent);
color: var(--accent-text);
}
@media (max-width: 500px) {