Add previews!
This commit is contained in:
parent
a08692bef0
commit
432bf550ff
|
|
@ -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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,6 +9,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0",
|
||||||
|
"puppeteer": "^24.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/tank.js
15
src/tank.js
|
|
@ -2,6 +2,7 @@ import "./tank.css";
|
||||||
|
|
||||||
export function renderTank(app) {
|
export function renderTank(app) {
|
||||||
document.title = "cumtanks.snowsune.net";
|
document.title = "cumtanks.snowsune.net";
|
||||||
|
updateMetaTags();
|
||||||
app.classList.remove("page-clones");
|
app.classList.remove("page-clones");
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<section class="layout">
|
<section class="layout">
|
||||||
|
|
@ -161,3 +162,17 @@ function mixColor(hours) {
|
||||||
const channel = Math.round(255 + (136 - 255) * t);
|
const channel = Math.round(255 + (136 - 255) * t);
|
||||||
return `rgb(${channel},${channel},${channel})`;
|
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");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const DEFAULT_CLONES = [
|
||||||
|
|
||||||
export async function renderClones(app) {
|
export async function renderClones(app) {
|
||||||
document.title = "Vixi Clones";
|
document.title = "Vixi Clones";
|
||||||
|
updateMetaTags();
|
||||||
app.classList.add("page-clones");
|
app.classList.add("page-clones");
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<header class="clones-header">
|
<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) {
|
function formatName(slug) {
|
||||||
if (!slug) return "";
|
if (!slug) return "";
|
||||||
return slug
|
return slug
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import { generateClonesPreview } from "./build/generate-preview.js";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => {
|
||||||
server: {
|
return {
|
||||||
host: true
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue