Changeset b9dc94c in Klonkt for src/routes/posts.js


Ignore:
Timestamp:
06/19/2026 04:28:02 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
8d32dcf
Parents:
6be57b4
Message:

Release scheduling + fan-only previews (premium feature #3)

  • DB: posts.publish_at (scheduled go-live) + posts.fan_only.
  • Release scheduling: published + future publish_at -> status 'scheduled' (excluded from public status='published' queries). Scheduler.js publishes them when the time is reached (setInterval 60s + on boot) + adds them to posts_fts. No public queries changed -> low risk.
  • Fan-only: posts.fan_only; the single-post view shows anonymous visitors a clean login gate (pages/fan-gate.ejs, link /auth/login?next=) instead of the content; logged-in fans see everything. Listings show the teaser.
  • create/save: read publish_at + fan_only; FTS excludes 'scheduled'.
  • editor (post-edit): premium-gated fields "Fans only" + "Publish on" (datetime-local) + "scheduled for" notice.
  • server.js: startScheduler() after initializeDatabase.

node --check passed.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    r6be57b4 rb9dc94c  
    154154
    155155  const { title, slug, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
     156  const fanOnly = req.body.fan_only ? 1 : 0;
    156157
    157158  // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize
     
    177178  const postId = uuid();
    178179  const now = new Date().toISOString();
    179   const finalStatus = status || 'draft';
    180   const publishedAt = finalStatus === 'published' ? now : null;
     180  let finalStatus = status || 'draft';
     181  let publishedAt = finalStatus === 'published' ? now : null;
     182  // Release-planning: gepubliceerd + een toekomstige publish_at -> 'scheduled'
     183  // (de Scheduler zet 'm live op het moment zelf). Verleden/leeg -> meteen live.
     184  let publishAt = null;
     185  const pa = Date.parse(req.body.publish_at || '');
     186  if (finalStatus === 'published' && Number.isFinite(pa) && pa > Date.now()) {
     187    finalStatus = 'scheduled';
     188    publishAt = new Date(pa).toISOString();
     189    publishedAt = null;
     190  }
    181191
    182192  db.prepare(`
    183193    INSERT INTO posts (
    184194      id, site_id, slug, author_id, title, content, excerpt,
    185       status, cover_image_url, pinned, tags, type, noindex,
     195      status, cover_image_url, pinned, tags, type, noindex, fan_only, publish_at,
    186196      created_at, updated_at, published_at
    187     ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     197    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    188198  `).run(
    189199    postId, site.id, finalSlug, req.session.user.id,
     
    191201    finalStatus, cover_image_url || null, parsePinnedRank(pinned),
    192202    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    193     finalType, noindex ? 1 : 0,
     203    finalType, noindex ? 1 : 0, fanOnly, publishAt,
    194204    now, now, publishedAt
    195205  );
     
    255265
    256266  const { title, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
     267  const fanOnly = req.body.fan_only ? 1 : 0;
    257268  const newSlug = req.body.slug;
    258269  const action = req.body.action || 'save';
     
    281292  }
    282293
     294  // Release-planning: gepubliceerd + toekomstige publish_at -> 'scheduled'.
     295  let publishAt = null;
     296  const pa = Date.parse(req.body.publish_at || '');
     297  if (finalStatus === 'published' && Number.isFinite(pa) && pa > Date.now()) {
     298    finalStatus = 'scheduled';
     299    publishAt = new Date(pa).toISOString();
     300    publishedAt = null;
     301  }
     302
    283303  db.prepare(`
    284304    UPDATE posts SET
    285305      title = ?, content = ?, excerpt = ?, status = ?,
    286306      cover_image_url = ?, pinned = ?, tags = ?,
    287       type = ?, noindex = ?,
     307      type = ?, noindex = ?, fan_only = ?, publish_at = ?,
    288308      slug = ?, published_at = ?, updated_at = ?
    289309    WHERE id = ?
     
    292312    cover_image_url || null, parsePinnedRank(pinned),
    293313    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    294     finalType, noindex ? 1 : 0,
     314    finalType, noindex ? 1 : 0, fanOnly, publishAt,
    295315    finalSlug, publishedAt, now, post.id
    296316  );
     
    393413    const canEdit = req.session?.user && PermissionsService.canEditPost(req.session.user, post, site);
    394414    if (!canEdit) return res.status(403).send('Not published');
     415  }
     416
     417  // Fan-only preview (premium #3): volledige inhoud alleen voor ingelogde fans.
     418  // Anonieme bezoekers krijgen een nette login-gate i.p.v. de inhoud (de titel/
     419  // teaser mag elders wel als lokkertje verschijnen).
     420  if (post.fan_only && !(req.session && req.session.user)) {
     421    return renderPage(req, res, 'pages/fan-gate', {
     422      pageTitle: post.title || 'Alleen voor fans',
     423      bodyClass: 'on-special',
     424      fgTitle: post.title || '',
     425      fgNext: (res.locals.siteUrlBase || '') + '/' + post.slug,
     426    });
    395427  }
    396428
Note: See TracChangeset for help on using the changeset viewer.