← Terug naar tutorials

SEO Metadata Generator (NL) – Titel, Beschrijving, Slug & Tags

seometadatameta descriptionpaginatitelurl-slugzoekwoordencontentmarketingnederlandstalig

SEO Metadata Generator (NL) – Titel, Beschrijving, Slug & Tags

Een goede pagina kan inhoudelijk uitstekend zijn en toch nauwelijks verkeer krijgen, simpelweg omdat de SEO-metadata niet optimaal is. Metadata is de “verpakking” waarmee zoekmachines (en gebruikers in de zoekresultaten) begrijpen waar je pagina over gaat. In deze tutorial bouw je een SEO Metadata Generator die voor Nederlandstalige content automatisch:

Je krijgt diepe uitleg, plus echte commands om alles lokaal te draaien. We gebruiken Node.js en een simpele CLI, zodat je het kunt integreren in je workflow (CMS, Markdown-site, e-commerce, etc.).


Inhoudsopgave

  1. Wat zijn SEO-metadata en waarom zijn ze belangrijk?
  2. SEO-titel: regels, valkuilen en best practices
  3. Meta description: hoe schrijf je een goede beschrijving?
  4. Slug: leesbaar, stabiel en zoekmachinevriendelijk
  5. Tags/keywords: nuttig, maar met nuance
  6. Ontwerp van de generator: input, output en kwaliteitsregels
  7. Project opzetten (Node.js CLI)
  8. Implementatie: titel, description, slug en tags genereren
  9. Kwaliteitschecks: lengte, duplicaten, stopwoorden, CTR-signalen
  10. Gebruik in de praktijk: voorbeelden en commands
  11. Integratie in een content-pipeline (Markdown, Git, CI)
  12. Veelgemaakte fouten en hoe je ze voorkomt
  13. Uitbreidingen: meervoudige varianten, SERP-preview, interne linking hints

Wat zijn SEO-metadata en waarom zijn ze belangrijk?

SEO-metadata zijn velden die niet (altijd) prominent zichtbaar zijn op de pagina zelf, maar wel door zoekmachines en sociale platforms worden gebruikt om je pagina te begrijpen en te presenteren.

De belangrijkste velden:

Waarom een generator?


SEO-titel: regels, valkuilen en best practices

Doel van de SEO-titel

De SEO-titel moet tegelijk:

  1. Relevantie signaleren (zoekmachine begrijpt onderwerp),
  2. Klikken uitlokken (gebruiker kiest jouw resultaat),
  3. Merk/vertrouwen uitstralen (optioneel je merknaam).

Lengte: pixels vs. karakters

Google toont titels op basis van pixelbreedte, niet strikt op karakters. Toch kun je in tooling vaak met een richtlijn werken:

Structuren die vaak werken

Valkuilen


Meta description: hoe schrijf je een goede beschrijving?

Wat doet de meta description?

De meta description is primair een marketingtekst in de SERP. Google kan soms een andere snippet tonen, maar een goede description helpt vaak bij:

Lengte

Inhoudelijke checklist

Een sterke description bevat meestal:

Voorbeeld:

Genereer automatisch SEO-titels, meta descriptions, slugs en tags in het Nederlands. Inclusief kwaliteitschecks en CLI-commands. Start direct.

Valkuilen


Slug: leesbaar, stabiel en zoekmachinevriendelijk

Een slug is het pad in de URL, vaak gebaseerd op de titel.

Regels voor goede slugs

Voorbeeld:


Tags/keywords: nuttig, maar met nuance

“Keywords” als meta tag zijn voor Google al jaren irrelevant. Maar tags zijn nog steeds nuttig voor:

Goede tags

Voorbeeldtags:


Ontwerp van de generator: input, output en kwaliteitsregels

Input

We willen minimaal:

Voor CLI is JSON handig, of een tekstbestand.

Output

We genereren een JSON-resultaat:

{
  "seoTitle": "...",
  "metaDescription": "...",
  "slug": "...",
  "tags": ["...", "..."],
  "checks": {
    "titleLength": 58,
    "descriptionLength": 154,
    "hasCallToAction": true
  }
}

Kwaliteitsregels (heuristieken)


Project opzetten (Node.js CLI)

Vereisten

Controleer:

node -v
npm -v

Nieuwe map en package

mkdir seo-metadata-generator-nl
cd seo-metadata-generator-nl
npm init -y

Dependencies installeren

We gebruiken:

Installeer basis:

npm install commander slugify stopword

Maak een src map:

mkdir src
touch src/index.js

Voeg een bin entry toe aan package.json zodat je het als command kunt draaien:

Open package.json en voeg toe:

