Changeset 48027f2 in Klonkt


Ignore:
Timestamp:
06/28/2026 10:23:01 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
8308c9ca
Parents:
3487567
git-author:
Robin Genis <roboburr@…> (06/28/2026 10:21:57 PM)
git-committer:
Robin Genis <roboburr@…> (06/28/2026 10:23:01 PM)
Message:

fix(og): stop double-escaping OG/meta content + keep apostrophes literal

og:title/description etc. were rendered as <%= _e(x) %> — _e() HTML-escaped, then EJS <%= %>
escaped again (e.g. "Jason's" -> "Jason&amp;#39;s"), and the lone ' -> &#39; which naive
link-preview scrapers (Signal/WhatsApp) show literally. Now _e() is emitted raw (<%- %>) and
no longer escapes ' (safe in a double-quoted attribute).

  • views/shell.ejs — _e() drops the ' escape; all <%= _e(...) %> -> <%- _e(...) %>
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/shell.ejs

    r3487567 r48027f2  
    11<%
    22// ── Helpers used inside this template ─────────────────────────────
     3// Escape for double-quoted HTML attributes. IMPORTANT: emit this with the RAW EJS output
     4// tag, never the escaping one — escaping it a second time turned og:title "Jason's" into the
     5// double-escaped "Jason&amp;#39;s", and naive OG scrapers (Signal/WhatsApp) show that literally.
     6// We deliberately do NOT escape the apostrophe: it is safe inside a double-quoted attribute and
     7// a literal apostrophe is what link-preview scrapers expect.
    38function _e(s) {
    49  return String(s == null ? '' : s)
    510    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    6     .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
     11    .replace(/"/g, '&quot;');
    712}
    813
     
    104109}
    105110%><!DOCTYPE html>
    106 <html lang="<%= _e(lang) %>" data-palette="<%= _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
     111<html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
    107112<head>
    108113<meta charset="utf-8">
     
    111116
    112117<title><%= _finalTitle %></title>
    113 <meta name="description" content="<%= _e(_socialDescr) %>">
    114 <meta name="theme-color" content="<%= _e(safeAccent) %>">
     118<meta name="description" content="<%- _e(_socialDescr) %>">
     119<meta name="theme-color" content="<%- _e(safeAccent) %>">
    115120<meta name="robots" content="<%= _shouldIndex ? 'index,follow' : 'noindex,nofollow' %>">
    116 <% if (safeSite.author) { %><meta name="author" content="<%= _e(safeSite.author) %>"><% } %>
    117 <% if (_canonical) { %><link rel="canonical" href="<%= _e(_canonical) %>"><% } %>
     121<% if (safeSite.author) { %><meta name="author" content="<%- _e(safeSite.author) %>"><% } %>
     122<% if (_canonical) { %><link rel="canonical" href="<%- _e(_canonical) %>"><% } %>
    118123
    119124<!-- Search-engine verification -->
    120 <% if (safeSite.google_verification)    { %><meta name="google-site-verification" content="<%= _e(safeSite.google_verification) %>"><% } %>
    121 <% if (safeSite.bing_verification)      { %><meta name="msvalidate.01" content="<%= _e(safeSite.bing_verification) %>"><% } %>
    122 <% if (safeSite.pinterest_verification) { %><meta name="p:domain_verify" content="<%= _e(safeSite.pinterest_verification) %>"><% } %>
    123 <% if (safeSite.yandex_verification)    { %><meta name="yandex-verification" content="<%= _e(safeSite.yandex_verification) %>"><% } %>
     125<% if (safeSite.google_verification)    { %><meta name="google-site-verification" content="<%- _e(safeSite.google_verification) %>"><% } %>
     126<% if (safeSite.bing_verification)      { %><meta name="msvalidate.01" content="<%- _e(safeSite.bing_verification) %>"><% } %>
     127<% if (safeSite.pinterest_verification) { %><meta name="p:domain_verify" content="<%- _e(safeSite.pinterest_verification) %>"><% } %>
     128<% if (safeSite.yandex_verification)    { %><meta name="yandex-verification" content="<%- _e(safeSite.yandex_verification) %>"><% } %>
    124129
    125130<!-- Feed autodiscovery -->
    126131<% if (site) { %>
    127 <link rel="alternate" type="application/rss+xml"  title="<%= _e(_siteTitle) %> — RSS"  href="<%= _e(safeUrlBase + '/feed.xml') %>">
    128 <link rel="alternate" type="application/atom+xml" title="<%= _e(_siteTitle) %> — Atom" href="<%= _e(safeUrlBase + '/atom.xml') %>">
     132<link rel="alternate" type="application/rss+xml"  title="<%- _e(_siteTitle) %> — RSS"  href="<%- _e(safeUrlBase + '/feed.xml') %>">
     133<link rel="alternate" type="application/atom+xml" title="<%- _e(_siteTitle) %> — Atom" href="<%- _e(safeUrlBase + '/atom.xml') %>">
    129134<% } %>
    130135
    131136<!-- PWA -->
    132 <link rel="manifest" href="<%= _e(safeUrlBase + '/manifest.webmanifest') %>">
     137<link rel="manifest" href="<%- _e(safeUrlBase + '/manifest.webmanifest') %>">
    133138<meta name="mobile-web-app-capable" content="yes">
    134139<meta name="apple-mobile-web-app-capable" content="yes">
    135140<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    136 <meta name="apple-mobile-web-app-title" content="<%= _e(_siteTitle.slice(0, 16)) %>">
    137 <link rel="apple-touch-icon" href="<%= _e(safeSite.profile_photo || '/favicon.ico') %>">
     141<meta name="apple-mobile-web-app-title" content="<%- _e(_siteTitle.slice(0, 16)) %>">
     142<link rel="apple-touch-icon" href="<%- _e(safeSite.profile_photo || '/favicon.ico') %>">
    138143<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=sf">
    139144<link rel="alternate icon" href="/favicon.ico?v=sf">
     
    141146<!-- OpenGraph -->
    142147<meta property="og:type"        content="<%= _ogType %>">
    143 <meta property="og:title"       content="<%= _e(_socialTitle) %>">
    144 <meta property="og:description" content="<%= _e(_socialDescr) %>">
    145 <meta property="og:site_name"   content="<%= _e(_siteTitle) %>">
    146 <meta property="og:locale"      content="<%= _e(ogLocale) %>">
     148<meta property="og:title"       content="<%- _e(_socialTitle) %>">
     149<meta property="og:description" content="<%- _e(_socialDescr) %>">
     150<meta property="og:site_name"   content="<%- _e(_siteTitle) %>">
     151<meta property="og:locale"      content="<%- _e(ogLocale) %>">
    147152<% if (_socialImage) { %>
    148 <meta property="og:image"     content="<%= _e(_socialImage) %>">
    149 <meta property="og:image:alt" content="<%= _e(_socialTitle) %>">
     153<meta property="og:image"     content="<%- _e(_socialImage) %>">
     154<meta property="og:image:alt" content="<%- _e(_socialTitle) %>">
    150155<% if (_ogGenerated) { %>
    151156<meta property="og:image:width"  content="1200">
     
    154159<% } %>
    155160<% } %>
    156 <% if (_canonical) { %><meta property="og:url" content="<%= _e(_canonical) %>"><% } %>
     161<% if (_canonical) { %><meta property="og:url" content="<%- _e(_canonical) %>"><% } %>
    157162<%
    158163// Fediverse/social PLAYER card for posts with audio: instead of shipping the raw
     
    167172%>
    168173<% if (_postAudio) { %>
    169 <meta property="og:video"            content="<%= _e(_embedUrl) %>">
    170 <meta property="og:video:secure_url" content="<%= _e(_embedUrl) %>">
     174<meta property="og:video"            content="<%- _e(_embedUrl) %>">
     175<meta property="og:video:secure_url" content="<%- _e(_embedUrl) %>">
    171176<meta property="og:video:type"       content="text/html">
    172177<meta property="og:video:width"      content="480">
     
    174179<% } %>
    175180<% if (typeof post !== 'undefined' && post && post.published_at) { %>
    176 <meta property="article:published_time" content="<%= _e(post.published_at) %>">
    177 <% if (post.author_username) { %><meta property="article:author" content="<%= _e(post.author_username) %>"><% } %>
    178 <% } %>
    179 <% if (safeSite.facebook_app_id) { %><meta property="fb:app_id" content="<%= _e(safeSite.facebook_app_id) %>"><% } %>
     181<meta property="article:published_time" content="<%- _e(post.published_at) %>">
     182<% if (post.author_username) { %><meta property="article:author" content="<%- _e(post.author_username) %>"><% } %>
     183<% } %>
     184<% if (safeSite.facebook_app_id) { %><meta property="fb:app_id" content="<%- _e(safeSite.facebook_app_id) %>"><% } %>
    180185
    181186<!-- Twitter Cards -->
    182187<meta name="twitter:card"        content="<%= _postAudio ? 'player' : (_socialImage ? 'summary_large_image' : 'summary') %>">
    183188<% if (_postAudio) { %>
    184 <meta name="twitter:player"        content="<%= _e(_embedUrl) %>">
     189<meta name="twitter:player"        content="<%- _e(_embedUrl) %>">
    185190<meta name="twitter:player:width"  content="480">
    186191<meta name="twitter:player:height" content="480">
    187192<% } %>
    188 <meta name="twitter:title"       content="<%= _e(_socialTitle) %>">
    189 <meta name="twitter:description" content="<%= _e(_socialDescr) %>">
    190 <% if (_socialImage)         { %><meta name="twitter:image"   content="<%= _e(_socialImage) %>"><% } %>
    191 <% if (safeSite.twitter)     { %><meta name="twitter:creator" content="<%= _e(safeSite.twitter) %>"><meta name="twitter:site" content="<%= _e(safeSite.twitter) %>"><% } %>
     193<meta name="twitter:title"       content="<%- _e(_socialTitle) %>">
     194<meta name="twitter:description" content="<%- _e(_socialDescr) %>">
     195<% if (_socialImage)         { %><meta name="twitter:image"   content="<%- _e(_socialImage) %>"><% } %>
     196<% if (safeSite.twitter)     { %><meta name="twitter:creator" content="<%- _e(safeSite.twitter) %>"><meta name="twitter:site" content="<%- _e(safeSite.twitter) %>"><% } %>
    192197
    193198<% if (_jsonLd) { %>
     
    266271  :root,
    267272  [data-palette] {
    268     --accent: <%= _e(safeAccent) %>;
    269     --accent-soft: color-mix(in srgb, <%= _e(safeAccent) %> 80%, white);
    270     --accent-tint: color-mix(in srgb, <%= _e(safeAccent) %> 12%, transparent);
     273    --accent: <%- _e(safeAccent) %>;
     274    --accent-soft: color-mix(in srgb, <%- _e(safeAccent) %> 80%, white);
     275    --accent-tint: color-mix(in srgb, <%- _e(safeAccent) %> 12%, transparent);
    271276  }
    272277</style>
     
    323328</head>
    324329
    325 <body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-feed-view="<%= _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%= _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">
     330<body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-feed-view="<%- _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%- _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">
    326331
    327332<% if (typeof isViewer !== 'undefined' && isViewer) { %>
Note: See TracChangeset for help on using the changeset viewer.