feat: add unfurl cards to blog page and homepage recent posts
Use two-strategy approach to work around async shortcode limitation
in deeply nested Nunjucks includes:
- blog.njk: async {% unfurl %} shortcode (top-level, works fine)
- recent-posts.njk: sync {{ url | unfurlCard | safe }} filter
(reads from pre-populated disk cache)
eleventy.before hook scans content files and pre-fetches all
interaction URLs before templates render, ensuring the sync filter
always has data — even on first build.
This commit is contained in:
@@ -35,7 +35,7 @@ function getCachePath(url) {
|
||||
return resolve(CACHE_DIR, `${hash}.json`);
|
||||
}
|
||||
|
||||
function readCache(url) {
|
||||
export function readCache(url) {
|
||||
const path = getCachePath(url);
|
||||
if (!existsSync(path)) return undefined; // undefined = not cached
|
||||
try {
|
||||
@@ -57,7 +57,7 @@ function writeCache(url, metadata, failed = false) {
|
||||
writeFileSync(path, JSON.stringify({ cachedAt: Date.now(), metadata, failed }));
|
||||
}
|
||||
|
||||
function extractDomain(url) {
|
||||
export function extractDomain(url) {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, "");
|
||||
} catch {
|
||||
@@ -65,7 +65,7 @@ function extractDomain(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
export function escapeHtml(str) {
|
||||
if (!str) return "";
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
@@ -74,12 +74,12 @@ function escapeHtml(str) {
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function renderFallbackLink(url) {
|
||||
export function renderFallbackLink(url) {
|
||||
const domain = escapeHtml(extractDomain(url));
|
||||
return `<a href="${escapeHtml(url)}" rel="noopener" target="_blank">${domain}</a>`;
|
||||
}
|
||||
|
||||
function renderCard(url, metadata) {
|
||||
export function renderCard(url, metadata) {
|
||||
const og = metadata.open_graph || {};
|
||||
const tc = metadata.twitter_card || {};
|
||||
|
||||
@@ -117,41 +117,58 @@ function renderCard(url, metadata) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch unfurl metadata for a URL and populate the disk cache.
|
||||
* Returns the rendered HTML card (or fallback link on failure).
|
||||
*/
|
||||
export async function prefetchUrl(url) {
|
||||
if (!url) return "";
|
||||
|
||||
// Already cached — skip network fetch
|
||||
const cached = readCache(url);
|
||||
if (cached !== undefined) {
|
||||
return cached.failed ? renderFallbackLink(url) : renderCard(url, cached.metadata);
|
||||
}
|
||||
|
||||
const metadata = await throttled(async () => {
|
||||
try {
|
||||
return await unfurl(url, {
|
||||
timeout: 20000,
|
||||
headers: { "User-Agent": USER_AGENT },
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`[unfurl] Failed to fetch ${url}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!metadata) {
|
||||
writeCache(url, null, true);
|
||||
return renderFallbackLink(url);
|
||||
}
|
||||
|
||||
writeCache(url, metadata, false);
|
||||
return renderCard(url, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous cache-only lookup. Returns the rendered card HTML if cached,
|
||||
* a fallback link if cached as failed, or empty string if not cached.
|
||||
* Safe to use in deeply nested Nunjucks includes where async isn't supported.
|
||||
*/
|
||||
export function getCachedCard(url) {
|
||||
if (!url) return "";
|
||||
const cached = readCache(url);
|
||||
if (cached === undefined) return renderFallbackLink(url);
|
||||
if (cached.failed) return renderFallbackLink(url);
|
||||
return renderCard(url, cached.metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the {% unfurl "URL" %} shortcode on an Eleventy config.
|
||||
*/
|
||||
export default function registerUnfurlShortcode(eleventyConfig) {
|
||||
eleventyConfig.addAsyncShortcode("unfurl", async function (url) {
|
||||
if (!url) return "";
|
||||
|
||||
// Check cache first (returns undefined if not cached)
|
||||
const cached = readCache(url);
|
||||
if (cached !== undefined) {
|
||||
// Cached failure → render fallback link
|
||||
if (cached.failed) return renderFallbackLink(url);
|
||||
// Cached success → render card
|
||||
return renderCard(url, cached.metadata);
|
||||
}
|
||||
|
||||
// Not cached — fetch with concurrency limiting
|
||||
const metadata = await throttled(async () => {
|
||||
try {
|
||||
return await unfurl(url, {
|
||||
timeout: 20000,
|
||||
headers: { "User-Agent": USER_AGENT },
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`[unfurl] Failed to fetch ${url}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!metadata) {
|
||||
writeCache(url, null, true); // cache failure for 1 day
|
||||
return renderFallbackLink(url);
|
||||
}
|
||||
|
||||
writeCache(url, metadata, false);
|
||||
return renderCard(url, metadata);
|
||||
return prefetchUrl(url);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user