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
All checks were successful
Code quality checks / biome (push) Successful in 8s
lazy image load
This commit is contained in:
parent
10416dbff0
commit
2ee5f0512e
7 changed files with 33 additions and 62 deletions
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
3
types/ejs.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
interface EjsTemplateData {
|
|
||||||
[key: string]: string | number | boolean | object | undefined | null;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue