Index: CHANGELOG.md
===================================================================
--- CHANGELOG.md	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ CHANGELOG.md	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -13,4 +13,9 @@
 
 ## [Unreleased]
+
+### Toegevoegd
+- Posts liken / favorieten: ingelogde gebruikers kunnen een post liken met een ♥-knop
+  (toont het aantal likes). Je gelikete posts staan op de nieuwe **Favorieten**-pagina
+  (`/favorieten`, link in het accountmenu). Niet-ingelogd → de knop leidt naar de login.
 
 ### Gewijzigd
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ src/config/database.js	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -256,4 +256,18 @@
     );
   `);
+
+  // Likes / favorieten: een ingelogde gebruiker kan een post liken. De set van
+  // posts die een gebruiker likte = z'n favorieten (/favorieten-pagina). Eén rij
+  // per (post, user); uniek zodat liken idempotent is.
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS post_likes (
+      post_id TEXT NOT NULL,
+      user_id TEXT NOT NULL,
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      PRIMARY KEY (post_id, user_id)
+    );
+    CREATE INDEX IF NOT EXISTS idx_post_likes_user ON post_likes(user_id, created_at);
+    CREATE INDEX IF NOT EXISTS idx_post_likes_post ON post_likes(post_id);
+  `);
 }
 
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ src/routes/posts.js	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -5,4 +5,5 @@
 import { fileURLToPath } from 'url';
 import multer from 'multer';
+import ejs from 'ejs';
 import db from '../config/database.js';
 import { requireAuth } from '../middleware/auth.js';
@@ -63,5 +64,5 @@
   'archive', 'search', 'account', 'sites', 'comments',
   'posts', 'media', 'audio', 'prutter', 'forum',
-  'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'feed.xml', 'atom.xml', 'sitemap.xml',
+  'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml',
   'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
 ]);
@@ -392,4 +393,56 @@
     bodyClass: 'on-archive',
   });
+});
+
+// Pad naar de like-knop-partial (voor de htmx-toggle re-render).
+const LIKE_PARTIAL = path.join(__dirname, '..', 'views', 'partials', 'like-button.ejs');
+
+// ==================== LIKE / FAVORIET ====================
+// Een ingelogde gebruiker (geen kijker — de globale guard blokkeert non-GET voor
+// kijkers) togglet een like op een gepubliceerde post. Geeft de her-gerenderde
+// knop terug (htmx outerHTML-swap).
+router.post('/posts/:id/like', requireAuth, (req, res) => {
+  const userId = req.session.user.id;
+  const post = db.prepare('SELECT id, status FROM posts WHERE id = ?').get(req.params.id);
+  if (!post) return res.status(404).send('Post niet gevonden');
+  if (post.status !== 'published') return res.status(403).send('Niet beschikbaar');
+
+  const exists = db.prepare('SELECT 1 FROM post_likes WHERE post_id = ? AND user_id = ?').get(post.id, userId);
+  if (exists) {
+    db.prepare('DELETE FROM post_likes WHERE post_id = ? AND user_id = ?').run(post.id, userId);
+  } else {
+    db.prepare('INSERT OR IGNORE INTO post_likes (post_id, user_id) VALUES (?, ?)').run(post.id, userId);
+  }
+  const likeCount = db.prepare('SELECT COUNT(*) AS c FROM post_likes WHERE post_id = ?').get(post.id).c;
+
+  const html = ejs.render(fs.readFileSync(LIKE_PARTIAL, 'utf8'), {
+    post: { id: post.id }, likedByMe: !exists, likeCount, loggedIn: true, loginNext: '/',
+  });
+  res.send(html);
+});
+
+// Favorieten = de posts die de ingelogde gebruiker likete. Solo: binnen de
+// huidige site. Hub: over alle sites (met juiste /user/<slug>-links).
+router.get('/favorieten', requireAuth, (req, res) => {
+  const userId = req.session.user.id;
+  const isHub = res.locals.tenancy === 'hub';
+  const site = res.locals.site;
+  const rows = isHub
+    ? db.prepare(`
+        SELECT p.id, p.slug, p.title, p.excerpt, p.cover_image_url, p.published_at,
+               p.tags, p.type, p.pinned, p.status, s.slug AS site_slug
+        FROM post_likes pl JOIN posts p ON p.id = pl.post_id JOIN sites s ON s.id = p.site_id
+        WHERE pl.user_id = ? AND p.status = 'published'
+        ORDER BY pl.created_at DESC
+      `).all(userId)
+    : db.prepare(`
+        SELECT p.id, p.slug, p.title, p.excerpt, p.cover_image_url, p.published_at,
+               p.tags, p.type, p.pinned, p.status
+        FROM post_likes pl JOIN posts p ON p.id = pl.post_id
+        WHERE pl.user_id = ? AND p.site_id = ? AND p.status = 'published'
+        ORDER BY pl.created_at DESC
+      `).all(userId, site ? site.id : '');
+  const posts = rows.map((p) => ({ ...p, _urlBase: (isHub && p.site_slug) ? `/user/${p.site_slug}` : '' }));
+  renderPage(req, res, 'pages/favorites', { posts, pageTitle: 'Favorieten', bodyClass: 'on-favorites' });
 });
 
