HTML
Basisdocument
Start elke pagina met een geldige structuur.
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Paginatitel</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Welkom</h1>
<script src="app.js" defer></script>
</body>
</html>
Let op: gebruik defer zodat JavaScript pas na de HTML wordt uitgevoerd.
HTML
Tekst en lijsten
<h1>Hoofdtitel</h1>
<h2>Tussentitel</h2>
<p>Tekst met <strong>belang</strong> en <em>nadruk</em>.</p>
<ul>
<li>Eerste item</li>
<li>Tweede item</li>
</ul>
Let op: kies kopniveaus op betekenis, niet op lettergrootte.
HTML
Links en afbeeldingen
<a href="contact.html">Contact</a>
<a href="https://example.com" target="_blank" rel="noopener">Website</a>
<figure>
<img src="images/team.jpg" alt="Het WebCraft-team">
<figcaption>Samen bouwen.</figcaption>
</figure>
Let op: schrijf een nuttige alt-tekst; gebruik alt="" voor puur decoratieve beelden.
HTML
Semantische structuur
<header>…</header>
<nav aria-label="Hoofdnavigatie">…</nav>
<main>
<section>
<h2>Onderwerp</h2>
<article>…</article>
</section>
</main>
<footer>…</footer>
Let op: er is normaal maar één zichtbaar hoofdgebied met <main>.
HTML
Tabel
<table>
<caption>Resultaten</caption>
<thead><tr><th scope="col">Naam</th><th scope="col">Score</th></tr></thead>
<tbody><tr><td>Noor</td><td>18/20</td></tr></tbody>
</table>
Let op: gebruik tabellen voor gegevens, nooit om een pagina op te maken.
HTML
Formulier
<form id="contact-form">
<label for="email">E-mail</label>
<input id="email" name="email" type="email" required>
<label for="bericht">Bericht</label>
<textarea id="bericht" name="bericht"></textarea>
<button type="submit">Versturen</button>
</form>
Let op: de for van een label moet gelijk zijn aan het id van het veld.
CSS
Selectors
p { color: #172033; } /* element */
.card { padding: 1rem; } /* class */
#menu { display: flex; } /* id */
.card h2 { margin: 0; } /* binnen card */
.button:hover { translate: 0 -2px; }
input:focus-visible { outline: 3px solid #d6006d; }
Let op: geef styling meestal met classes; vermijd onnodig specifieke selectors.
CSS
Boxmodel
* { box-sizing: border-box; }
.card {
width: min(100%, 32rem);
margin: 1.5rem auto;
padding: 1.25rem;
border: 1px solid #e3e5ea;
border-radius: 1rem;
}
Let op: verticale marges kunnen samenvallen; gebruik in layouts vaak gap.
CSS
Flexbox
.container {
display: flex;
align-items: center; /* dwarsas */
justify-content: space-between; /* hoofdas */
gap: 1rem;
flex-wrap: wrap;
}
.item { flex: 1 1 15rem; }
Gebruik voor: één rij of kolom en het uitlijnen van onderdelen.
CSS
CSS Grid
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.25rem;
}
.breed { grid-column: 1 / -1; }
Gebruik voor: layouts met rijen én kolommen.
CSS
Responsive ontwerp
img { max-width: 100%; height: auto; }
h1 { font-size: clamp(2rem, 6vw, 4.5rem); }
@media (max-width: 48rem) {
.layout { grid-template-columns: 1fr; }
}
Let op: test ook tussen de vaste schermbreedtes, niet alleen op één telefoon.
CSS
Variabelen
:root {
--brand: #d6006d;
--ink: #172033;
--space: 1rem;
}
.button {
color: white;
background: var(--brand);
padding: calc(var(--space) * .75) var(--space);
}
Let op: geef variabelen namen op basis van hun functie.
JS
Variabelen en strings
const naam = 'Alex';
let score = 0;
const geslaagd = true;
score += 1;
const melding = `Hallo ${naam}, je score is ${score}.`;
Let op: gebruik standaard const; alleen let wanneer de waarde opnieuw wordt toegewezen.
JS
Voorwaarden
if (score >= 10 && aanwezig) {
console.log('Geslaagd');
} else {
console.log('Nog even oefenen');
}
const label = gebruikersnaam ?? 'Gast';
Let op: vergelijk strikt met === en !==.
JS
Functies
const berekenTotaal = (prijs, aantal = 1) => {
return prijs * aantal;
};
const totaal = berekenTotaal(8.5, 3);
const verdubbel = getal => getal * 2;
Let op: laat een functie één duidelijke taak uitvoeren.
JS
Arrays en objecten
const leerling = { naam: 'Noor', score: 16 };
const { naam, score } = leerling;
const scores = [8, 12, 16, 19];
const geslaagd = scores.filter(score => score >= 10);
const dubbel = scores.map(score => score * 2);
const eersteTopscore = scores.find(score => score >= 16);
Let op: map maakt een nieuwe array; forEach niet.
JS
DOM aanpassen
const titel = document.querySelector('h1');
const kaarten = document.querySelectorAll('.card');
titel.textContent = 'Nieuwe titel';
titel.classList.add('accent');
titel.setAttribute('aria-live', 'polite');
kaarten.forEach(kaart => kaart.classList.toggle('actief'));
Let op: controleer of een element bestaat of gebruik ?. wanneer dat niet zeker is.
JS
Events en formulieren
const formulier = document.querySelector('#contact-form');
formulier?.addEventListener('submit', event => {
event.preventDefault();
const data = new FormData(formulier);
const email = data.get('email')?.trim();
console.log(email);
});
Let op: schrijf 'click' zonder on en geef de functienaam zonder haakjes door.
JS
Debuggen
console.log('Waarde:', waarde);
console.table(leerlingen);
console.log(typeof invoer);
try {
voerActieUit();
} catch (error) {
console.error(error.message);
}
Werkwijze: reproduceer → lees de melding → test één hypothese → controleer opnieuw.
JS
API’s en fetch
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const postResponse = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ country: 'Belgium' })
});
Let op: voorzie loading, error en result; zet nooit een geheime sleutel in publieke frontendcode.
BS
Bootstrap koppelen
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" defer></script>
Let op: eigen CSS komt na Bootstrap.
BS
Responsive grid
<div class="container">
<div class="row g-4">
<article class="col-12 col-md-6 col-lg-4">…</article>
</div>
</div>
Patroon: container → row → col. Begin mobiel met col-12.
BS
Utilities
py-5 /* verticale padding */
mb-3 /* margin onderaan */
d-flex /* Flexbox */
gap-3 /* ruimte tussen items */
d-none d-md-block /* tonen vanaf md */
Leesregel: eigenschap + zijde + breakpoint + waarde.
BS
Navbar en collapse
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#menu"
aria-controls="menu" aria-expanded="false">…</button>
<div class="collapse navbar-collapse" id="menu">…</div>
Debug: target en id moeten exact gelijk zijn; de bundle moet geladen zijn.
BS
Formulieren
<label class="form-label" for="email">E-mail</label>
<input class="form-control" id="email" name="email"
type="email" required>
<div class="invalid-feedback">Vul een geldig e-mailadres in.</div>
Let op: styling vervangt correcte labels en inputtypes niet.
BS
Componenten
btn btn-primary
card h-100
badge text-bg-dark
accordion
modal fade
offcanvas offcanvas-start
alert alert-info
Kies bewust: gebruik een component alleen wanneer het echt helpt.
PHP
PHP 8-basis
<?php
declare(strict_types=1);
const APP_NAME = 'ProjectHub';
$title = 'Mijn project';
$tags = ['PHP', 'MySQL'];
function e(string|int|null $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
?>
<h1><?= e($title) ?></h1>
Let op: escape dynamische waarden bij HTML-output, niet wanneer je ze in de database bewaart.
PHP
Eerst data, dan HTML
Gebruik de MVC-denkwijze zonder een strikt framework.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/bootstrap.php';
// 1. Request/controller
$filter = trim($_GET['filter'] ?? '');
// 2. Model/repository
$items = findItems(database(), $filter);
// 3. Viewdata
$pageTitle = 'Overzicht';
$itemCount = count($items);
// 4. View
require __DIR__ . '/../views/items/index.php';
WebCraft-afspraak: de view toont voorbereide variabelen en start geen query's.
PHP
Request valideren en redirecten
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
if (!is_int($id) || $id < 1) {
http_response_code(400);
exit('Ongeldige id.');
}
header('Location: detail.php?id=' . $id, true, 303);
exit;
Let op: browservalidatie helpt de gebruiker; de server valideert altijd opnieuw.
PDO
PDO-verbinding
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=projecthub;charset=utf8mb4';
$pdo = new PDO($dsn, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
Let op: bewaar lokale credentials buiten public en buiten Git.
PDO
Prepared statement
$statement = $pdo->prepare(
'SELECT id, title FROM projects WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
$project = $statement->fetch();
WebCraft-afspraak: prepare → expliciet binden met PDO-type → execute zonder argumenten.
CRUD
Write en soft delete
// Create
$statement = $pdo->prepare(
'INSERT INTO projects (title) VALUES (:title)'
);
$statement->bindValue(':title', $title, PDO::PARAM_STR);
$statement->execute();
$id = (int) $pdo->lastInsertId();
// Archive
$statement = $pdo->prepare(
'UPDATE projects SET active = FALSE WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
Let op: wijzig gegevens via POST en gebruik na succes een 303-redirect.
PDO
Transactie
$pdo->beginTransaction();
try {
// Meerdere afhankelijke writes
$pdo->commit();
} catch (Throwable $error) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
Gebruik voor: één gebruikersactie die alleen geldig is wanneer alle writes slagen.
PDO
Veilig sorteren en pagineren
$allowedSorts = ['title', 'created_at'];
$sort = in_array($requestedSort, $allowedSorts, true)
? $requestedSort
: 'created_at';
$statement = $pdo->prepare(
"SELECT id, title FROM projects
ORDER BY $sort DESC LIMIT :limit OFFSET :offset"
);
$statement->bindValue(':limit', $limit, PDO::PARAM_INT);
$statement->bindValue(':offset', $offset, PDO::PARAM_INT);
$statement->execute();
Let op: bind waarden; controleer SQL-structuur zoals kolomnamen met een allowlist.
AUTH
Veilige login en sessie
session_set_cookie_params([
'httponly' => true,
'secure' => true,
'samesite' => 'Lax',
]);
session_start();
if (password_verify($password, $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = (int) $user['id'];
}
Let op: gebruik password_hash() en password_verify(); bewaar nooit platte wachtwoorden.
CSRF
CSRF-token
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// In het formulier
<input type="hidden" name="csrf"
value="<?= e($_SESSION['csrf']) ?>">
// Bij elke wijzigende POST-request
$token = $_POST['csrf'] ?? '';
if (!hash_equals($_SESSION['csrf'], $token)) {
http_response_code(403);
exit('Ongeldige aanvraag.');
}
Let op: een CSRF-token vervangt geen autorisatiecontrole.
GUARD
Autorisatie op de server
function requireRole(string ...$allowed): void
{
$role = $_SESSION['role'] ?? null;
if (!is_string($role) || !in_array($role, $allowed, true)) {
http_response_code(403);
exit('Geen toegang.');
}
}
requireRole('admin', 'teacher');
Let op: een verborgen knop is geen beveiliging; controleer elke route server-side.
RELEASE
Productiecheck
□ display_errors uit, logging aan
□ unieke productiegeheimen buiten public en Git
□ debugroutes en testaccounts verwijderd
□ migraties en smoke tests uitgevoerd
□ database én uploads geback-upt
□ restore in aparte omgeving getest
□ 403, 404 en 500 geven veilige feedback
Bewijs: noteer test, resultaat, datum en verantwoordelijke bij elke releasecheck.