Adds dynamic case study and curriculo routes, project health monitoring for live demo status, and strengthens local SEO (structured data, service-area targeting for Garuva/Joinville/Curitiba, a dedicated "criação de sites" service page) so search results better match the professional title (Engenheiro da Computação) and experience (5 anos). Co-Authored-By: Claude Sonnet 5 <[email protected]>
69 lines
1.9 KiB
JavaScript
69 lines
1.9 KiB
JavaScript
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const sourcePath = resolve(root, "public/portfolio-data.json");
|
|
const outputPath = resolve(root, "data/project-health.json");
|
|
const timeoutMs = 10_000;
|
|
|
|
const data = JSON.parse(await readFile(sourcePath, "utf8"));
|
|
const projects = data.projects.filter((project) => project.demoUrl);
|
|
const checkedAt = new Date().toISOString();
|
|
|
|
const results = await Promise.all(
|
|
projects.map(async ({ id, demoUrl }) => {
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
const response = await fetch(demoUrl, {
|
|
redirect: "follow",
|
|
signal: AbortSignal.timeout(timeoutMs),
|
|
headers: { "user-agent": "evandro-project-health/1.0" },
|
|
});
|
|
const contentType = response.headers.get("content-type");
|
|
await response.body?.cancel();
|
|
|
|
return [
|
|
id,
|
|
{
|
|
status: response.status,
|
|
contentType,
|
|
checkedAt,
|
|
finalUrl: response.url,
|
|
latencyMs: Date.now() - startedAt,
|
|
},
|
|
];
|
|
} catch (error) {
|
|
return [
|
|
id,
|
|
{
|
|
status: null,
|
|
contentType: null,
|
|
checkedAt,
|
|
latencyMs: Date.now() - startedAt,
|
|
error:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Unknown health-check error",
|
|
},
|
|
];
|
|
}
|
|
}),
|
|
);
|
|
|
|
const snapshot = { checkedAt, projects: Object.fromEntries(results) };
|
|
const temporaryPath = `${outputPath}.${process.pid}.tmp`;
|
|
await writeFile(
|
|
temporaryPath,
|
|
`${JSON.stringify(snapshot, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
await rename(temporaryPath, outputPath);
|
|
|
|
const live = Object.values(snapshot.projects).filter(
|
|
(check) =>
|
|
check.status === 200 &&
|
|
check.contentType?.toLowerCase().startsWith("text/html"),
|
|
).length;
|
|
console.log(`Checked ${projects.length} project demos; ${live} are live.`);
|