the home site for me: also iteration 3 or 4 of my site
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: use relative time everywhere

+4 -249
+1 -8
templates/blog-page.html
··· 2 2 <div><a href="..">..</a>/<span class="accent-data">{{ page.slug }}</span></div> 3 3 <article class="h-entry"> 4 4 <a class="u-url" href="{{ page.permalink }}" style="display: none"> </a> 5 - <time 6 - datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S%z') }}" 7 - class="dt-published" 8 - >Published on: 9 - <span class="accent-data" 10 - >{{ page.date | split(pat="T") | first }}</span 11 - ></time 12 - > 5 + <span class="dt-published">Published <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D" class="accent-data">{{ page.date | split(pat="T") | first }}</relative-time></span> 13 6 {% if config.extra.author and config.extra.display_author == true %} 14 7 <address rel="author"> 15 8 By
+2 -2
templates/blog.html
··· 11 11 {% for page in section.pages %} {% if "archival" not in page.taxonomies.tags 12 12 %} 13 13 <li> 14 - {{ page.date | split(pat="T") | first }} &mdash; 14 + <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> &mdash; 15 15 <a href="{{ page.permalink | safe }}" class="text-glow" 16 16 >{{ page.title }}</a 17 17 > ··· 26 26 <ul> 27 27 {% for page in section.pages %} {% if "archival" in page.taxonomies.tags %} 28 28 <li> 29 - {{ page.date }} &mdash; 29 + <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> &mdash; 30 30 <a href="{{ page.permalink | safe }}" class="text-glow" 31 31 >{{ page.title }}</a 32 32 >
-238
templates/shortcodes/now_status.html
··· 1 - {% set api_url = 2 - "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update" 3 - %} {% set response = load_data(url=api_url, format="json") %} 4 - 5 - <style> 6 - #status-updates-container { 7 - display: flex; 8 - flex-direction: column; 9 - gap: 1.5rem; 10 - width: 100%; 11 - margin-bottom: 2rem; 12 - } 13 - 14 - .bsky-post { 15 - border-left: 0.375rem solid var(--accent); 16 - padding: 0.7em 1em; 17 - font-size: 1rem; 18 - background-color: var(--bg-light); 19 - border-radius: 0.375rem; 20 - } 21 - 22 - .bsky-post-content { 23 - margin-bottom: 0.75rem; 24 - line-height: 1.4; 25 - } 26 - 27 - .bsky-post-footer { 28 - display: flex; 29 - justify-content: space-between; 30 - align-items: center; 31 - color: var(--text-light); 32 - } 33 - 34 - .bsky-post-footer cite { 35 - display: inline-flex; 36 - align-items: center; 37 - gap: 0.4rem; 38 - } 39 - 40 - .bsky-post-time { 41 - font-size: 0.8rem; 42 - color: var(--text-light); 43 - } 44 - </style> 45 - 46 - <div id="status-updates-container"> 47 - {% if response.records %} {% for record in response.records | 48 - sort(attribute="value.createdAt") | reverse %} {% set created_at = 49 - record.value.createdAt %} {% set status_text = record.value.text %} 50 - <div 51 - class="bsky-post" 52 - data-cid="{{ record.cid }}" 53 - data-created="{{ created_at }}" 54 - > 55 - <div class="bsky-post-content">Kieran was {{ status_text }}</div> 56 - <div class="bsky-post-footer"> 57 - <cite> 58 - <img 59 - src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg" 60 - alt="Kieran's avatar" 61 - class="avatar" 62 - /> 63 - <a 64 - href="https://bsky.app/profile/doing.dunkirk.sh" 65 - target="_blank" 66 - rel="noopener" 67 - >@doing.dunkirk.sh</a 68 - > 69 - </cite> 70 - <span class="bsky-post-time"> 71 - {{ record.value.createdAt | date(format="%b %d, %Y") }} 72 - </span> 73 - </div> 74 - </div> 75 - {% endfor %} {% else %} 76 - <div class="bsky-post"> 77 - <div class="bsky-post-content">No status updates found.</div> 78 - </div> 79 - {% endif %} 80 - </div> 81 - 82 - <script> 83 - document.addEventListener("DOMContentLoaded", () => { 84 - const container = document.getElementById("status-updates-container"); 85 - const API_URL = 86 - "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update"; 87 - const existingPosts = new Map(); 88 - 89 - // Collect existing posts by CID 90 - document.querySelectorAll(".bsky-post[data-cid]").forEach((post) => { 91 - existingPosts.set(post.dataset.cid, { 92 - element: post, 93 - created: new Date(post.dataset.created), 94 - }); 95 - }); 96 - 97 - // Format time relative to now 98 - function formatTimeAgo(date) { 99 - const now = new Date(); 100 - const diffInMs = now - date; 101 - const diffInMins = Math.floor(diffInMs / (1000 * 60)); 102 - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); 103 - 104 - if (diffInMins < 1) return "just now"; 105 - if (diffInMins < 60) return `${Math.round(diffInMins)}m`; 106 - if (diffInHours < 24) return `${Math.round(diffInHours)}h`; 107 - 108 - return new Intl.DateTimeFormat("en", { 109 - month: "short", 110 - day: "numeric", 111 - }).format(date); 112 - } 113 - 114 - // Update timestamps and verbs on existing posts 115 - function updateTimestamps() { 116 - existingPosts.forEach((post) => { 117 - const timeElement = 118 - post.element.querySelector(".bsky-post-time"); 119 - const contentElement = 120 - post.element.querySelector(".bsky-post-content"); 121 - if (timeElement) { 122 - timeElement.textContent = formatTimeAgo(post.created); 123 - } 124 - 125 - // Update the is/was verb based on post age 126 - const now = new Date(); 127 - const diffInMs = now - post.created; 128 - const diffInMins = diffInMs / (1000 * 60); 129 - const verb = diffInMins < 30 ? "is" : "was"; 130 - 131 - // Get the status text (everything after "Kieran was/is ") 132 - if (contentElement) { 133 - const text = contentElement.textContent; 134 - const statusText = text.replace(/^Kieran (is|was) /, ""); 135 - contentElement.textContent = `Kieran ${verb} ${statusText}`; 136 - } 137 - }); 138 - } 139 - 140 - // Create a new post element 141 - function createPostElement(record) { 142 - const createdDate = new Date(record.value.createdAt); 143 - const postElement = document.createElement("div"); 144 - postElement.className = "bsky-post"; 145 - postElement.dataset.cid = record.cid; 146 - postElement.dataset.created = record.value.createdAt; 147 - 148 - // Determine if status is recent (within 30 minutes) 149 - const now = new Date(); 150 - const diffInMs = now - createdDate; 151 - const diffInMins = diffInMs / (1000 * 60); 152 - const verb = diffInMins < 30 ? "is" : "was"; 153 - 154 - postElement.innerHTML = ` 155 - <div class="bsky-post-content">Kieran ${verb} ${record.value.text}</div> 156 - <div class="bsky-post-footer"> 157 - <cite> 158 - <img src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg" alt="Kieran's avatar" class="avatar" /> 159 - <a href="https://bsky.app/@doing.dunkirk.sh" target="_blank" rel="noopener">@doing.dunkirk.sh</a> 160 - </cite> 161 - <span class="bsky-post-time">${formatTimeAgo(createdDate)}</span> 162 - </div> 163 - `; 164 - 165 - return postElement; 166 - } 167 - 168 - // Fetch and update posts 169 - function fetchAndUpdatePosts() { 170 - fetch(API_URL) 171 - .then((response) => response.json()) 172 - .then((data) => { 173 - if (!data.records || data.records.length === 0) { 174 - if (existingPosts.size === 0) { 175 - container.innerHTML = 176 - '<div class="bsky-post"><div class="bsky-post-content">No status updates found.</div></div>'; 177 - } 178 - return; 179 - } 180 - 181 - // Sort newest first 182 - const sortedRecords = data.records.sort((a, b) => { 183 - return ( 184 - new Date(b.value.createdAt) - 185 - new Date(a.value.createdAt) 186 - ); 187 - }); 188 - 189 - // Track if we need to reorder 190 - let needsReordering = false; 191 - 192 - // Add new posts 193 - for (const record of sortedRecords) { 194 - if (!existingPosts.has(record.cid)) { 195 - const newPostElement = createPostElement(record); 196 - // Always insert at the beginning for now (we'll reorder if needed) 197 - container.insertBefore( 198 - newPostElement, 199 - container.firstChild, 200 - ); 201 - existingPosts.set(record.cid, { 202 - element: newPostElement, 203 - created: new Date(record.value.createdAt), 204 - }); 205 - needsReordering = true; 206 - } 207 - } 208 - 209 - // If we added new posts, reorder everything 210 - if (needsReordering) { 211 - const sortedElements = [...existingPosts.entries()] 212 - .sort((a, b) => b[1].created - a[1].created) 213 - .map((entry) => entry[1].element); 214 - 215 - // Reattach in correct order 216 - sortedElements.forEach((element) => { 217 - container.appendChild(element); 218 - }); 219 - } 220 - 221 - // Update all timestamps 222 - updateTimestamps(); 223 - }) 224 - .catch((error) => { 225 - console.error("Error fetching status updates:", error); 226 - }); 227 - } 228 - 229 - // Initial update 230 - fetchAndUpdatePosts(); 231 - 232 - // Update timestamps every minute 233 - setInterval(updateTimestamps, 60000); 234 - 235 - // Fetch new posts every 5 minutes 236 - setInterval(fetchAndUpdatePosts, 300000); 237 - }); 238 - </script>
+1 -1
templates/tags/single.html
··· 6 6 {% for page in pages %} 7 7 <li> 8 8 <a href="{{ page.permalink | safe }}" 9 - >{% if page.date %}{{ page.date }} - {% endif %}{{ page.title }}</a 9 + >{% if page.date %}<relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> - {% endif %}{{ page.title }}</a 10 10 > 11 11 </li> 12 12 {% endfor %}