Index: src/services/PostAccessService.js
===================================================================
--- src/services/PostAccessService.js	(revision b1b765257cdbf35042bd15d214253011fbbd295e)
+++ src/services/PostAccessService.js	(revision 9622f20fcfd256d8b1a7f89671b3f415d48cf12f)
@@ -69,3 +69,29 @@
 export function canReadBody(post, viewer) { return postAccess(post, viewer) === 'full'; }
 
-export default { postAccess, canReadBody, paidTeaser };
+/**
+ * Eén bericht, klaar voor de tijdlijn.
+ *
+ * DE HELE REDEN DAT DIT EEN FUNCTIE IS: het lijf wordt alleen GERENDERD als het
+ * ook getoond mag worden. Een sjabloon dat zelf `<% if (access === 'full') %>`
+ * doet krijgt de volledige tekst tóch mee in het model, en dan is het één
+ * vergeten conditie -- of één `?partial=1` die net iets anders samenstelt --
+ * tussen een betaalmuur en de tekst erachter. Hier komt hij niet eens mee.
+ *
+ * `renderBody` wordt ingespoten (posts.js levert renderPostBodyHtml), zodat
+ * deze module niets van routes of sjablonen hoeft te weten.
+ */
+export function postEntry(post, viewer, { renderBody } = {}) {
+  const access = postAccess(post, viewer);
+  const full = access === 'full';
+  return {
+    slug: post && post.slug,
+    title: (post && post.title) || '',
+    access,
+    // Alleen bij een gesloten poort een teaser, en nooit allebei: de teaser is
+    // de vervanging van het lijf, niet een voorproefje ernaast.
+    teaser: full || access === 'forbidden' ? null : paidTeaser(post),
+    content_html: full && typeof renderBody === 'function' ? renderBody(post) : null,
+  };
+}
+
+export default { postAccess, canReadBody, paidTeaser, postEntry };