"bin": {
  "seo-meta-nl": "src/index.js"
}

Maak src/index.js executable (macOS/Linux):

chmod +x src/index.js

Implementatie: titel, description, slug en tags genereren

Plak onderstaande code in src/index.js.

Let op: dit is bewust “plain Node.js” zonder TypeScript, zodat je het direct kunt draaien. Je kunt later altijd migreren.

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { Command } from "commander";
import slugify from "slugify";
import { removeStopwords, nld } from "stopword";

const program = new Command();

function readInput({ file, text }) {
  if (file) {
    const p = path.resolve(process.cwd(), file);
    const raw = fs.readFileSync(p, "utf8");
    // Ondersteun JSON of plain text
    try {
      return JSON.parse(raw);
    } catch {
      return { title: "", content: raw };
    }
  }
  if (text) return { title: "", content: text };
  // stdin fallback
  const stdin = fs.readFileSync(0, "utf8");
  try {
    return JSON.parse(stdin);
  } catch {
    return { title: "", content: stdin };
  }
}

function normalizeWhitespace(s) {
  return (s || "").replace(/\s+/g, " ").trim();
}

function truncateAtWordBoundary(text, maxLen) {
  const t = normalizeWhitespace(text);
  if (t.length <= maxLen) return t;
  const cut = t.slice(0, maxLen + 1);
  const lastSpace = cut.lastIndexOf(" ");
  if (lastSpace < 0) return t.slice(0, maxLen);
  return cut.slice(0, lastSpace).trim();
}

function makeSeoTitle({ title, content, brand, maxLen = 60 }) {
  const base = normalizeWhitespace(title) || normalizeWhitespace(content).slice(0, 80);
  // Heuristiek: voeg (NL) toe als het nuttig is, en brand achteraan
  let candidate = base;

  // Als titel erg lang is, probeer te comprimeren door stukjes na ":" of "–" te beperken
  if (candidate.length > maxLen) {
    const parts = candidate.split(/[:–-]\s+/);
    if (parts.length > 1) {
      candidate = parts[0] + ": " + parts[1];
    }
  }

  // Brand achteraan (met scheidingsteken)
  if (brand) {
    const withBrand = `${candidate} | ${brand}`;
    candidate = withBrand.length <= maxLen + 10 ? withBrand : candidate; // niet forceren
  }

  candidate = truncateAtWordBoundary(candidate, maxLen);
  return candidate;
}

function makeMetaDescription({ title, content, maxLen = 155 }) {
  const t = normalizeWhitespace(title);
  const c = normalizeWhitespace(content);

  // Heuristiek: bouw een description met onderwerp + voordeel + CTA
  // We nemen een eerste zin uit content als basis, maar zorgen voor een “marketing” vorm.
  const firstSentenceMatch = c.match(/^(.+?[.!?])\s/);
  const firstSentence = firstSentenceMatch ? firstSentenceMatch[1] : c;

  let desc = "";
  if (t) {
    desc = `${t}: ${firstSentence}`;
  } else {
    desc = firstSentence;
  }

  // Voeg CTA toe als er ruimte is
  const cta = " Ontdek de stappen en voorbeelden.";
  if ((desc + cta).length <= maxLen) desc += cta;

  desc = truncateAtWordBoundary(desc, maxLen);

  // Verwijder dubbele punt aan het einde, rare resten
  desc = desc.replace(/[:–-]\s*$/, "").trim();
  return desc;
}

function makeSlug({ title, content, maxWords = 10 }) {
  const base = normalizeWhitespace(title) || normalizeWhitespace(content).slice(0, 120);
  // slugify met NL-friendly instellingen
  let s = slugify(base, {
    lower: true,
    strict: true, // verwijdert speciale tekens
    locale: "nl"
  });

  // Stopwoorden verwijderen (optioneel)
  const words = s.split("-").filter(Boolean);
  const filtered = removeStopwords(words, nld);

  const finalWords = (filtered.length ? filtered : words).slice(0, maxWords);
  s = finalWords.join("-").replace(/-+/g, "-").replace(/^-|-$/g, "");
  return s || "pagina";
}

