Add previews!

This commit is contained in:
KenwoodFox 2025-11-13 17:16:55 -05:00
parent a08692bef0
commit 432bf550ff
6 changed files with 1405 additions and 5 deletions

162
build/generate-preview.js Normal file
View File

@ -0,0 +1,162 @@
import puppeteer from "puppeteer";
import { createServer } from "http";
import { readFileSync, existsSync } from "fs";
import { join, dirname, extname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, "..");
const distDir = join(root, "dist");
const PORT = 5174;
async function startServer() {
const httpServer = createServer((req, res) => {
const decodedUrl = decodeURIComponent(req.url || "/");
let filePath = join(distDir, decodedUrl === "/" ? "index.html" : decodedUrl);
if (!existsSync(filePath) || extname(filePath) === "") {
filePath = join(distDir, "index.html");
}
if (existsSync(filePath)) {
const content = readFileSync(filePath);
const ext = extname(filePath);
const contentType =
ext === ".html" ? "text/html" :
ext === ".js" ? "application/javascript" :
ext === ".css" ? "text/css" :
ext === ".json" ? "application/json" :
ext === ".png" ? "image/png" :
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" :
"application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
res.end(content);
} else {
res.writeHead(404);
res.end("Not found");
}
});
await new Promise((resolve, reject) => {
httpServer.listen(PORT, () => {
console.log(`Preview server running on http://localhost:${PORT}`);
resolve();
});
httpServer.on("error", reject);
});
return httpServer;
}
async function generatePreview(outputPath, uri, selector) {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"]
});
try {
const page = await browser.newPage();
await page.setViewport({
width: 1400,
height: 2000,
deviceScaleFactor: 2
});
await page.goto(`http://localhost:${PORT}${uri}`, {
waitUntil: "networkidle0"
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await page.waitForSelector(selector, { timeout: 10000 });
// Wait for images to load
await page.evaluate(() => {
return Promise.all(
Array.from(document.images).map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
setTimeout(resolve, 3000);
});
})
);
});
// For tank page, ensure stage renders at full size and wait for layers
if (uri === "/") {
await page.evaluate(() => {
const layout = document.querySelector(".layout");
if (layout) {
layout.style.width = "1400px";
layout.style.minWidth = "1200px";
}
const stage = document.querySelector(".stage");
if (stage) {
stage.style.flex = "1 1 1200px";
stage.style.width = "1200px";
stage.style.maxWidth = "1200px";
}
});
await new Promise((resolve) => setTimeout(resolve, 500));
// Wait for liquid layers to appear and have height
await page.waitForFunction(
() => {
const layers = document.querySelectorAll(".liquid-layer");
return layers.length > 0 && Array.from(layers).some(l => l.offsetHeight > 0);
},
{ timeout: 10000 }
);
}
await new Promise((resolve) => setTimeout(resolve, 500));
const element = await page.$(selector);
if (element) {
await element.screenshot({
path: outputPath,
type: "png"
});
console.log(`✓ Generated preview: ${outputPath}`);
}
} finally {
await browser.close();
}
}
export async function generateClonesPreview() {
if (!existsSync(distDir) || !existsSync(join(distDir, "index.html"))) {
return;
}
let httpServer;
try {
httpServer = await startServer();
await generatePreview(
join(distDir, "vixiclones-preview.png"),
"/vixiclones",
".clones-rows"
);
await generatePreview(
join(distDir, "main-preview.png"),
"/",
".stage"
);
} catch (error) {
console.error("Failed to generate previews:", error);
throw error;
} finally {
if (httpServer) {
await new Promise((resolve) => {
httpServer.close(() => resolve());
});
}
}
}

1193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.4.0"
"vite": "^5.4.0",
"puppeteer": "^24.15.0"
}
}

View File

@ -2,6 +2,7 @@ import "./tank.css";
export function renderTank(app) {
document.title = "cumtanks.snowsune.net";
updateMetaTags();
app.classList.remove("page-clones");
app.innerHTML = `
<section class="layout">
@ -161,3 +162,17 @@ function mixColor(hours) {
const channel = Math.round(255 + (136 - 255) * t);
return `rgb(${channel},${channel},${channel})`;
}
function updateMetaTags() {
const ogImage = document.querySelector('meta[property="og:image"]');
const twitterImage = document.querySelector('meta[name="twitter:image"]');
const ogTitle = document.querySelector('meta[property="og:title"]');
const twitterTitle = document.querySelector('meta[name="twitter:title"]');
const previewUrl = "/main-preview.png";
if (ogImage) ogImage.setAttribute("content", previewUrl);
if (twitterImage) twitterImage.setAttribute("content", previewUrl);
if (ogTitle) ogTitle.setAttribute("content", "cumtanks.snowsune.net");
if (twitterTitle) twitterTitle.setAttribute("content", "cumtanks.snowsune.net");
}

View File

@ -14,6 +14,7 @@ const DEFAULT_CLONES = [
export async function renderClones(app) {
document.title = "Vixi Clones";
updateMetaTags();
app.classList.add("page-clones");
app.innerHTML = `
<header class="clones-header">
@ -96,6 +97,20 @@ function renderClone(clone) {
`;
}
function updateMetaTags() {
const ogImage = document.querySelector('meta[property="og:image"]');
const twitterImage = document.querySelector('meta[name="twitter:image"]');
const ogTitle = document.querySelector('meta[property="og:title"]');
const twitterTitle = document.querySelector('meta[name="twitter:title"]');
const previewUrl = "/vixiclones-preview.png";
if (ogImage) ogImage.setAttribute("content", previewUrl);
if (twitterImage) twitterImage.setAttribute("content", previewUrl);
if (ogTitle) ogTitle.setAttribute("content", "Vixi Clones");
if (twitterTitle) twitterTitle.setAttribute("content", "Vixi Clones");
}
function formatName(slug) {
if (!slug) return "";
return slug

View File

@ -1,7 +1,21 @@
import { defineConfig } from "vite";
import { generateClonesPreview } from "./build/generate-preview.js";
export default defineConfig({
server: {
host: true
}
export default defineConfig(({ command }) => {
return {
server: {
host: true
},
plugins: [
{
name: "generate-clones-preview",
async closeBundle() {
// Only generate previews during build, not in dev/serve mode
if (command === "build") {
await generateClonesPreview();
}
}
}
]
};
});