Render GitHub blob image URLs

Image URLs pointing at a GitHub "blob" page, such as
https://github.com/<owner>/<repo>/blob/<ref>/<path>, serve an HTML
page rather than the raw image bytes, so they render as broken images
in markdown.

Add a canonicalizeGithubImageUrl helper that appends ?raw=true to such
URLs, which makes GitHub redirect to the raw content. URLs already
pointing at the raw content, via ?raw=true or raw.githubusercontent.com,
are left unchanged, as are non-GitHub and non-URL sources.

Wire the helper into the markdown image loop so external image URLs are
canonicalized while relative paths keep resolving against the repo's raw
endpoint.
This commit is contained in:
andreolf
2026-08-24 16:23:09 +02:00
committed by Rūdolfs Ošiņš
parent 678cdee6b1
commit 8bb45aa11d
3 changed files with 82 additions and 3 deletions
+8 -3
View File
@@ -37,6 +37,7 @@
twemoji,
scrollIntoView,
canonicalize,
canonicalizeGithubImageUrl,
isCommit,
} from "@app/lib/utils";
import { Renderer, markdown, sanitizeConfig } from "@app/lib/markdown";
@@ -226,10 +227,14 @@
const imagePath = i.getAttribute("src");
const imageClass = i.getAttribute("class");
// Make sure the source isn't a URL before trying to fetch it from the repo
const emoji = imageClass && imageClass === "txt-emoji";
if (imagePath && !isUrl(imagePath) && !emoji) {
i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
if (imagePath && !emoji) {
if (isUrl(imagePath)) {
// Rewrite GitHub "blob" URLs so the raw image content is rendered.
i.setAttribute("src", canonicalizeGithubImageUrl(imagePath));
} else {
i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
}
}
}
+23
View File
@@ -184,6 +184,29 @@ export function isUrl(input: string): boolean {
return /^https?:\/\//.test(input);
}
// Rewrite GitHub "blob" image URLs so they resolve to the raw image content.
// A URL like https://github.com/<owner>/<repo>/blob/<ref>/<path> serves an
// HTML page rather than the image bytes, so appending `?raw=true` makes GitHub
// redirect to the raw content. URLs already pointing at the raw content (either
// via `?raw=true` or raw.githubusercontent.com) are returned unchanged.
export function canonicalizeGithubImageUrl(input: string): string {
let url;
try {
url = new URL(input);
} catch {
return input;
}
if (
(url.hostname === "github.com" || url.hostname === "www.github.com") &&
/^\/[^/]+\/[^/]+\/blob\//.test(url.pathname) &&
url.searchParams.get("raw") !== "true"
) {
url.searchParams.set("raw", "true");
return url.toString();
}
return input;
}
export function isCommit(input: string): boolean {
return /^[a-f0-9]{40}$/.test(input);
}
+51
View File
@@ -82,6 +82,57 @@ describe("String Assertions", () => {
])("isUrl $url => $expected", ({ url, expected }) => {
expect(utils.isUrl(url)).toBe(expected);
});
test.each([
{
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png",
expected:
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
},
{
// Already has ?raw=true, leave it untouched.
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
expected:
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
},
{
// Raw content host is already correct, leave it untouched.
url: "https://raw.githubusercontent.com/radicle-dev/radicle-explorer/master/img.png",
expected:
"https://raw.githubusercontent.com/radicle-dev/radicle-explorer/master/img.png",
},
{
// An existing query string is preserved.
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?token=abc",
expected:
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?token=abc&raw=true",
},
{
// The www subdomain is handled too.
url: "https://www.github.com/radicle-dev/radicle-explorer/blob/master/img.png",
expected:
"https://www.github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
},
{
// A "blob" segment that isn't in the <owner>/<repo>/blob/ position is
// left untouched.
url: "https://github.com/radicle-dev/radicle-explorer/raw/master/blob/img.png",
expected:
"https://github.com/radicle-dev/radicle-explorer/raw/master/blob/img.png",
},
{
// Non-GitHub URLs are returned unchanged.
url: "https://example.com/img.png",
expected: "https://example.com/img.png",
},
{
// Not a valid URL, returned unchanged.
url: "not a url",
expected: "not a url",
},
])("canonicalizeGithubImageUrl $url => $expected", ({ url, expected }) => {
expect(utils.canonicalizeGithubImageUrl(url)).toEqual(expected);
});
});
describe("Parse Functions", () => {