aboutsummaryrefslogtreecommitdiffstats
path: root/partials/recent_static.php
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /partials/recent_static.php
downloadwww-main.tar.gz
www-main.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to 'partials/recent_static.php')
-rw-r--r--partials/recent_static.php195
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) . ') &rarr;</a>';
+ echo '</li>';
+ }
+ echo '</ol>';
+}