first commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Plugin, ResolvedConfig } from "vite";
|
||||
import type { RecipesIndex } from "@ben-to/recipes/catalog";
|
||||
import {
|
||||
buildRecipeOgHeadModel,
|
||||
forEachRecipeSharePair,
|
||||
injectRecipeSeoIntoIndexHtml
|
||||
} from "@ben-to/recipes/recipe-og-meta";
|
||||
|
||||
const DEFAULT_SITE_ORIGIN = "https://ben-to.fr";
|
||||
|
||||
/**
|
||||
* Pré-rendu des pages recette pour les métadonnées Open Graph / Twitter (HTML initial).
|
||||
* - **Dev** : middleware qui réinjecte le `<head>` pour les URLs `/r/:base/:variant`.
|
||||
* - **Build** : écrit `dist/r/<base>/<variant>/index.html` pour chaque couple catalogue.
|
||||
*/
|
||||
export function recipeSeoPlugin(): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: "ben-to-recipe-seo",
|
||||
configResolved(resolved) {
|
||||
config = resolved;
|
||||
},
|
||||
configureServer(server) {
|
||||
const root = server.config.root;
|
||||
const port = server.config.server.port ?? 3000;
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.method !== "GET" || !req.url) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const pathname = new URL(req.url, "http://localhost").pathname.replace(/\/$/, "");
|
||||
const match = /^\/r\/([^/]+)\/([^/]+)$/.exec(pathname);
|
||||
if (!match) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const baseId = decodeURIComponent(match[1]);
|
||||
const variantId = decodeURIComponent(match[2]);
|
||||
const catalogPath = path.join(root, "public/recipes/catalog.json");
|
||||
const indexPath = path.join(root, "index.html");
|
||||
const [catalogRaw, rawIndexHtml] = await Promise.all([
|
||||
fs.readFile(catalogPath, "utf8"),
|
||||
fs.readFile(indexPath, "utf8")
|
||||
]);
|
||||
// On passe par le pipeline de transformation HTML de Vite (injecte la CSP
|
||||
// et tout autre plugin qui utilise `transformIndexHtml`) avant d'injecter
|
||||
// les balises OG/Twitter recette.
|
||||
const indexHtml = await server.transformIndexHtml(req.url ?? pathname, rawIndexHtml);
|
||||
const catalog = JSON.parse(catalogRaw) as RecipesIndex;
|
||||
const base = catalog.bases.find((b) => b.id === baseId);
|
||||
const variant = catalog.variants.find((v) => v.id === variantId);
|
||||
if (!base || !variant || !variant.baseIds.includes(baseId)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const devOrigin = process.env.BEN_TO_SITE_ORIGIN ?? `http://localhost:${port}`;
|
||||
const model = buildRecipeOgHeadModel({
|
||||
siteOrigin: devOrigin,
|
||||
base,
|
||||
variant,
|
||||
baseId,
|
||||
variantId
|
||||
});
|
||||
const html = injectRecipeSeoIntoIndexHtml(indexHtml, model);
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(html);
|
||||
} catch {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
async closeBundle() {
|
||||
const outDir = path.resolve(config.root, config.build.outDir);
|
||||
const indexPath = path.join(outDir, "index.html");
|
||||
const catalogPath = path.join(outDir, "recipes/catalog.json");
|
||||
const siteOrigin = process.env.BEN_TO_SITE_ORIGIN ?? DEFAULT_SITE_ORIGIN;
|
||||
const [indexHtml, catalogRaw] = await Promise.all([
|
||||
fs.readFile(indexPath, "utf8"),
|
||||
fs.readFile(catalogPath, "utf8")
|
||||
]);
|
||||
const catalog = JSON.parse(catalogRaw) as RecipesIndex;
|
||||
const tasks: Promise<void>[] = [];
|
||||
forEachRecipeSharePair(catalog, ({ base, variant, baseId, variantId }) => {
|
||||
tasks.push(
|
||||
(async () => {
|
||||
const model = buildRecipeOgHeadModel({
|
||||
siteOrigin,
|
||||
base,
|
||||
variant,
|
||||
baseId,
|
||||
variantId
|
||||
});
|
||||
const html = injectRecipeSeoIntoIndexHtml(indexHtml, model);
|
||||
const dir = path.join(outDir, "r", encodeURIComponent(baseId), encodeURIComponent(variantId));
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(path.join(dir, "index.html"), html, "utf8");
|
||||
})()
|
||||
);
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
config.logger.info(`recipe-seo: ${tasks.length} page(s) OG générée(s) sous dist/r/`);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user