function extractTags({ title, content, maxTags = 10 }) {
  const text = normalizeWhitespace(`${title} ${content}`).toLowerCase();

  // Simpele tokenization: letters/cijfers, splits op niet-woord
  const tokens = text
    .split(/[^a-z0-9à-ÿ]+/i)
    .map(t => t.trim())
    .filter(t => t.length >= 3);

  // Stopwoorden eruit
  const filtered = removeStopwords(tokens, nld);

  // Frequentietelling
  const freq = new Map();
  for (const tok of filtered) {
    freq.set(tok, (freq.get(tok) || 0) + 1);
  }

  // Boost woorden uit titel
  const titleTokens = removeStopwords(
    normalizeWhitespace(title).toLowerCase().split(/[^a-z0-9à-ÿ]+/i).filter(t => t.length >= 3),
    nld
  );
  for (const tok of titleTokens) {
    freq.set(tok, (freq.get(tok) || 0) + 2);
  }

  // Sorteer op score
  const sorted = [...freq.entries()]
    .sort((a, b) => b[1] - a[1])
    .map(([w]) => w);

  // Maak tags iets “menselijker”: combineer veelvoorkomende bigrams
  // (Heel simpele bigram aanpak)
  const words = filtered;
  const bigramFreq = new Map();
  for (let i = 0; i < words.length - 1; i++) {
    const a = words[i], b = words[i + 1];
    if (!a || !b) continue;
    const bg = `${a} ${b}`;
    bigramFreq.set(bg, (bigramFreq.get(bg) || 0) + 1);
  }
  const bigrams = [...bigramFreq.entries()]
    .filter(([, n]) => n >= 2)
    .sort((a, b) => b[1] - a[1])
    .map(([bg]) => bg);

  const combined = [];
  for (const bg of bigrams) {
    combined.push(bg);
    if (combined.length >= Math.floor(maxTags / 2)) break;
  }

  // Vul aan met single-word tags
  for (const w of sorted) {
    if (combined.length >= maxTags) break;
    if (combined.some(t => t.includes(w))) continue;
    combined.push(w);
  }

  // Uniek + maxTags
  const unique = [...new Set(combined)].slice(0, maxTags);
  return unique;
}

function runChecks({ seoTitle, metaDescription, slug, tags }) {
  const checks = {
    titleLength: seoTitle.length,
    descriptionLength: metaDescription.length,
    slugLength: slug.length,
    tagsCount: tags.length,
    hasCallToAction: /ontdek|lees|bekijk|download|probeer/i.test(metaDescription),
    hasBrandSeparator: /\s[|]\s/.test(seoTitle),
    looksSpammyTitle: /(seo\s*){3,}/i.test(seoTitle)
  };
  return checks;
}

program
  .name("seo-meta-nl")
  .description("Genereer SEO-titel, meta description, slug en tags (NL).")
  .option("-f, --file <path>", "Inputbestand (JSON of tekst)")
  .option("-t, --text <text>", "Inputtekst (als alternatief)")
  .option("--title <title>", "Titel/H1")
  .option("--brand <brand>", "Merknaam (optioneel)")
  .option("--max-title <n>", "Max lengte titel", v => parseInt(v, 10), 60)
  .option("--max-desc <n>", "Max lengte description", v => parseInt(v, 10), 155)
  .option("--max-tags <n>", "Max aantal tags", v => parseInt(v, 10), 10)
  .option("--json", "Output als JSON", true);

program.parse(process.argv);
const opts = program.opts();

const input = readInput({ file: opts.file, text: opts.text });
const title = normalizeWhitespace(opts.title ?? input.title ?? "");
const content = normalizeWhitespace(input.content ?? "");

if (!title && !content) {
  console.error("Geen input gevonden. Gebruik --file, --text, stdin of --title + content.");
  process.exit(1);
}

const seoTitle = makeSeoTitle({
  title,
  content,
  brand: opts.brand,
  maxLen: opts.maxTitle
});

const metaDescription = makeMetaDescription({
  title,
  content,
  maxLen: opts.maxDesc
});

const slug = makeSlug({ title, content });
const tags = extractTags({ title, content, maxTags: opts.maxTags });

const result = {
  seoTitle,
  metaDescription,
  slug,
  tags,
  checks: runChecks({ seoTitle, metaDescription, slug, tags })
};

process.stdout.write(JSON.stringify(result, null, 2) + "\n");

Belangrijke keuzes in de code (uitleg)


Kwaliteitschecks: lengte, duplicaten, stopwoorden, CTR-signalen

De checks in de output zijn minimale heuristieken. In echte omgevingen wil je vaak extra checks:

1) Duplicatiecontrole (site-breed)

Een generator kan per pagina mooi werk leveren, maar je wil voorkomen dat 200 pagina’s dezelfde titel/description krijgen. Dat vraagt om een index van bestaande metadata.

Praktisch idee:

Voorbeeld command om JSONL te maken:

seo-meta-nl --file input.json >> metadata.jsonl

En later duplicaten zoeken (Linux/macOS):

cat metadata.jsonl | jq -r '.seoTitle' | sort | uniq -c | sort -nr | head

