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]>
113 lines
2.7 KiB
TypeScript
113 lines
2.7 KiB
TypeScript
export const PROJECT_HEALTH_MAX_AGE_MS = 15 * 60 * 1000;
|
|
|
|
export interface ProjectHealthCheck {
|
|
status: number | null;
|
|
contentType: string | null;
|
|
checkedAt: string;
|
|
finalUrl?: string;
|
|
latencyMs?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface ProjectHealthSnapshot {
|
|
checkedAt: string;
|
|
projects: Record<string, ProjectHealthCheck>;
|
|
}
|
|
|
|
interface ProjectLike {
|
|
id: string;
|
|
featured?: boolean;
|
|
}
|
|
|
|
function isFresh(checkedAt: string, now: Date) {
|
|
const timestamp = Date.parse(checkedAt);
|
|
return (
|
|
Number.isFinite(timestamp) &&
|
|
now.getTime() - timestamp <= PROJECT_HEALTH_MAX_AGE_MS
|
|
);
|
|
}
|
|
|
|
export function isCurrentLiveCheck(
|
|
check: ProjectHealthCheck | undefined,
|
|
now = new Date(),
|
|
) {
|
|
return Boolean(
|
|
check &&
|
|
check.status === 200 &&
|
|
check.contentType?.toLowerCase().startsWith("text/html") &&
|
|
isFresh(check.checkedAt, now),
|
|
);
|
|
}
|
|
|
|
export function filterLiveProjects<T extends ProjectLike>(
|
|
projects: T[],
|
|
snapshot: ProjectHealthSnapshot | null,
|
|
now = new Date(),
|
|
) {
|
|
if (!snapshot || !isFresh(snapshot.checkedAt, now)) {
|
|
return [] as T[];
|
|
}
|
|
|
|
return projects.filter(
|
|
(project) =>
|
|
project.featured !== false &&
|
|
isCurrentLiveCheck(snapshot.projects[project.id], now),
|
|
);
|
|
}
|
|
|
|
interface ProjectDemo {
|
|
id: string;
|
|
demoUrl: string;
|
|
}
|
|
|
|
interface HealthResponse {
|
|
status: number;
|
|
url: string;
|
|
headers: { get(name: string): string | null };
|
|
body?: { cancel?: () => unknown } | null;
|
|
}
|
|
|
|
type HealthFetcher = (url: string, init: RequestInit) => Promise<HealthResponse>;
|
|
|
|
export async function createProjectHealthSnapshot(
|
|
projects: ProjectDemo[],
|
|
fetcher: HealthFetcher = (url, init) => fetch(url, init),
|
|
now = new Date(),
|
|
): Promise<ProjectHealthSnapshot> {
|
|
const checkedAt = now.toISOString();
|
|
const checks = await Promise.all(
|
|
projects.map(async (project) => {
|
|
try {
|
|
const response = await fetcher(project.demoUrl, {
|
|
redirect: "follow",
|
|
signal: AbortSignal.timeout(10_000),
|
|
headers: { "user-agent": "evandro-project-health/1.0" },
|
|
});
|
|
await response.body?.cancel?.();
|
|
|
|
return [
|
|
project.id,
|
|
{
|
|
status: response.status,
|
|
contentType: response.headers.get("content-type"),
|
|
checkedAt,
|
|
finalUrl: response.url,
|
|
},
|
|
] as const;
|
|
} catch (error) {
|
|
return [
|
|
project.id,
|
|
{
|
|
status: null,
|
|
contentType: null,
|
|
checkedAt,
|
|
error: error instanceof Error ? error.message : "Health check failed",
|
|
},
|
|
] as const;
|
|
}
|
|
}),
|
|
);
|
|
|
|
return { checkedAt, projects: Object.fromEntries(checks) };
|
|
}
|