feat(workshop): add social previews

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:51:44 +02:00
1 parent 6edf78acd7
commit 7833cca86b
7 files changed
+189

No files matched your search

@@ -1,5 +1,6 @@
import { access, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import vm from "node:vm";
const read = (path) => readFile(resolve(path), "utf8");
@@ -39,6 +40,34 @@ for (const artifact of [
if (hook.includes('findAllRecords("users")') || !hook.includes('findRecordsByFilter("users"')) {
throw new Error("Workshop report notifications must use a filtered, bounded administrator query");
}
if (!hook.includes('routerAdd("GET", "/workshop/{id}"') || !hook.includes('getStringSlice("screenshots")')) {
throw new Error("Workshop detail routes must render server-side social metadata with submission screenshots");
}
const hookUtilsModule = { exports: {} };
vm.runInNewContext(await read("apps/workshop/pb_hooks/workshop-utils.js"), { module: hookUtilsModule });
const socialHtml = hookUtilsModule.exports.renderWorkshopSocialHtml(
'<html><head><title data-rh="true">Homarr documentation</title><meta data-rh="true" property="og:title" content="Homarr documentation"><meta data-rh="true" name="description" content="Generic"><link data-rh="true" rel="canonical" href="https://homarr.dev/"></head></html>',
{
title: "Ocean <Glow> · Homarr Workshop",
description: "Custom CSS for Homarr. Calm & readable.",
url: "https://preview.example/workshop/abc",
image: "https://preview.example/api/files/submissions/abc/preview.png",
section: "Custom CSS",
submissionTitle: "Ocean <Glow>",
},
);
for (const expected of [
"<title>Ocean &lt;Glow&gt; · Homarr Workshop</title>",
'property="og:description" content="Custom CSS for Homarr. Calm &amp; readable."',
'rel="canonical" href="https://preview.example/workshop/abc"',
'property="og:image" content="https://preview.example/api/files/submissions/abc/preview.png"',
'name="twitter:card" content="summary_large_image"',
'property="article:section" content="Custom CSS"',
]) {
if (!socialHtml.includes(expected)) throw new Error(`Workshop social metadata is missing: ${expected}`);
}
if (socialHtml.includes("Homarr documentation")) throw new Error("Workshop social metadata must replace generic tags");
const migration = await read("apps/workshop/pb_migrations/1784240000_workshop_widgets.js");
for (const removedField of ["displayName", "avatarUrl", "githubProfileUrl", "githubUsername"]) {
@@ -0,0 +1,55 @@
const baseUrl = process.env.WORKSHOP_TEST_URL ?? "http://127.0.0.1:18091";
const publicWorkshopUrl = process.env.EXPECTED_WORKSHOP_WEB_URL ?? "https://v2.preview.homarr.dev/workshop";
const publicApiUrl = process.env.EXPECTED_WORKSHOP_API_URL ?? "https://v2.preview.homarr.dev";
const request = async (path, init = {}) => {
const response = await fetch(`${baseUrl}${path}`, init);
const body = await response.json().catch(() => null);
if (!response.ok) throw new Error(`${response.status} ${path}: ${JSON.stringify(body)}`);
return body;
};
const root = await request("/api/collections/_superusers/auth-with-password", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ identity: "workshop-image@example.invalid", password: "WorkshopImageTest123!" }),
});
const headers = { authorization: `Bearer ${root.token}` };
const author = await request("/api/collections/users/records", {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({
email: "social-author@example.invalid",
password: "WorkshopSocialTest123!",
passwordConfirm: "WorkshopSocialTest123!",
name: "social-author",
}),
});
const png = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
);
const data = new FormData();
data.set("type", "customCss");
data.set("title", "Ocean <Glow>");
data.set("description", "Calm & readable.");
data.set("widgetSchema", "homarr-custom-css-v1");
data.set("content", "body { color: white; }");
data.set("author", author.id);
data.set("screenshots", new File([png], "preview.png", { type: "image/png" }));
const submission = await request("/api/collections/submissions/records", { method: "POST", headers, body: data });
const screenshot = encodeURIComponent(submission.screenshots[0]);
const html = await fetch(`${baseUrl}/workshop/${submission.id}`).then((response) => response.text());
for (const expected of [
"<title>Ocean &lt;Glow&gt; · Homarr Workshop</title>",
'property="og:description" content="Custom CSS for Homarr. Calm &amp; readable."',
`rel="canonical" href="${publicWorkshopUrl}/${submission.id}"`,
`property="og:image" content="${publicApiUrl}/api/files/submissions/${submission.id}/${screenshot}"`,
'name="twitter:card" content="summary_large_image"',
'property="article:section" content="Custom CSS"',
]) {
if (!html.includes(expected)) throw new Error(`Rendered Workshop page is missing social metadata: ${expected}`);
}
console.log("Workshop social metadata integration passed");