blog image bug fix

This commit is contained in:
Mayur Shinde
2026-07-17 21:17:29 +05:30
parent 09119ae6dc
commit e6d6b60713
13 changed files with 80 additions and 136 deletions
+52 -6
View File
@@ -27,6 +27,40 @@ function truncate(text: string, max: number): string {
return `${cut.slice(0, lastSpace > 0 ? lastSpace : max).trimEnd()}`;
}
/**
* Upgrades a Ghost-hosted `http://` URL to `https://`.
*
* Ghost builds asset URLs from its own configured `url` setting. If that is set
* to http (as api.lynkedup.dev currently is), every image comes back insecure and
* browsers block it as mixed content on our HTTPS pages — silently, before the
* server's 301 to https can help.
*
* Only the configured Ghost host is upgraded: an arbitrary external http image in
* post content may genuinely have no https, and forcing it would break it.
*/
export function secureGhostUrl(url: string | null, ghostHost: string): string | null {
if (!url || !url.startsWith("http://")) return url;
try {
const parsed = new URL(url);
if (parsed.host !== ghostHost) return url;
parsed.protocol = "https:";
return parsed.toString();
} catch {
return url;
}
}
/** Applies {@link secureGhostUrl} to src/srcset/href attributes inside post HTML. */
function secureHtmlUrls(html: string, ghostHost: string): string {
return html.replace(
/(\s(?:src|href)=")(http:\/\/[^"]+)(")/gi,
(match, pre: string, url: string, post: string) => {
const secure = secureGhostUrl(url, ghostHost);
return secure ? `${pre}${secure}${post}` : match;
}
);
}
/**
* Rewrites a Ghost image URL to a resized variant. Ghost serves derivatives from
* `/content/images/size/w<width>/...`; any non-Ghost or already-sized URL is
@@ -77,12 +111,13 @@ function resolveExcerpt(post: GhostPost, plain: string): string {
function resolveSeo(
post: GhostPost,
config: GhostConfig,
ghostHost: string,
title: string,
excerpt: string
): PostSeo {
const metaTitle = post.meta_title?.trim() || `${title} - LynkedUp Pro Blog`;
const metaDescription = post.meta_description?.trim() || excerpt;
const ogImage = ghostImage(post.og_image || post.feature_image, 1200);
const ogImage = secureGhostUrl(ghostImage(post.og_image || post.feature_image, 1200), ghostHost);
return {
metaTitle,
@@ -92,7 +127,7 @@ function resolveSeo(
ogImage,
twitterTitle: post.twitter_title?.trim() || metaTitle,
twitterDescription: post.twitter_description?.trim() || metaDescription,
twitterImage: ghostImage(post.twitter_image, 1200) || ogImage,
twitterImage: secureGhostUrl(ghostImage(post.twitter_image, 1200), ghostHost) || ogImage,
// Ghost's canonical_url wins when an editor has set one (e.g. syndicated
// posts); otherwise the canonical is this site's own URL for the slug.
canonicalUrl: post.canonical_url?.trim() || `${config.siteUrl}/blog/${post.slug}/`,
@@ -106,9 +141,20 @@ const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
});
/** The Ghost host (e.g. "api.lynkedup.dev"), for upgrading its http asset URLs. */
function ghostHostFrom(config: GhostConfig): string {
try {
return new URL(config.apiUrl).host;
} catch {
return "";
}
}
export function normalizePost(post: GhostPost, config: GhostConfig): BlogPost {
const html = post.html ?? "";
const plain = toPlainText(html);
const ghostHost = ghostHostFrom(config);
const rawHtml = post.html ?? "";
const html = secureHtmlUrls(rawHtml, ghostHost);
const plain = toPlainText(rawHtml);
const title = post.title?.trim() || "Untitled";
const excerpt = resolveExcerpt(post, plain);
const tags = resolveTags(post);
@@ -120,7 +166,7 @@ export function normalizePost(post: GhostPost, config: GhostConfig): BlogPost {
title,
html,
excerpt,
featureImage: post.feature_image,
featureImage: secureGhostUrl(post.feature_image, ghostHost),
featureImageAlt: post.feature_image_alt?.trim() || title,
author: resolveAuthor(post),
publishedAt: validDate,
@@ -129,7 +175,7 @@ export function normalizePost(post: GhostPost, config: GhostConfig): BlogPost {
readingTime: resolveReadingTime(post, plain),
tags,
primaryTagLabel: (tags[0]?.name || "Field notes").toUpperCase(),
seo: resolveSeo(post, config, title, excerpt),
seo: resolveSeo(post, config, ghostHost, title, excerpt),
};
}
+2 -2
View File
@@ -91,8 +91,8 @@
}
.nav{display:flex;align-items:center;gap:28px;height:60px}
.logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:17px;letter-spacing:-.01em}
.logo svg{width:24px;height:24px}
.logo span b{color:var(--amber);font-weight:700}
/* Wordmark lockup (195x42): fix the height and let width follow the aspect ratio. */
.logo img{height:26px;width:auto;display:block}
.nav-links{display:flex;gap:26px;margin-left:auto}
.nav-links a{font-size:14px;color:var(--dim);transition:color .12s ease;position:relative}
.nav-links a::before{