refonte: portfolio statique Astro (design moderne développeur)
Some checks failed
Deploy production / build-deploy (push) Failing after 56s

- Remplace l'ancien Astro hybrid + admin par un site one-page statique (sortie static)
- Thème clair/sombre sans flash, contenu typé (src/data/content.ts), zéro admin
- Icônes simple-icons (SVG monochrome), polices auto-hébergées (RGPD)
- SEO complet : canonical, Open Graph, Twitter Card, JSON-LD Person, robots.txt, sitemap.xml, image OG 1200x630
- CI Gitea de déploiement FTPS vers Plesk (.gitea/workflows/prod.yml)
This commit is contained in:
Johan LEROY
2026-06-19 16:03:09 +02:00
parent dc4e3820c4
commit ce17bf590b
189 changed files with 1836 additions and 9872 deletions

99
src/layouts/Base.astro Normal file
View File

@@ -0,0 +1,99 @@
---
import "../styles/global.css";
import { social, profile } from "../data/content";
interface Props {
title?: string;
description?: string;
}
const {
title = "Johan Leroy — Développeur fullstack",
description = "Johan Leroy, développeur fullstack basé à Nantes. TypeScript, NestJS, Nuxt/Vue, Next.js. Conception d'applications web performantes, du front à la mise en production.",
} = Astro.props;
const siteUrl = Astro.site?.href ?? "https://johanleroy.fr/";
const canonical = new URL(Astro.url.pathname, siteUrl).href;
const ogImage = new URL("/images/og.png", siteUrl).href;
const ogDescription =
"Développeur fullstack à Nantes — applications web performantes, propres et scalables.";
// Données structurées (schema.org Person) — aide les moteurs à comprendre la page.
const jsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: profile.name,
jobTitle: profile.role,
url: siteUrl,
image: ogImage,
email: social.email,
address: { "@type": "PostalAddress", addressLocality: "Nantes", addressCountry: "FR" },
sameAs: [social.github, social.linkedin, social.gitea],
knowsAbout: ["TypeScript", "NestJS", "Nuxt", "Next.js", "Vue.js", "Astro", "Docker", "AWS", "Terraform"],
};
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="author" content="Johan Leroy" />
<link rel="canonical" href={canonical} />
{/* Open Graph */}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Johan Leroy" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:title" content={title} />
<meta property="og:description" content={ogDescription} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Johan Leroy — Développeur fullstack" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:image" content={ogImage} />
<meta name="theme-color" content="#09090b" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/logo/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/logo/favicon-16x16.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/images/logo/apple-touch-icon.png" />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<script type="application/ld+json" is:inline set:html={JSON.stringify(jsonLd)} />
{/* Thème posé AVANT le premier paint : aucun flash au chargement. */}
<script is:inline>
(function () {
try {
var saved = localStorage.getItem("jl-theme");
var light = saved
? saved === "light"
: window.matchMedia("(prefers-color-scheme: light)").matches;
if (light) {
document.documentElement.setAttribute("data-theme", "light");
var m = document.querySelector('meta[name="theme-color"]');
if (m) m.setAttribute("content", "#fafafa");
}
} catch (e) {}
})();
</script>
</head>
<body>
<div class="halo" aria-hidden="true"></div>
<div class="grain" aria-hidden="true"></div>
<div class="page">
<slot />
</div>
<script>
import "../scripts/app";
</script>
</body>
</html>