Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision e9c9ae1db68ca932789d82108c91af5e8bab1058)
+++ src/routes/posts.js	(revision 7b04d3b709bae5f43a5c7f73f2d86123a46aa1d4)
@@ -883,8 +883,14 @@
 }
 
+const FEED_PAGE = 72; // divisible by 2/3/4 → every grid column count keeps full rows
 router.get('/news', requireSiteManager, (req, res) => {
   const site = res.locals.site;
+  const append = req.query.append === '1';
+  const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
   const cspOrigins = new Set();
-  const timeline = (site ? ActivityPubService.getTimeline(site.slug, 60) : []).map((p) => {
+  // Fetch one extra to know whether a "Load more" button belongs on this page.
+  const rows = site ? ActivityPubService.getTimeline(site.slug, FEED_PAGE + 1, offset) : [];
+  const hasMore = rows.length > FEED_PAGE;
+  const timeline = rows.slice(0, FEED_PAGE).map((p) => {
     let embedHtml = timelineEmbedHtml(p.content);
     let content = p.content;
@@ -910,7 +916,11 @@
     }
   }
+  const moreBase = res.locals.siteUrlBase || '';
+  if (append) {
+    return renderPage(req, res, 'partials/news-append', { timeline, hasMore, nextOffset: offset + FEED_PAGE, moreBase });
+  }
   renderPage(req, res, 'pages/news', {
     pageTitle: 'News', bodyClass: 'on-special',
-    timeline,
+    timeline, hasMore, nextOffset: offset + FEED_PAGE, moreBase,
     success: req.query.success || null, error: req.query.error || null,
   });
