Categorieënpagina, screenshot-hero, emoji/vlaggen, auteur-byline en publicatie-planning
- Categorie automatisch gekoppeld aan een vaste dekkleur (geen coverHue-veld meer nodig), /categorieen groepeert alle gepubliceerde reviews en categorieën zijn overal klikbaar - Screenshot verschijnt als gelinkte hero tussen specs en body i.p.v. los in de Markdown-tekst; afbeeldingen in de body breken nu uit de tekstkolom - :shortcode:-emoji en :flag_xx:-landvlaggen werken zowel in de Markdown-body als in frontmatter-tekstvelden - Zichtbare "Door Menno"-byline + auteurs-metatag, adres via src/lib/site.ts - Reviews met draft: false en een datum in de toekomst blijven volledig verborgen (ook niet als placeholder) tot na een build ná die datum; reviewpagina krijgt dan noindex - make deploy-target (build + rsync naar de server) - CGI-formulier gebruikt nu het net opgezette aitestbank.nl-maildomein i.p.v. een tijdelijk adres
This commit is contained in:
@@ -10,15 +10,22 @@
|
||||
*
|
||||
* Wat het doet:
|
||||
* 1. Screenshot van de site (Playwright) -> src/assets/reviews/<slug>/screenshot.png
|
||||
* (de reviewpagina toont deze automatisch als hero bovenaan, gelinkt naar de
|
||||
* website, zodra het bestand bestaat — zie reviews/[slug].astro)
|
||||
* 2. Titel/meta/zichtbare tekst van de site scrapen
|
||||
* 3. Claude (Haiku) laten samenvatten tot titel/bedrijfsnaam/categorie/doelgroep/prijsindicatie
|
||||
* 3. Claude (Haiku) laten samenvatten tot titel/bedrijfsnaam/categorie (één woord,
|
||||
* bepaalt ook de coverkleur en de sectie op /categorieen)/doelgroep/prijsindicatie
|
||||
* — met expliciete instructie om niets te verzinnen, onbekend = leeg veld
|
||||
* 4. Nieuw src/content/reviews/<slug>.md wegschrijven vanuit _template.md, met die velden ingevuld
|
||||
* 4. Claude laat ook een concepttekst schrijven voor de secties "Voor wie",
|
||||
* "Wie zit erachter" en "Prijs" (gebaseerd op de site-tekst, gemarkeerd
|
||||
* als AI-concept)
|
||||
* 5. Nieuw src/content/reviews/<slug>.md wegschrijven vanuit _template.md, met die velden ingevuld
|
||||
*
|
||||
* Wat het BEWUST NIET automatisch invult:
|
||||
* score, verdict, hands-on indruk — dat is jouw eigen testervaring na het zelf proberen,
|
||||
* niet iets wat een script (of AI die de website leest) kan beoordelen.
|
||||
* Alle AI-ingevulde velden krijgen een "# AI-concept, controleren!"-comment: check ze na.
|
||||
* score, verdict, "Wat is het" en "Hands-on indruk" — dat is jouw eigen testervaring
|
||||
* na het zelf proberen, niet iets wat een script (of AI die de website leest) kan
|
||||
* beoordelen. Alle AI-ingevulde velden/secties krijgen een "AI-concept, controleren!"-
|
||||
* markering: check ze na.
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
@@ -46,6 +53,15 @@ function fillField(template, field, value) {
|
||||
return template.replace(re, `${field}: ${JSON.stringify(value)} # AI-concept, controleren!`);
|
||||
}
|
||||
|
||||
function fillSection(template, heading, placeholderComment, prose) {
|
||||
// Zelfde idee als fillField, maar dan voor een hele body-sectie: als er niets
|
||||
// gevonden is, blijft de originele hint-comment gewoon staan voor handmatig invullen.
|
||||
if (!prose) return template;
|
||||
const search = `## ${heading}\n\n<!-- ${placeholderComment} -->`;
|
||||
const replacement = `## ${heading}\n\n${prose}\n\n<!-- AI-concept, controleren! -->`;
|
||||
return template.replace(search, replacement);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [, , urlArg, slugArg] = process.argv;
|
||||
if (!urlArg) {
|
||||
@@ -111,7 +127,7 @@ async function main() {
|
||||
|
||||
const extraction = await anthropic.messages.create({
|
||||
model: "claude-haiku-4-5-20251001",
|
||||
max_tokens: 1024,
|
||||
max_tokens: 2048,
|
||||
tools: [
|
||||
{
|
||||
name: "record_tool_info",
|
||||
@@ -130,7 +146,8 @@ async function main() {
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
description: "Korte Nederlandse categorie, bv. 'Klantenservice', 'Boekhouding', 'Marketing'",
|
||||
description:
|
||||
"Categorie in ÉÉN Nederlands woord (bepaalt ook de sectie op de categorieënpagina), bv. 'Klantenservice', 'Boekhouding', 'Marketing', 'Juridisch'. Geen meerwoordige omschrijvingen.",
|
||||
},
|
||||
for_who: { type: "string", description: "Korte typering van de doelgroep, gebaseerd op de site-tekst" },
|
||||
price_from: {
|
||||
@@ -143,8 +160,34 @@ async function main() {
|
||||
description:
|
||||
"Korte typering van hosting/privacy ALLEEN als expliciet vermeld (bv. 'EU, GDPR-verklaring'), anders lege string",
|
||||
},
|
||||
for_who_prose: {
|
||||
type: "string",
|
||||
description:
|
||||
"1-3 zinnen Nederlandse lopende tekst voor de sectie 'Voor wie': welk type gebruiker/bedrijf dit relevant vindt, en voor wie juist niet. Gebaseerd op de site-tekst, niet verzinnen. Lege string als er te weinig te zeggen valt.",
|
||||
},
|
||||
company_info_prose: {
|
||||
type: "string",
|
||||
description:
|
||||
"1-3 zinnen Nederlandse lopende tekst voor de sectie 'Wie zit erachter': team/locatie/funding/omzet ALLEEN als dat ergens op de site staat. Lege string als er niets over te vinden is (niet gokken of aanvullen met algemene kennis).",
|
||||
},
|
||||
price_prose: {
|
||||
type: "string",
|
||||
description:
|
||||
"1-3 zinnen Nederlandse lopende tekst voor de sectie 'Prijs': iets uitgebreider dan een los bedrag, bv. tiers, gratis laag, wat er wel/niet in zit — ALLEEN gebaseerd op wat er echt op de site staat. Lege string als er geen prijsinformatie te vinden is.",
|
||||
},
|
||||
},
|
||||
required: ["title", "company", "location", "category", "for_who", "price_from", "hosting"],
|
||||
required: [
|
||||
"title",
|
||||
"company",
|
||||
"location",
|
||||
"category",
|
||||
"for_who",
|
||||
"price_from",
|
||||
"hosting",
|
||||
"for_who_prose",
|
||||
"company_info_prose",
|
||||
"price_prose",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -153,8 +196,12 @@ async function main() {
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Hier is de inhoud van een bedrijfswebsite. Haal er alleen op wat er ECHT letterlijk staat, " +
|
||||
"verzin nooit iets. Onbekende/niet-vermelde velden: lege string, niet gokken.\n\n" +
|
||||
"Hier is de inhoud van een bedrijfswebsite, voor een conceptreview op een AI-tool-blog. " +
|
||||
"Haal er alleen op wat er ECHT letterlijk staat, verzin nooit feiten (prijzen, locatie, " +
|
||||
"team). Onbekende/niet-vermelde velden: lege string, niet gokken. Voor de lopende-tekst-" +
|
||||
"velden (for_who_prose/company_info_prose/price_prose) mag je de site-tekst wel in eigen " +
|
||||
"woorden samenvatten, in de rustige/nuchtere toon van de rest van de site — maar blijf bij " +
|
||||
"wat de site zelf claimt, voeg geen eigen mening of hands-on-oordeel toe.\n\n" +
|
||||
`Paginatitel: ${pageTitle}\n` +
|
||||
`Meta-omschrijving: ${metaDescription ?? ""}\n\n` +
|
||||
`Zichtbare tekst van de pagina:\n${bodyText}`,
|
||||
@@ -179,10 +226,17 @@ async function main() {
|
||||
// template zet ze op respectievelijk "" en 0 als leesbaar voorbeeld voor de
|
||||
// handmatige workflow, maar allebei zijn dat *ongeldige* waarden voor het
|
||||
// Zod-schema (date: "" faalt de date-coercion, score: 0 faalt min(1)) — dus
|
||||
// hier de regels helemaal weghalen, zodat het veld echt afwezig is en de
|
||||
// .optional() in het schema z'n werk kan doen.
|
||||
content = content.replace(/^date: ""\s*#.*\n/m, "");
|
||||
content = content.replace(/^score: 0\s*#.*\n/m, "");
|
||||
// hier uitcommentariëren i.p.v. laten staan, zodat het veld echt afwezig is
|
||||
// (de .optional() in het schema kan er dan mee uit de voeten) maar er wél
|
||||
// zichtbaar blijft staan HOE je 'm moet invullen zodra je gaat publiceren.
|
||||
content = content.replace(
|
||||
/^date: ""(\s*#.*)?$/m,
|
||||
"# date: 2026-01-01 # invullen zodra je draft: false zet",
|
||||
);
|
||||
content = content.replace(
|
||||
/^score: 0(\s*#.*)?$/m,
|
||||
"# score: 3 # geheel getal 1-5 (aantal robots) — invullen vóórdat je draft: false zet",
|
||||
);
|
||||
|
||||
content = content.replace(/^website: ""(\s*#.*)?$/m, `website: ${JSON.stringify(url)}`);
|
||||
content = fillField(content, "title", info.title);
|
||||
@@ -193,16 +247,33 @@ async function main() {
|
||||
content = fillField(content, "price_from", info.price_from);
|
||||
content = fillField(content, "hosting", info.hosting);
|
||||
|
||||
const companyForAlt = info.company || slug;
|
||||
content = content.replace(
|
||||
"## Wat is het\n\n<!--",
|
||||
`## Wat is het\n\n\n\n<!--`,
|
||||
content = fillSection(
|
||||
content,
|
||||
"Voor wie",
|
||||
"Voor welk type gebruiker/bedrijf is dit relevant, en voor wie juist niet.",
|
||||
info.for_who_prose,
|
||||
);
|
||||
content = fillSection(
|
||||
content,
|
||||
"Wie zit erachter",
|
||||
"Team, locatie, funding/omzet indien bekend. Onbekend? Dat gewoon zo benoemen.",
|
||||
info.company_info_prose,
|
||||
);
|
||||
content = fillSection(
|
||||
content,
|
||||
"Prijs",
|
||||
"Prijsmodel iets uitgebreider dan het specs-balkje: tiers, gratis laag, addertjes onder het gras.",
|
||||
info.price_prose,
|
||||
);
|
||||
|
||||
// Geen screenshot-markdown meer in de body: de reviewpagina toont 'm zelf al
|
||||
// automatisch als hero bovenaan (gelinkt naar de website) zodra
|
||||
// src/assets/reviews/<slug>/screenshot.png bestaat, zie reviews/[slug].astro.
|
||||
|
||||
await writeFile(contentPath, content, "utf-8");
|
||||
console.error(`Concept aangemaakt: ${path.relative(projectRoot, contentPath)}`);
|
||||
console.error(
|
||||
"Nog met de hand te doen: elk 'AI-concept, controleren!'-veld naast lezen, en zelf score/verdict/hands-on indruk schrijven na het testen.",
|
||||
"Nog met de hand te doen: elk 'AI-concept, controleren!'-veld/sectie naast lezen, en zelf 'Wat is het', score, verdict en hands-on indruk schrijven na het testen.",
|
||||
);
|
||||
|
||||
// enige stdout-regel, bedoeld om door scripts (bv. de Makefile) opgepakt te worden
|
||||
|
||||
Reference in New Issue
Block a user