diff options
| author | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
|---|---|---|
| committer | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
| commit | 898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch) | |
| tree | 85c6ee5ad58b860144551184d4cf86b560c62b91 /partials/recent_static.php | |
| download | www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.tar.gz www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.zip | |
Diffstat (limited to 'partials/recent_static.php')
| -rw-r--r-- | partials/recent_static.php | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/partials/recent_static.php b/partials/recent_static.php new file mode 100644 index 0000000..484bd18 --- /dev/null +++ b/partials/recent_static.php @@ -0,0 +1,195 @@ +<?php +// partials/recent_static.php +// Cheap server-side prerender of recent blog/changelog items so users do +// not stare at "Loading…" while JS is still booting (and so the page is +// useful even when the fetch fails). + +const RECENT_CACHE_TTL = 300; // 5 min + +function _recent_cache_get(string $key, int $ttl, int $invalidate_before = 0) { + $f = sys_get_temp_dir() . '/sillylaird_recent_' . $key . '.json'; + if (!is_file($f)) return null; + $mt = filemtime($f); + // Stale if older than TTL, or older than the upstream source mtime + if ((time() - $mt) >= $ttl) return null; + if ($invalidate_before > 0 && $mt < $invalidate_before) return null; + $raw = @file_get_contents($f); + if ($raw === false) return null; + $data = json_decode($raw, true); + return is_array($data) ? $data : null; +} + +function _recent_changelog_source_mtime(): int { + $latest = 0; + foreach (glob(__DIR__ . '/../changelog/*-changelog.txt') ?: [] as $f) { + $m = @filemtime($f); + if ($m && $m > $latest) $latest = $m; + } + return $latest; +} + +function _recent_cache_put(string $key, array $data): void { + $f = sys_get_temp_dir() . '/sillylaird_recent_' . $key . '.json'; + @file_put_contents($f, json_encode($data), LOCK_EX); +} + +// Last-resort fallback: return whatever is cached regardless of age, so an +// upstream hiccup never blanks the list (stale-on-error). +function _recent_cache_get_stale(string $key): ?array { + $f = sys_get_temp_dir() . '/sillylaird_recent_' . $key . '.json'; + if (!is_file($f)) return null; + $raw = @file_get_contents($f); + if ($raw === false) return null; + $data = json_decode($raw, true); + return is_array($data) ? $data : null; +} + +const BLOG_SQLITE = '/mnt/slab/blog.sillylaird.ca/blog.sqlite'; + +function _recent_fetch(string $url, float $timeout = 1.5): ?string { + $ctx = stream_context_create(['http' => [ + 'timeout' => $timeout, + 'ignore_errors' => true, + 'header' => "User-Agent: sillylaird-static-render/1.0\r\n", + ]]); + $body = @file_get_contents($url, false, $ctx); + return $body === false ? null : $body; +} + +function recent_blog_posts(int $limit = 2): array { + // Invalidate cache when the blog DB changes so new posts show within the + // window without waiting out the full TTL. + $src_mtime = @filemtime(BLOG_SQLITE) ?: 0; + $cached = _recent_cache_get('blog', RECENT_CACHE_TTL, $src_mtime); + if ($cached !== null) return array_slice($cached, 0, $limit); + + // Blog runs on the same box; read its SQLite directly instead of an HTTP + // round-trip to blog.sillylaird.ca (the old fetch blocked ~1.5s on every + // cache miss and stalled when the blog vhost was slow/down). + $posts = null; + if (is_file(BLOG_SQLITE)) { + try { + $db = new PDO('sqlite:' . BLOG_SQLITE, null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_TIMEOUT => 2, + ]); + $stmt = $db->query('SELECT title, path, year, month, day FROM posts ORDER BY id DESC'); + $posts = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Throwable $e) { + $posts = null; + } + } + + if (!is_array($posts)) { + // DB unavailable — serve the last good list rather than blanking. + $stale = _recent_cache_get_stale('blog'); + return $stale !== null ? array_slice($stale, 0, $limit) : []; + } + + _recent_cache_put('blog', $posts); + return array_slice($posts, 0, $limit); +} + +function recent_changelog_entries(int $limit = 2): array { + $cached = _recent_cache_get('changelog', RECENT_CACHE_TTL, _recent_changelog_source_mtime()); + if ($cached !== null) return array_slice($cached, 0, $limit); + + $html = _recent_fetch('https://www.sillylaird.ca/changelog/latest.php'); + if ($html === null) { + $stale = _recent_cache_get_stale('changelog'); + return $stale !== null ? array_slice($stale, 0, $limit) : []; + } + + $entries = []; + $doc = new DOMDocument(); + libxml_use_internal_errors(true); + if (!$doc->loadHTML('<?xml encoding="UTF-8">' . $html)) { + libxml_clear_errors(); + $stale = _recent_cache_get_stale('changelog'); + return $stale !== null ? array_slice($stale, 0, $limit) : []; + } + libxml_clear_errors(); + $xp = new DOMXPath($doc); + foreach ($xp->query('//div[contains(@class,"changelog-entry")]') as $node) { + /** @var DOMElement $node */ + $title = ''; + foreach (['b', 'strong'] as $tag) { + $el = $node->getElementsByTagName($tag)->item(0); + if ($el) { $title = trim($el->textContent); break; } + } + $timeEl = $node->getElementsByTagName('time')->item(0); + $iso = $timeEl ? trim($timeEl->getAttribute('datetime')) : ''; + $when = $timeEl ? trim($timeEl->textContent) : ''; + + $body = ''; + foreach ($xp->query('.//span[contains(@class,"text")]', $node) as $b) { + $body = trim($b->textContent); + break; + } + $entries[] = compact('title', 'iso', 'when', 'body'); + } + + _recent_cache_put('changelog', $entries); + return array_slice($entries, 0, $limit); +} + +function render_recent_blog(int $limit = 2): void { + $posts = recent_blog_posts($limit); + if (!$posts) { + echo '<div class="muted">No posts to show right now.</div>'; + return; + } + $h = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); + echo '<div><strong>Posts</strong> (<a href="https://blog.sillylaird.ca/">all</a>)</div>'; + echo '<ol class="recent-list">'; + foreach ($posts as $p) { + $path = ltrim((string)($p['path'] ?? ''), '/'); + $iso = sprintf('%04d-%02d-%02d', (int)($p['year'] ?? 0), (int)($p['month'] ?? 0), (int)($p['day'] ?? 0)); + $iso = $iso === '0000-00-00' ? '' : $iso; + echo '<li><a href="https://blog.sillylaird.ca/' . $h($path) . '">' . $h($p['title'] ?? 'Untitled') . '</a>'; + if ($iso) echo '<div class="muted"><time datetime="' . $h($iso) . '">' . $h($iso) . '</time></div>'; + echo '</li>'; + } + echo '</ol>'; +} + +/** Latest changelog row for sidebar widgets (reads local TXT, no HTTP). */ +function latest_changelog_entry(): ?array { + static $entry = null; + static $loaded = false; + if ($loaded) return $entry; + $loaded = true; + + $parser = __DIR__ . '/../changelog/api/parser.php'; + if (!is_file($parser)) return null; + require_once $parser; + + $rows = get_changelog_entries(1); + $entry = $rows[0] ?? null; + return $entry; +} + +function render_recent_changelog(int $limit = 2): void { + $entries = recent_changelog_entries($limit); + if (!$entries) { + echo '<div class="muted">No entries to show right now.</div>'; + return; + } + $h = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); + echo '<div><strong>Entries</strong> (<a href="https://www.sillylaird.ca/changelog/">all</a>)</div>'; + echo '<ol class="recent-list">'; + foreach ($entries as $e) { + echo '<li>'; + if ($e['title']) echo '<strong>' . $h($e['title']) . '</strong>'; + if ($e['iso'] || $e['when']) { + echo '<div class="muted"><time'; + if ($e['iso']) echo ' datetime="' . $h($e['iso']) . '"'; + echo '>' . $h($e['when'] ?: $e['iso']) . '</time></div>'; + } + if ($e['body']) echo '<div>' . $h($e['body']) . '</div>'; + $yr = $e['iso'] ? substr((string)$e['iso'], 0, 4) : ''; + if ($yr) echo '<a class="changelog-more" href="https://www.sillylaird.ca/changelog/#year-' . $h($yr) . '">more (' . $h($yr) . ') →</a>'; + echo '</li>'; + } + echo '</ol>'; +} |
