move to raw html, make readme use buns html rewrite and always set to
All checks were successful
Code quality checks / biome (push) Successful in 8s

lazy image load
This commit is contained in:
creations 2025-04-26 11:10:31 -04:00
parent 10416dbff0
commit 2ee5f0512e
Signed by: creations
GPG key ID: 8F553AA4320FC711
7 changed files with 33 additions and 62 deletions

View file

@ -1,6 +1,6 @@
# Discord Profile Page # Discord Profile Page
A cool little web app that shows your Discord profile, current activity, and more. Built with Bun and EJS. A cool little web app that shows your Discord profile, current activity, and more. Built with Bun.
--- ---

View file

@ -12,7 +12,6 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@types/bun": "^1.2.8", "@types/bun": "^1.2.8",
"@types/ejs": "^3.1.5",
"globals": "^16.0.0" "globals": "^16.0.0"
}, },
"peerDependencies": { "peerDependencies": {
@ -20,9 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@creations.works/logger": "^1.0.3", "@creations.works/logger": "^1.0.3",
"ejs": "^3.1.10",
"isomorphic-dompurify": "^2.23.0", "isomorphic-dompurify": "^2.23.0",
"linkedom": "^0.18.9",
"marked": "^15.0.7" "marked": "^15.0.7"
} }
} }

View file

@ -1,26 +0,0 @@
import { resolve } from "node:path";
import { renderFile } from "ejs";
export async function renderEjsTemplate(
viewName: string | string[],
data: EjsTemplateData,
headers?: Record<string, string | number | boolean>,
): Promise<Response> {
let templatePath: string;
if (Array.isArray(viewName)) {
templatePath = resolve("src", "views", ...viewName);
} else {
templatePath = resolve("src", "views", viewName);
}
if (!templatePath.endsWith(".ejs")) {
templatePath += ".ejs";
}
const html: string = await renderFile(templatePath, data);
return new Response(html, {
headers: { "Content-Type": "text/html", ...headers },
});
}

View file

@ -1,5 +1,6 @@
import { resolve } from "node:path";
import { badgeApi, lanyardConfig } from "@config/environment"; import { badgeApi, lanyardConfig } from "@config/environment";
import { renderEjsTemplate } from "@helpers/ejs"; import { file } from "bun";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "GET", method: "GET",
@ -13,17 +14,24 @@ async function handler(request: ExtendedRequest): Promise<Response> {
.replace(/^https?:\/\//, "") .replace(/^https?:\/\//, "")
.replace(/\/$/, ""); .replace(/\/$/, "");
const ejsTemplateData: EjsTemplateData = { const path = resolve("src", "views", "index.html");
title: "Discord Profile", const bunFile = file(path);
username: "",
user: { id: id || lanyardConfig.userId },
instance: instance,
badgeApi: badgeApi,
avatar: `https://cdn.discordapp.com/embed/avatars/${Math.floor(Math.random() * 5)}.png`,
extraOptions: {},
};
return await renderEjsTemplate("index", ejsTemplateData); const html = new HTMLRewriter()
.on("head", {
element(head) {
head.setAttribute("data-user-id", id || lanyardConfig.userId);
head.setAttribute("data-instance-uri", instance);
head.setAttribute("data-badge-url", badgeApi || "");
},
})
.transform(await bunFile.text());
return new Response(html, {
headers: {
"Content-Type": "text/html",
},
});
} }
export { handler, routeDef }; export { handler, routeDef };

View file

@ -1,8 +1,6 @@
import { redisTtl } from "@config/environment"; import { redisTtl } from "@config/environment";
import { fetch } from "bun"; import { fetch } from "bun";
import { redis } from "bun"; import { redis } from "bun";
import DOMPurify from "isomorphic-dompurify";
import { parseHTML } from "linkedom";
import { marked } from "marked"; import { marked } from "marked";
const routeDef: RouteDef = { const routeDef: RouteDef = {
@ -12,6 +10,16 @@ const routeDef: RouteDef = {
log: false, log: false,
}; };
async function addLazyLoading(html: string): Promise<string> {
return new HTMLRewriter()
.on("img", {
element(el) {
el.setAttribute("loading", "lazy");
},
})
.transform(html);
}
async function fetchAndCacheReadme(url: string): Promise<string | null> { async function fetchAndCacheReadme(url: string): Promise<string | null> {
const cacheKey = `readme:${url}`; const cacheKey = `readme:${url}`;
const cached = await redis.get(cacheKey); const cached = await redis.get(cacheKey);
@ -33,22 +41,9 @@ async function fetchAndCacheReadme(url: string): Promise<string | null> {
const text = await res.text(); const text = await res.text();
if (!text || text.length < 10) return null; if (!text || text.length < 10) return null;
let html: string; const html = /\.(html?|htm)$/i.test(url) ? text : await marked.parse(text);
if (/\.(html?|htm)$/i.test(url)) {
html = text;
} else {
html = await marked.parse(text);
}
const { document } = parseHTML(html); const safe = await addLazyLoading(html);
for (const img of Array.from(document.querySelectorAll("img"))) {
if (!img.hasAttribute("loading")) {
img.setAttribute("loading", "lazy");
}
}
const dirtyHtml = document.toString();
const safe = DOMPurify.sanitize(dirtyHtml) || "";
await redis.set(cacheKey, safe); await redis.set(cacheKey, safe);
await redis.expire(cacheKey, redisTtl); await redis.expire(cacheKey, redisTtl);

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head data-user-id="<%= user.id %>" data-instance-uri="<%= instance %>" data-badge-url="<%= badgeApi %>"> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">

3
types/ejs.d.ts vendored
View file

@ -1,3 +0,0 @@
interface EjsTemplateData {
[key: string]: string | number | boolean | object | undefined | null;
}