2) CTR-signalen

CTR is niet alleen “clickbait”; het gaat om duidelijke waarde:

Je kunt een check toevoegen die kijkt of description minstens één concreet woord bevat, bijvoorbeeld:

3) Stopwoorden en leesbaarheid

Stopwoorden verwijderen uit slugs is vaak goed, maar soms wordt de slug te “telegramstijl”. Overweeg:


Gebruik in de praktijk: voorbeelden en commands

In je projectmap:

npm link

Nu is seo-meta-nl beschikbaar.

Test met tekst:

seo-meta-nl --title "SEO Metadata Generator (NL) – Titel, Beschrijving, Slug & Tags" \
  --text "In deze tutorial bouw je een generator die automatisch SEO-titels, meta descriptions, slugs en tags maakt. Inclusief kwaliteitschecks en praktische CLI-commands." \
  --brand "JouwSite" \
  --max-title 60 \
  --max-desc 155 \
  --max-tags 10

2) Input via JSON-bestand

Maak input.json:

{
  "title": "Meta description schrijven: voorbeelden en checklist",
  "content": "Een goede meta description verhoogt je klikratio in Google. In deze gids leer je hoe je beschrijvingen schrijft die duidelijk, concreet en aantrekkelijk zijn, inclusief voorbeelden en valkuilen."
}

Run:

seo-meta-nl --file input.json

3) Input via stdin

cat input.json | seo-meta-nl

Of plain text:

echo "Dit is een artikel over slugs en hoe je ze SEO-vriendelijk maakt." | seo-meta-nl --title "SEO-vriendelijke slugs maken"

Integratie in een content-pipeline (Markdown, Git, CI)

Veel teams schrijven content in Markdown. Je kunt metadata genereren tijdens build of pre-commit.

Scenario: Markdown-bestanden in content/

Stel je hebt:

Je kunt een script maken dat per bestand:

  1. titel uit eerste # haalt,
  2. content samenvat (bijv. eerste 1–2 alinea’s),
  3. generator draait,
  4. output opslaat in metadata/.

Snelle aanpak met shell + node

Installeer ripgrep (rg) als je het nog niet hebt (macOS met Homebrew):

brew install ripgrep

Voor Ubuntu/Debian:

sudo apt-get update
sudo apt-get install -y ripgrep

Voorbeeld: titel uit Markdown halen:

rg -m 1 '^#\s+' content/artikel-1.md

Je kunt dit combineren in een klein script, maar dat is een uitbreiding. Het kernidee: metadata is een artefact dat je naast content kunt versioneren.

CI-check: faal build als title te lang is

Je kunt checks.titleLength gebruiken om builds te breken.

Voorbeeld (conceptueel) met jq:

seo-meta-nl --file input.json | jq '.checks.titleLength'

En in bash:

LEN=$(seo-meta-nl --file input.json | jq -r '.checks.titleLength')
if [ "$LEN" -gt 60 ]; then
  echo "Titel te lang: $LEN"
  exit 1
fi

Veelgemaakte fouten en hoe je ze voorkomt

Fout 1: één template voor alles

Als elke description eindigt met “Lees meer.” zonder inhoud, voelt het generiek. Zorg dat je generator:

Fout 2: slugs blijven veranderen

Slugs zijn onderdeel van je URL. Wijzigingen betekenen redirects, verlies van “netheid” en risico op 404’s. Oplossing:

Fout 3: tags als dumpplaats

Te veel tags maakt je site rommelig. Houd het beperkt en consistent.

Fout 4: title tag is te “branding-heavy”

Merknaam | Pagina | Categorie | Jaar is vaak te lang en duwt het onderwerp weg. Zet het onderwerp vooraan, merk optioneel achteraan.


Uitbreidingen: meervoudige varianten, SERP-preview, interne linking hints

Als je generator werkt, zijn dit logische uitbreidingen.

1) Meerdere varianten (A/B)

Je kunt bijvoorbeeld 3 titels genereren:

In code: laat makeSeoTitle meerdere kandidaten maken en kies op score (lengte, aanwezigheid zoekwoord, etc.).

2) SERP-preview in terminal

Je kunt een “preview” printen:

SEO Metadata Generator (NL) – Titel... | JouwSite
www.jouwsite.nl/seo-metadata-generator-nl...
Genereer automatisch SEO-titels...

Dat helpt redacties om snel te beoordelen of het “klikt”.

3) Interne linking hints

Op basis van tags kun je contentclusters bouwen:


Afronding: wat je nu hebt

Je hebt een werkende Nederlandstalige SEO Metadata Generator als CLI-tool, met:

Als je wilt, kan ik ook: