Changeset 837fc9c in Klonkt
- Timestamp:
- 06/27/2026 07:51:44 AM (2 months ago)
- Branches:
- main
- Children:
- 92f6976
- Parents:
- aa45208
- Location:
- src
- Files:
-
- 12 edited
-
assets/css/style.css (modified) (1 diff)
-
config/database.js (modified) (1 diff)
-
routes/activitypub.js (modified) (2 diffs)
-
routes/posts.js (modified) (8 diffs)
-
services/ActivityPubService.js (modified) (2 diffs)
-
services/Scheduler.js (modified) (2 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/post-edit.ejs (modified) (1 diff)
-
views/pages/post.ejs (modified) (1 diff)
-
views/partials/post-card.ejs (modified) (1 diff)
-
views/partials/post-tile.ejs (modified) (2 diffs)
-
views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
raa45208 r837fc9c 4539 4539 .fedi-remote-form .fedi-remote-cancel { background: none; border: none; color: var(--ink-soft, #888); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: .25rem .35rem; } 4540 4540 .fedi-remote-form .fedi-remote-cancel:hover { color: var(--ink, #000); } 4541 4542 /* ── NSFW / sensitive content — blur + click to reveal ───────────────── */ 4543 .nsfw-media { position: relative; } 4544 .nsfw-veil { 4545 position: absolute; inset: 0; z-index: 4; cursor: pointer; border-radius: inherit; 4546 display: flex; flex-direction: column; align-items: center; justify-content: center; 4547 gap: .5rem; padding: .75rem; text-align: center; 4548 background: color-mix(in srgb, var(--paper, #14141a) 50%, transparent); 4549 backdrop-filter: blur(22px) saturate(.6); -webkit-backdrop-filter: blur(22px) saturate(.6); 4550 } 4551 .nsfw-media.is-shown > .nsfw-veil { display: none; } 4552 .nsfw-veil-label { font-weight: 700; font-size: .82rem; color: var(--ink, #f3f1ea); text-shadow: 0 1px 2px rgba(0,0,0,.5); } 4553 .nsfw-veil-btn { 4554 font-size: .72rem; font-weight: 600; padding: .3rem .8rem; border-radius: 999px; 4555 border: 1px solid color-mix(in srgb, var(--ink, #fff) 55%, transparent); 4556 background: color-mix(in srgb, var(--paper, #000) 35%, transparent); color: var(--ink, #f3f1ea); 4557 } 4558 /* Post page: blur cover + body behind a content-warning banner until revealed */ 4559 .nsfw-gate .post-cover, 4560 .nsfw-gate .post-content { filter: blur(26px); pointer-events: none; user-select: none; transition: filter .2s ease; } 4561 .nsfw-gate.is-shown .post-cover, 4562 .nsfw-gate.is-shown .post-content { filter: none; pointer-events: auto; user-select: auto; } 4563 .nsfw-banner { 4564 display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin: 0 0 1.5rem; 4565 padding: .8rem 1rem; border-radius: 12px; 4566 border: 1px solid color-mix(in srgb, var(--accent, #e8b04b) 45%, transparent); 4567 background: color-mix(in srgb, var(--accent, #e8b04b) 9%, transparent); 4568 } 4569 .nsfw-gate.is-shown .nsfw-banner { display: none; } 4570 .nsfw-banner-icon { font-size: 1.2rem; } 4571 .nsfw-banner-text { font-weight: 600; font-size: .92rem; color: var(--ink); } 4572 .nsfw-banner-btn { 4573 margin-left: auto; cursor: pointer; font: inherit; font-size: .82rem; font-weight: 600; 4574 padding: .4rem 1rem; border-radius: 999px; border: 0; 4575 background: var(--accent, #e8b04b); color: #1a1205; 4576 } -
src/config/database.js
raa45208 r837fc9c 84 84 ensureColumn('posts', 'publish_at', 'DATETIME'); // release planning (premium #3): scheduled go-live 85 85 ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0'); // fan-only preview (premium #3) 86 ensureColumn('posts', 'nsfw', 'INTEGER DEFAULT 0'); // sensitive content → blur + click-to-reveal; fediverse sensitive 86 87 ensureColumn('posts', 'type', "TEXT DEFAULT 'post'"); // post | foto | video | audio 87 88 -
src/routes/activitypub.js
raa45208 r837fc9c 67 67 if (!site) return res.status(404).end(); 68 68 const posts = db.prepare( 69 `SELECT id, slug, title, content, cover_image_url, published_at, created_at69 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at 70 70 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 71 71 ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20` … … 90 90 // rank 1 last) → Mastodon flips it back to pin-rank ascending on the profile. 91 91 const posts = db.prepare( 92 `SELECT id, slug, title, content, cover_image_url, published_at, created_at92 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at 93 93 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 94 94 AND pinned IS NOT NULL AND pinned > 0 -
src/routes/posts.js
raa45208 r837fc9c 176 176 const { title, slug, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body; 177 177 const fanOnly = req.body.fan_only ? 1 : 0; 178 const nsfw = req.body.nsfw ? 1 : 0; 178 179 179 180 // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize … … 213 214 INSERT INTO posts ( 214 215 id, site_id, slug, author_id, title, content, excerpt, 215 status, cover_image_url, pinned, tags, type, noindex, fan_only, publish_at,216 status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, publish_at, 216 217 created_at, updated_at, published_at 217 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )218 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 218 219 `).run( 219 220 postId, site.id, finalSlug, req.session.user.id, … … 221 222 finalStatus, cover_image_url || null, parsePinnedRank(pinned), 222 223 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 223 finalType, noindex ? 1 : 0, fanOnly, publishAt,224 finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt, 224 225 now, now, publishedAt 225 226 ); … … 238 239 id: postId, slug: finalSlug, title: title || finalSlug, 239 240 content: cleanContent, cover_image_url: cover_image_url || null, 240 published_at: publishedAt, created_at: now, fan_only: fanOnly, 241 published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, 241 242 }).catch(() => { /* best-effort */ }); 242 243 } … … 296 297 const { title, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body; 297 298 const fanOnly = req.body.fan_only ? 1 : 0; 299 const nsfw = req.body.nsfw ? 1 : 0; 298 300 const newSlug = req.body.slug; 299 301 const action = req.body.action || 'save'; … … 334 336 title = ?, content = ?, excerpt = ?, status = ?, 335 337 cover_image_url = ?, pinned = ?, tags = ?, 336 type = ?, noindex = ?, fan_only = ?, publish_at = ?,338 type = ?, noindex = ?, fan_only = ?, nsfw = ?, publish_at = ?, 337 339 slug = ?, published_at = ?, updated_at = ? 338 340 WHERE id = ? … … 341 343 cover_image_url || null, parsePinnedRank(pinned), 342 344 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 343 finalType, noindex ? 1 : 0, fanOnly, publishAt,345 finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt, 344 346 finalSlug, publishedAt, now, post.id 345 347 ); … … 362 364 id: post.id, slug: finalSlug, title: title || finalSlug, 363 365 content: cleanContent, cover_image_url: cover_image_url || null, 364 published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, 366 published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, 365 367 }; 366 368 if (post.status !== 'published') ActivityPubService.deliverCreate(site, apPost).catch(() => { /* best-effort */ }); -
src/services/ActivityPubService.js
raa45208 r837fc9c 246 246 tag: Array.isArray(post.tags) ? post.tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/\s+/g, '') })) : [], 247 247 replies: `${id}/replies`, 248 // NSFW → Mastodon-style content warning: sensitive (blurs media) + a summary/spoiler 249 // (hides the whole post behind a "Gevoelige inhoud" button until the reader opens it). 250 sensitive: !!post.nsfw, 248 251 }; 252 if (post.nsfw) note.summary = 'Gevoelige inhoud'; 249 253 if (attachment.length) note.attachment = attachment; 250 254 // Playable-audio posts suppress the cover attachment (player card). Still expose … … 771 775 if (!site) return; 772 776 const recent = db.prepare( 773 `SELECT id, slug, title, content, cover_image_url, published_at, created_at777 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at 774 778 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 775 779 ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20` -
src/services/Scheduler.js
raa45208 r837fc9c 15 15 try { 16 16 const due = db.prepare(` 17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, 17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw, 18 18 p.published_at, p.publish_at, p.created_at, u.username 19 19 FROM posts p JOIN users u ON u.id = p.author_id … … 39 39 id: p.id, slug: p.slug, title: p.title || p.slug, 40 40 content: p.content, cover_image_url: p.cover_image_url || null, 41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, 41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, 42 42 }).catch(() => { /* best-effort */ }); 43 43 } -
src/services/i18n.js
raa45208 r837fc9c 771 771 'pedit.pin_top': 'bovenaan', 772 772 'pedit.pin_nth_suffix': 'e van boven', 773 'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 773 'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 'pedit.nsfw_label': 'NSFW / gevoelige inhoud', 'post.nsfw_warning': 'Gevoelige inhoud', 'post.nsfw_show': 'Tonen', 774 774 'pedit.fan_only_label': 'Alleen voor ingelogde fans', 775 775 'pedit.schedule_label': 'Publicatie inplannen', … … 1782 1782 'pedit.pin_top': 'at the top', 1783 1783 'pedit.pin_nth_suffix': 'th from top', 1784 'pedit.noindex_label': 'noindex (hide from search engines)', 1784 'pedit.noindex_label': 'noindex (hide from search engines)', 'pedit.nsfw_label': 'NSFW / sensitive content', 'post.nsfw_warning': 'Sensitive content', 'post.nsfw_show': 'Show', 1785 1785 'pedit.fan_only_label': 'Logged-in fans only', 1786 1786 'pedit.schedule_label': 'Schedule publication', … … 2793 2793 'pedit.pin_top': 'ganz oben', 2794 2794 'pedit.pin_nth_suffix': '. von oben', 2795 'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 2795 'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 'pedit.nsfw_label': 'NSFW / sensibler Inhalt', 'post.nsfw_warning': 'Sensibler Inhalt', 'post.nsfw_show': 'Anzeigen', 2796 2796 'pedit.fan_only_label': 'Nur für angemeldete Fans', 2797 2797 'pedit.schedule_label': 'Veröffentlichung planen', -
src/views/pages/post-edit.ejs
raa45208 r837fc9c 237 237 <input type="checkbox" name="noindex" value="1" <%= post.noindex ? 'checked' : '' %>> 238 238 <span>🚫 <%= t('pedit.noindex_label') %></span> 239 </label> 240 <label class="pe-checkbox"> 241 <input type="checkbox" name="nsfw" value="1" <%= post.nsfw ? 'checked' : '' %>> 242 <span>🔞 <%= t('pedit.nsfw_label') %></span> 239 243 </label> 240 244 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %> -
src/views/pages/post.ejs
raa45208 r837fc9c 5 5 const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; 6 6 %> 7 <article class="container post-page ">7 <article class="container post-page<%= post.nsfw ? ' nsfw-gate' : '' %>"> 8 8 9 9 <%# Navigation across ALL posts — ABOVE the post (shared partial). %> 10 10 <%- include('../partials/post-nav', { newerPost: newerPost, olderPost: olderPost }) %> 11 12 <% if (post.nsfw) { %> 13 <div class="nsfw-banner"> 14 <span class="nsfw-banner-icon">🔞</span> 15 <span class="nsfw-banner-text"><%= t('post.nsfw_warning') %></span> 16 <button type="button" class="nsfw-banner-btn nsfw-reveal"><%= t('post.nsfw_show') %></button> 17 </div> 18 <% } %> 11 19 12 20 <% if (post.cover_image_url) { %> -
src/views/partials/post-card.ejs
raa45208 r837fc9c 46 46 47 47 <% if (_hasCover) { %> 48 <a class="post-list-cover " href="<%= _href %>"48 <a class="post-list-cover<%= post.nsfw ? ' nsfw-media' : '' %>" href="<%= _href %>" 49 49 <% if (_ext) { %>target="_blank" rel="noopener"<% } else { %>hx-get="<%= _href %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="<%= _href %>" hx-indicator="#pcms-loading"<% } %> 50 50 aria-label="<%= post.title || '(zonder titel)' %>" tabindex="-1"> 51 51 <img src="<%= post.cover_image_url %>" alt="" loading="lazy" decoding="async"> 52 <% if (post.nsfw) { %><span class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></span><% } %> 52 53 </a> 53 54 <% } %> -
src/views/partials/post-tile.ejs
raa45208 r837fc9c 14 14 const _ext = !!_external; 15 15 %> 16 <a class="grid-tile<%= _hasCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %> "16 <a class="grid-tile<%= _hasCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %><%= post.nsfw ? ' nsfw-media' : '' %>" 17 17 href="<%= _href %>" 18 18 <% if (_hasCover) { %>style="background-image: url('<%= post.cover_image_url %>'); --tile-hue: <%= _tileHue %>;"<% } else { %>style="--tile-hue: <%= _tileHue %>;"<% } %> … … 37 37 <% if (_src) { %><span class="grid-tile-gradient-src">van <%= _src %></span><% } %> 38 38 <% } %> 39 <% if (post.nsfw) { %><div class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></div><% } %> 39 40 </a> -
src/views/shell.ejs
raa45208 r837fc9c 198 198 199 199 <!-- v9 stylesheet (full palette system) --> 200 <link rel="stylesheet" href="/assets/css/style.css?v=5 5">200 <link rel="stylesheet" href="/assets/css/style.css?v=56"> 201 201 <script> 202 202 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 725 725 </script> 726 726 727 <!-- NSFW / sensitive content: click a veil/reveal to un-blur. Capture-phase so the 728 click reveals instead of following the card link or firing htmx navigation. --> 729 <script> 730 (function () { 731 if (window.__nsfwWired) return; window.__nsfwWired = true; 732 document.addEventListener('click', function (e) { 733 var hit = e.target.closest && e.target.closest('.nsfw-veil, .nsfw-reveal'); 734 if (!hit) return; 735 e.preventDefault(); e.stopPropagation(); 736 var box = hit.closest('.nsfw-media, .nsfw-gate'); 737 if (box) box.classList.add('is-shown'); 738 }, true); 739 })(); 740 </script> 741 727 742 <!-- Per-site custom footer HTML --> 728 743 <% if (safeSite.custom_foot_html) { %>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)