@@ -631,4 +684,9 @@
   relatedPosts = relatedPosts.map(({ _overlap, tags, ...rest }) => ({ ...rest, _urlBase: urlBaseFor(rest) }));
 
+  // Likes / favorieten: aantal + of de ingelogde gebruiker deze post likete.
+  const likeCount = db.prepare('SELECT COUNT(*) AS c FROM post_likes WHERE post_id = ?').get(post.id).c;
+  const likedByMe = !!(req.session?.user &&
+    db.prepare('SELECT 1 FROM post_likes WHERE post_id = ? AND user_id = ?').get(post.id, req.session.user.id));
+
   renderPage(req, res, 'pages/post', {
     post,
@@ -638,4 +696,6 @@
     comments: topLevel,
     totalComments,
+    likeCount,
+    likedByMe,
     pageTitle: post.title + ' - ' + site.title,
     socialDescr: post.excerpt || '',
Index: src/views/pages/favorites.ejs
===================================================================
--- src/views/pages/favorites.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
+++ src/views/pages/favorites.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -0,0 +1,21 @@
+<div class="container favorites-page">
+  <h1>Favorieten</h1>
+  <p class="fav-sub">Posts die je hebt geliket. Tik op het ♥ bij een post om 'm hier te bewaren.</p>
+
+  <% if (!posts || !posts.length) { %>
+    <p class="fav-empty">Je hebt nog geen favorieten. Open een post en tik op het hartje ♡.</p>
+  <% } else { %>
+    <ul class="post-list">
+      <% posts.forEach(function(p){ %>
+        <%- include('../partials/post-card', { post: p, siteUrlBase: (p._urlBase || '') }) %>
+      <% }); %>
+    </ul>
+  <% } %>
+</div>
+
+<style>
+  .favorites-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
+  .favorites-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 0.3rem; }
+  .fav-sub { color: var(--ink-muted); margin: 0 0 2rem; }
+  .fav-empty { color: var(--ink-muted); padding: 2rem 0; }
+</style>
Index: src/views/pages/post.ejs
===================================================================
--- src/views/pages/post.ejs	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ src/views/pages/post.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -31,4 +31,9 @@
       <% } %>
     </div>
+    <% if (post.status === 'published') { %>
+      <div class="post-like-row">
+        <%- include('../partials/like-button', { post: post, likedByMe: (typeof likedByMe !== 'undefined' && likedByMe), likeCount: (typeof likeCount !== 'undefined' ? likeCount : 0), loggedIn: !!user, loginNext: _base + '/' + post.slug }) %>
+      </div>
+    <% } %>
   </header>
 
@@ -250,4 +255,16 @@
 .post-content { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); }
 .post-content p { margin: 1.2em 0; }
