feat: rebuild mintel.me as a single-page freelance site

The site now presents one freelance developer on one page: hero, introduction,
four typical cases (developer for a product, make an app reliable, automate a
process, a website) with what you get, how it runs and what it costs, a
process automation section, technology and project fit, how I work, two live
references, questions and contact.

- Hero and closing headline are drawn from running binary digits.
- Each case explains its four steps with an animated drawing; the automation
  section shows example workflows with work travelling between systems; a
  pipeline run shows what a test catches before a release.
- Contact is one email address, kept in parts and assembled in the browser so
  it never appears in the page source (src/domain/email.ts, spec first).
- Imprint and privacy pages added.
- The old site (blog, case studies, contact form, fixed-price pages, video
  rendering and related scripts) is removed. Its URLs redirect to the homepage.
- The deploy smoke test checks the legal pages and the redirects instead of
  the removed case study.
- Dev scripts use docker compose v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 22:33:06 +02:00
co-authored by Claude Opus 5.5
parent 4eb663f42a
commit fa40fefe2f
781 changed files with 4925 additions and 55137 deletions
-210
View File
@@ -1,210 +0,0 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { existsSync } from "node:fs";
import { config as dotenvConfig } from "dotenv";
import { ConceptPipeline } from "@mintel/concept-engine";
import { EstimationPipeline } from "@mintel/estimation-engine";
import { PdfEngine } from "@mintel/pdf/server";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
dotenvConfig({ path: path.resolve(process.cwd(), "../../.env") });
dotenvConfig({ path: path.resolve(process.cwd(), ".env") });
try {
const require = createRequire(import.meta.url);
const conceptPkg = require.resolve("@mintel/concept-engine");
const atMintelEnv = path.resolve(path.dirname(conceptPkg), "../../../.env");
if (existsSync(atMintelEnv)) {
dotenvConfig({ path: atMintelEnv });
}
} catch {
/* @mintel/concept-engine not resolvable — skip */
}
async function main() {
const OPENROUTER_KEY =
process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_KEY;
if (!OPENROUTER_KEY) {
console.error("❌ Error: OPENROUTER_API_KEY not found in environment.");
process.exit(1);
}
const args = process.argv.slice(2);
const briefingArg = args[0];
if (!briefingArg) {
console.error(
"❌ Error: Provide a briefing file or text as the first argument.",
);
process.exit(1);
}
let targetUrl = "";
let budget = "";
let comments = "";
const clearCache = args.includes("--clear-cache");
for (let i = 1; i < args.length; i++) {
if (args[i] === "--url") targetUrl = args[++i];
if (args[i] === "--budget") budget = args[++i];
if (args[i] === "--comments") comments = args[++i];
}
let briefing = briefingArg;
if (briefing.startsWith("@")) {
const rawPath = briefing.substring(1);
const filePath = rawPath.startsWith("/")
? rawPath
: path.resolve(process.cwd(), rawPath);
if (existsSync(filePath)) {
briefing = await fs.readFile(filePath, "utf8");
console.log(`📄 Loaded briefing from: ${filePath}`);
} else {
console.error(`❌ Briefing file not found: ${filePath}`);
process.exit(1);
}
}
if (!targetUrl) {
const urlMatch = briefing.match(/https?:\/\/[^\s]+/);
if (urlMatch) targetUrl = urlMatch[0];
}
const monorepoRoot = path.resolve(process.cwd(), "../../");
const crawlDir = path.join(
path.resolve(monorepoRoot, "../at-mintel"),
"data/crawls",
);
const outputDir = path.join(monorepoRoot, "out");
const konzeptDir = path.join(outputDir, "konzept");
const schaetzungDir = path.join(outputDir, "schaetzung");
const agbDir = path.join(outputDir, "agb");
const infoDir = path.join(outputDir, "info");
const deckblattDir = path.join(outputDir, "deckblatt");
const abschlussDir = path.join(outputDir, "abschluss");
await fs.mkdir(outputDir, { recursive: true });
await fs.mkdir(konzeptDir, { recursive: true });
await fs.mkdir(schaetzungDir, { recursive: true });
await fs.mkdir(agbDir, { recursive: true });
await fs.mkdir(infoDir, { recursive: true });
await fs.mkdir(deckblattDir, { recursive: true });
await fs.mkdir(abschlussDir, { recursive: true });
const conceptPipeline = new ConceptPipeline({
openrouterKey: OPENROUTER_KEY,
zyteApiKey: process.env.ZYTE_API_KEY,
outputDir,
crawlDir,
});
const engine = new PdfEngine() as any;
const headerIcon = path.join(
monorepoRoot,
"apps/web/src/assets/logo/Icon-Black-Transparent.png",
);
const footerLogo = path.join(
monorepoRoot,
"apps/web/src/assets/logo/Logo-Black-Transparent.png",
);
try {
const conceptResult = await conceptPipeline.run({
briefing,
url: targetUrl,
comments,
clearCache,
});
const companyName = conceptResult.auditedFacts?.companyName || "unknown";
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
console.log("\n📄 Generating Concept PDF...");
const conceptPdfPath = path.join(
konzeptDir,
`${companyName}_Konzept_${timestamp}.pdf`,
);
await engine.generateConceptPdf(conceptResult, conceptPdfPath, {
headerIcon,
footerLogo,
});
console.log(`✅ Created Concept PDF at: ${conceptPdfPath}`);
console.log("\n==================================================");
console.log("💰 PART 2: ESTIMATION ENGINE");
console.log("==================================================");
const estimationPipeline = new EstimationPipeline({
openrouterKey: OPENROUTER_KEY,
outputDir,
crawlDir: "",
});
const estimationResult = await estimationPipeline.run({
concept: conceptResult,
budget,
});
if (estimationResult.formState) {
console.log("\n📄 Generating Estimation PDF...");
const estimationPdfPath = path.join(
schaetzungDir,
`${companyName}_Angebot_${timestamp}.pdf`,
);
await engine.generateEstimatePdf(
estimationResult.formState,
estimationPdfPath,
{ headerIcon, footerLogo },
);
console.log(`✅ Created Angebot PDF at: ${estimationPdfPath}`);
console.log("\n📄 Generating AGBs PDF...");
const agbPdfPath = path.join(
agbDir,
`${companyName}_AGBs_${timestamp}.pdf`,
);
await engine.generateAgbsPdf(agbPdfPath, { headerIcon, footerLogo });
console.log(`✅ Created AGBs PDF at: ${agbPdfPath}`);
console.log("\n📄 Generating Deckblatt PDF...");
const deckblattPdfPath = path.join(
deckblattDir,
`${companyName}_Deckblatt_${timestamp}.pdf`,
);
await engine.generateFrontPagePdf(
estimationResult.formState,
deckblattPdfPath,
{ headerIcon },
);
console.log(`✅ Created Deckblatt PDF at: ${deckblattPdfPath}`);
console.log("\n📄 Generating Abschluss PDF...");
const abschlussPdfPath = path.join(
abschlussDir,
`${companyName}_Abschluss_${timestamp}.pdf`,
);
await engine.generateClosingPdf(abschlussPdfPath, {
headerIcon,
footerLogo,
});
console.log(`✅ Created Abschluss PDF at: ${abschlussPdfPath}`);
} else {
console.log("\n⚠️ No formState generated, skipping Estimation PDF.");
}
// Generate Info PDF
console.log("\n📄 Generating Arbeitsweise PDF...");
const infoPdfPath = path.join(
infoDir,
`${companyName}_Arbeitsweise_${timestamp}.pdf`,
);
await engine.generateInfoPdf(infoPdfPath, { headerIcon, footerLogo });
console.log(`✅ Created Arbeitsweise PDF at: ${infoPdfPath}`);
} catch (e) {
console.error(`\n❌ Pipeline failed: ${(e as Error).message}`);
process.exit(1);
}
}
main();
-230
View File
@@ -1,230 +0,0 @@
import puppeteer from "puppeteer";
const targetUrl = process.env.TEST_URL || "http://localhost:3000";
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "secret";
async function fetchSitemapUrls(baseUrl: string): Promise<string[]> {
const sitemapUrl = `${baseUrl.replace(/\/$/, "")}/sitemap.xml`;
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
try {
const response = await fetch(sitemapUrl);
const text = await response.text();
// Simple regex to extract loc tags
const matches = text.matchAll(/<loc>(.*?)<\/loc>/g);
let urls = Array.from(matches, (m) => m[1]);
// Normalize to target URL instance
const urlPattern = /https?:\/\/[^\/]+/;
urls = [...new Set(urls)]
.filter((u) => u.startsWith("http"))
.map((u) => u.replace(urlPattern, baseUrl.replace(/\/$/, "")))
.sort();
console.log(`✅ Found ${urls.length} target URLs.`);
return urls;
} catch (err: any) {
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
return [];
}
}
async function main() {
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
let urls = await fetchSitemapUrls(targetUrl);
if (urls.length === 0) {
console.warn(`⚠️ Falling back to just the homepage.`);
urls = [targetUrl];
}
// Launch browser with KLZ pattern: use system chromium via env
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
const browser = await puppeteer.launch({
headless: true,
executablePath:
process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.CHROME_PATH ||
undefined,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--ignore-certificate-errors",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
],
});
const page = await browser.newPage();
let hasBrokenAssets = false;
let currentScannedUrl = urls[0] || "";
// Listen for console logging from the page for debugging
page.on("console", (msg) => {
const type = msg.type();
// Only capture errors and warnings, not info/logs
if (type === "error" || type === "warn") {
const text = msg.text();
// Exclude common noise
if (
text.includes("google-analytics") ||
text.includes("googletagmanager") ||
text.includes("Fast Refresh")
)
return;
console.log(` [PAGE ${type.toUpperCase()}] ${text}`);
}
});
page.on("pageerror", (err: unknown) => {
if (currentScannedUrl.includes("showcase")) return;
console.error(
` [PAGE EXCEPTION] ${err instanceof Error ? err.message : String(err)}`,
);
});
// Listen to ALL network responses to catch broken assets (404/500)
page.on("response", (response) => {
const status = response.status();
// Catch classic 404s and 500s on ANY fetch/image/script
if (
status >= 400 &&
status !== 429 &&
status !== 999 &&
!response.url().includes("google-analytics") &&
!response.url().includes("googletagmanager")
) {
const type = response.request().resourceType();
// We explicitly care about images, scripts, stylesheets, and fetches getting 404/500s.
if (
["image", "script", "stylesheet", "fetch", "xhr", "document"].includes(
type,
)
) {
// Exclude showcase routes from strict sub-asset checking since they proxy external content
if (
(currentScannedUrl.includes("showcase") ||
response.url().includes("showcase")) &&
type !== "document"
) {
return;
}
console.error(
` [REQUEST FAILED] ${response.url()} - Status: ${status} (${type})`,
);
hasBrokenAssets = true;
}
}
});
try {
// Authenticate through Gatekeeper
console.log(`\n🛡️ Authenticating through Gatekeeper...`);
console.log(` Navigating to: ${urls[0]}`);
const response = await page.goto(urls[0], {
waitUntil: "domcontentloaded",
timeout: 120000,
});
// Give Gatekeeper a second to redirect if needed
console.log(` Waiting for potential Gatekeeper redirect...`);
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log(` Response status: ${response?.status()}`);
console.log(` Response URL: ${response?.url()}`);
const isGatekeeperPage = await page.$('input[name="password"]');
if (isGatekeeperPage) {
await page.type('input[name="password"]', gatekeeperPassword);
await Promise.all([
page.waitForNavigation({
waitUntil: "domcontentloaded",
timeout: 120000,
}),
page.click('button[type="submit"]'),
]);
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log(`✅ Gatekeeper authentication successful!`);
} else {
console.log(`✅ Already authenticated (no Gatekeeper gate detected).`);
}
// Scan each page
console.log(`\n🧪 Testing all ${urls.length} pages...`);
for (let i = 0; i < urls.length; i++) {
const u = urls[i];
currentScannedUrl = u;
console.log(`\n[${i + 1}/${urls.length}] Scanning: ${u}`);
try {
await page.goto(u, { waitUntil: "domcontentloaded", timeout: 120000 });
// Simulate a scroll to bottom to trigger lazy-loads if necessary
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
let totalHeight = 0;
const distance = 500;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
// Stop scrolling if we reached the bottom or scrolled for more than 5 seconds
if (totalHeight >= scrollHeight || totalHeight > 10000) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
// Small delay for final hydration and asynchronous asset loading
await new Promise((resolve) => setTimeout(resolve, 1500));
const title = await page.title();
console.log(` ✅ Page Title: ${title}`);
if (!title) {
throw new Error(`Page title is missing.`);
}
} catch (err: any) {
console.error(
` ❌ Timeout or navigation error on ${u}: ${err.message}`,
);
hasBrokenAssets = true;
}
}
} catch (err: any) {
console.error(`\n❌ Fatal Test Error: ${err.message}`);
// Take a screenshot for debugging on crash
try {
const screenshotPath = "/tmp/e2e-failure.png";
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`📸 Screenshot saved to ${screenshotPath}`);
} catch {
/* ignore */
}
hasBrokenAssets = true;
}
await browser.close();
if (hasBrokenAssets) {
console.error(
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
);
process.exit(1);
}
console.log(
`\n🎉 SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken assets!`,
);
process.exit(0);
}
main();
-99
View File
@@ -1,99 +0,0 @@
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";
import path from "path";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const client = new S3Client({
region: process.env.S3_REGION || "fsn1",
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY || "",
secretAccessKey: process.env.S3_SECRET_KEY || "",
},
forcePathStyle: true,
});
async function downloadFile(key: string, localPath: string) {
try {
const bucket = process.env.S3_BUCKET || "mintel";
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
const response = await client.send(command);
if (response.Body) {
const dir = path.dirname(localPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const stream = fs.createWriteStream(localPath);
const reader = response.Body as any;
// Node.js stream handling
if (typeof reader.pipe === "function") {
reader.pipe(stream);
} else {
// Alternative for web streams if necessary, but in Node it should have pipe
const arr = await response.Body.transformToByteArray();
fs.writeFileSync(localPath, arr);
}
return new Promise((resolve, reject) => {
stream.on("finish", resolve);
stream.on("error", reject);
});
}
} catch (err) {
console.error(`Failed to download ${key}:`, err);
}
}
function parseMatter(content: string) {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return { data: {}, content };
const data: Record<string, any> = {};
match[1].split("\n").forEach((line) => {
const [key, ...rest] = line.split(":");
if (key && rest.length) {
const field = key.trim();
let val = rest.join(":").trim();
data[field] = val.replace(/^["']|["']$/g, "");
}
});
return { data, content: match[2].trim() };
}
async function run() {
const webDir = path.resolve(__dirname, "..");
const contentDir = path.join(webDir, "content", "blog");
const publicDir = path.join(webDir, "public");
const prefix = `${process.env.S3_PREFIX || "mintel-me"}/media/`;
const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
for (const file of files) {
const content = fs.readFileSync(path.join(contentDir, file), "utf-8");
const { data } = parseMatter(content);
if (data.thumbnail) {
const fileName = path.basename(data.thumbnail);
const s3Key = `${prefix}${fileName}`;
const localPath = path.join(publicDir, data.thumbnail.replace(/^\//, ""));
console.log(`Downloading ${s3Key} to ${localPath}...`);
await downloadFile(s3Key, localPath);
}
}
console.log("Downloads complete.");
}
run();
-160
View File
@@ -1,160 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline/promises";
import { fileURLToPath } from "node:url";
import { createElement } from "react";
import { renderToFile } from "@react-pdf/renderer";
import { calculateTotals, initialState, PRICING } from "@mintel/pdf";
import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js";
import {
getTechDetails,
getPrinciples,
getMaintenanceDetails,
getStandardsDetails,
} from "../src/logic/content-provider.js";
const __filename = fileURLToPath(import.meta.url);
async function main() {
const args = process.argv.slice(2);
const isInteractive = args.includes("--interactive") || args.includes("-I");
const isEstimationOnly = args.includes("--estimation") || args.includes("-E");
const inputPath = args.find(
(_, i) => args[i - 1] === "--input" || args[i - 1] === "-i",
);
let state = { ...initialState };
if (inputPath) {
const rawData = fs.readFileSync(
path.resolve(process.cwd(), inputPath),
"utf8",
);
const diskState = JSON.parse(rawData);
state = { ...state, ...diskState };
}
if (isInteractive) {
state = await runWizard(state);
}
// Final confirmation of data needed for PDF
if (!state.name || !state.email) {
console.warn(
"⚠️ Missing recipient name or email. Document might look incomplete.",
);
}
const totals = calculateTotals(state, PRICING);
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
const finalOutputPath = generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Resolve assets for the PDF
const assetsDir = path.resolve(process.cwd(), "src/assets");
const headerIcon = path.join(assetsDir, "logo/Icon White Transparent.png");
const footerLogo = path.join(assetsDir, "logo/Logo Black Transparent.png");
console.log(`🚀 Generating PDF: ${finalOutputPath}`);
const estimationProps = {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo,
};
await renderToFile(
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples(),
maintenanceDetails: getMaintenanceDetails(),
standardsDetails: getStandardsDetails(),
mode: isEstimationOnly ? "estimation" : "full",
showAgbs: !isEstimationOnly, // AGBS only for full quotes
}) as any,
finalOutputPath,
);
console.log("✅ Done!");
}
async function runWizard(state: any) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("\n--- Mintel Quote Generator Wizard ---\n");
const ask = async (q: string, def?: string) => {
const answer = await rl.question(`${q}${def ? ` [${def}]` : ""}: `);
return answer || def || "";
};
const selectOne = async (
q: string,
options: { id: string; label: string }[],
) => {
console.log(`\n${q}:`);
options.forEach((opt, i) => console.log(`${i + 1}) ${opt.label}`));
const answer = await rl.question("Selection (number): ");
const idx = parseInt(answer) - 1;
return options[idx]?.id || options[0].id;
};
state.name = await ask("Recipient Name", state.name);
state.email = await ask("Recipient Email", state.email);
state.companyName = await ask("Company Name", state.companyName);
state.projectType = await selectOne("Project Type", [
{ id: "website", label: "Website" },
{ id: "web-app", label: "Web App" },
]);
if (state.projectType === "website") {
state.websiteTopic = await ask("Website Topic", state.websiteTopic);
// Simplified for now, in a real tool we'd loop through all options
}
rl.close();
return state;
}
function generateDefaultPath(state: any) {
const now = new Date();
const month = now.toISOString().slice(0, 7);
const day = now.toISOString().slice(0, 10);
// Add seconds and minutes for 100% unique names without collision
const time = now
.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
.replace(/:/g, "-");
const company = (state.companyName || state.name || "Unknown").replace(
/[^a-z0-9]/gi,
"_",
);
return path.join(
process.cwd(),
"out",
"estimations",
month,
`${day}_${time}_${company}_${state.projectType}.pdf`,
);
}
main().catch((err) => {
console.error("❌ Error:", err);
process.exit(1);
});
-91
View File
@@ -1,91 +0,0 @@
import { ThumbnailGenerator } from "@mintel/thumbnail-generator";
import * as path from "node:path";
import * as fs from "node:fs/promises";
async function run() {
const apiKey =
process.env.REPLICATE_API_TOKEN || process.env.REPLICATE_API_KEY;
if (!apiKey) {
console.error("❌ Missing REPLICATE_API_TOKEN in environment.");
process.exit(1);
}
const targetFile = process.argv[2];
if (!targetFile) {
console.error(
"❌ Usage: npx tsx scripts/generate-thumbnail.ts <file-or-topic>",
);
process.exit(1);
}
let topic = targetFile;
let filename = "thumbnail.png";
// Try to parse the topic from the MDX frontmatter if a file is provided
if (targetFile.endsWith(".mdx")) {
try {
const content = await fs.readFile(targetFile, "utf8");
const titleMatch = content.match(/title:\s*"?([^"\n]+)"?/);
topic = titleMatch ? titleMatch[1] : path.basename(targetFile, ".mdx");
filename = `${path.basename(targetFile, ".mdx")}.png`;
} catch (e) {
console.warn(
`⚠️ Could not read ${targetFile} as a file. Using literal argument as topic.`,
);
topic = targetFile;
}
}
console.log(`Generating abstract thumbnail for topic: "${topic}"`);
const generator = new ThumbnailGenerator({ replicateApiKey: apiKey });
const isRoot = process.cwd().endsWith("mintel.me");
const baseDir = isRoot
? path.join(process.cwd(), "apps", "web")
: process.cwd();
const outputPath = path.join(baseDir, "public", "blog", filename);
// Check if thumbnail already exists to avoid redundant generation
try {
await fs.access(outputPath);
console.log(`⏭️ Thumbnail already exists, skipping: ${filename}`);
return;
} catch {
// File does not exist, proceed with generation
}
const inspirationPath = path.join(
baseDir,
"public",
"blog",
"inspiration.png",
);
let hasInspiration = false;
try {
await fs.access(inspirationPath);
hasInspiration = true;
} catch {
hasInspiration = false;
}
let customPrompt = `Extremely clean, flat, abstract geometric illustration. Use the provided image prompt ONLY as a STRICT color and texture reference. You MUST generate completely new, distinct geometric shapes that directly represent the topic. Characteristics: Flat vector design, 2D only (no 3D), tech/startup/agency aesthetics, highly professional, extensive use of whitespace. No text, no chaotic lines, no humans.`;
if (topic.includes("Service") || topic.includes("Sorglos")) {
customPrompt +=
" Visually depict the concept of 'effortless automation'. Use a large, distinct, central endless loop or a floating interconnected minimal machine.";
} else if (topic.includes("Zero") || topic.includes("Overhead")) {
customPrompt +=
" Visually depict the concept of 'cutting out the middleman'. Use a massive, heavy block being sliced in half or a direct, unfiltered sharp lightning/beam connecting two points.";
}
await generator.generateImage(topic, outputPath, {
systemPrompt: customPrompt,
imagePrompt: hasInspiration ? inspirationPath : undefined,
});
}
run().catch((e) => {
console.error("❌ Thumbnail generation failed:", e);
process.exit(1);
});
-35
View File
@@ -1,35 +0,0 @@
import { AiBlogPostOrchestrator } from "@mintel/content-engine";
import { config } from "../content-engine.config.js";
async function main() {
const OPENROUTER_KEY = process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) {
console.error("❌ Error: OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.");
process.exit(1);
}
const args = process.argv.slice(2);
const targetFile = args.find(arg => !arg.startsWith('--'));
const shouldRename = args.includes('--rename');
if (!targetFile) {
console.error("❌ Usage: pnpm optimize-blog <file> [--rename]");
process.exit(1);
}
const orchestrator = new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: 'google/gemini-3-flash-preview'
});
await orchestrator.optimizeFile(targetFile, {
contextDir: config.contextDir,
availableComponents: config.components,
shouldRename
});
}
main().catch(console.error);
-61
View File
@@ -1,61 +0,0 @@
import puppeteer from 'puppeteer';
(async () => {
try {
console.log("Starting Chrome...");
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Pass any console logs from the browser to our terminal
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
console.log("Navigating to http://localhost:3000/blog/why-pagespeed-fails ...");
await page.goto('http://localhost:3000/blog/why-pagespeed-fails', { waitUntil: 'networkidle0' });
// Wait a bit just in case
await new Promise(r => setTimeout(r, 2000));
console.log("--- Inspecting Mermaid ---");
const mermaidLabels = await page.evaluate(() => {
const labels = Array.from(document.querySelectorAll('.mermaid svg text, .mermaid svg .nodeLabel'));
return labels.map(l => l.textContent).filter(Boolean);
});
console.log(`Found ${mermaidLabels.length} mermaid labels.`);
if (mermaidLabels.length > 0) {
console.log("Sample labels:", mermaidLabels.slice(0, 5));
} else {
console.log("FAIL: No SVG labels found inside Mermaid containers!");
}
console.log("\n--- Inspecting Twitter Embed ---");
const tweets = await page.evaluate(() => {
const tweetContainers = Array.from(document.querySelectorAll('.react-tweet-theme'));
return tweetContainers.map(container => ({
html: container.outerHTML.substring(0, 150) + "..."
}));
});
console.log(`Found ${tweets.length} Tweet containers.`);
if (tweets.length > 0) {
console.log("Success! Tweet container snippet:", tweets[0].html);
// Further inspection of react-tweet - it sometimes renders an error div if not found
const tweetErrors = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.react-tweet-theme [data-testid="tweet-not-found"]')).length;
});
if (tweetErrors > 0) {
console.log(`FAIL: Found ${tweetErrors} 'Tweet not found' error states inside the container.`);
}
} else {
console.log("FAIL: No react-tweet containers found on page. It might be completely crashing or skipped.");
}
await browser.close();
} catch (e) {
console.error("Script failed:", e);
}
})();