+.post-like-row { margin-top: 1rem; }
+.like-btn {
+  display: inline-flex; align-items: center; gap: 0.45rem;
+  padding: 0.4rem 0.85rem; border: 1px solid var(--rule); border-radius: 999px;
+  background: var(--paper-2); color: var(--ink); font: inherit; font-size: 0.95rem;
+  cursor: pointer; text-decoration: none; transition: border-color 120ms, color 120ms, background 120ms;
+}
+.like-btn:hover { border-color: var(--accent); }
+.like-btn .like-heart { font-size: 1.1rem; line-height: 1; }
+.like-btn.is-liked { border-color: var(--accent); color: var(--accent); }
+.like-btn.is-liked .like-heart { color: var(--accent); }
+.like-btn .like-count { font-variant-numeric: tabular-nums; }
 .post-content h2 { font-family: var(--font-display, serif); font-size: 1.6rem; margin: 2rem 0 1rem; }
 .post-content h3 { font-family: var(--font-display, serif); font-size: 1.3rem; margin: 1.5rem 0 0.75rem; }
Index: src/views/partials/like-button.ejs
===================================================================
--- src/views/partials/like-button.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
+++ src/views/partials/like-button.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -0,0 +1,24 @@
+<%# Like-/favorietknop voor een post. Ingelogd → htmx-toggle (POST /posts/:id/like,
+    swap outerHTML met deze partial terug). Niet ingelogd → link naar login.
+    Verwacht: post (met .id), likedByMe, likeCount, loggedIn, loginNext. %>
+<%
+  var _liked = (typeof likedByMe !== 'undefined' && likedByMe);
+  var _cnt = (typeof likeCount !== 'undefined' && likeCount != null) ? likeCount : 0;
+  var _logged = (typeof loggedIn !== 'undefined' && loggedIn);
+%>
+<% if (_logged) { %>
+  <button type="button" class="like-btn<%= _liked ? ' is-liked' : '' %>"
+          hx-post="/posts/<%= post.id %>/like" hx-swap="outerHTML"
+          aria-pressed="<%= _liked ? 'true' : 'false' %>"
+          aria-label="<%= _liked ? 'Uit favorieten verwijderen' : 'Toevoegen aan favorieten' %>"
+          title="<%= _liked ? 'Uit favorieten verwijderen' : 'Toevoegen aan favorieten' %>">
+    <span class="like-heart" aria-hidden="true"><%= _liked ? '♥' : '♡' %></span>
+    <span class="like-count"><%= _cnt %></span>
+  </button>
+<% } else { %>
+  <a class="like-btn" href="/auth/login?next=<%= encodeURIComponent((typeof loginNext !== 'undefined' && loginNext) ? loginNext : '/') %>"
+     title="Log in om deze post te liken">
+    <span class="like-heart" aria-hidden="true">♡</span>
+    <span class="like-count"><%= _cnt %></span>
+  </a>
+<% } %>
Index: src/views/partials/profile-sheet.ejs
===================================================================
--- src/views/partials/profile-sheet.ejs	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ src/views/partials/profile-sheet.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -53,4 +53,13 @@
           <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
           <span class="psi-label">Account</span>
+          <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
+        </a>
+      </li>
+      <li role="none">
+        <a href="/favorieten" class="profile-sheet-item" role="menuitem" data-close-sheet
+           hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
+           hx-push-url="/favorieten" hx-indicator="#pcms-loading">
+          <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
+          <span class="psi-label">Favorieten</span>
           <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
         </a>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision 8cdb377943308d65de80427694f9692337081aa7)
+++ src/views/partials/topnav.ejs	(revision 535f955817ec83f38e0bd69aafbc344a7f7c716a)
@@ -119,4 +119,10 @@
                 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
                 <span>Account</span>
+              </a>
+              <a href="/favorieten" role="menuitem" class="udi"
+                 hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
+                 hx-push-url="/favorieten" hx-indicator="#pcms-loading">
+                <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
+                <span>Favorieten</span>
               </a>
               <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
