dotrino-content 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/public.js +31 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotrino-content",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Nodo de contenido del ecosistema Dotrino: guarda los bytes del usuario direccionados por su hash (cid), en su propia maquina y, si el dueno quiere, respaldados en su propio bucket (R2, Backblaze, Hetzner, Storj o S3). Lo publico sale por URL directa; lo privado, cifrado y solo por la red Dotrino.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/public.js CHANGED
@@ -148,6 +148,18 @@ function baseUrl (req, configured) {
148
148
  return `${proto}://${host}`
149
149
  }
150
150
 
151
+ /** El host de una URL, o '' si no se puede leer. */
152
+ const hostOf = (u) => { try { return new URL(u).hostname } catch { return '' } }
153
+
154
+ /** Un enlace legible: se cae el `https://` y la cola larga, no el destino. */
155
+ function shortLink (u) {
156
+ try {
157
+ const url = new URL(u)
158
+ const tail = (url.pathname === '/' ? '' : url.pathname) + url.search
159
+ return url.hostname.replace(/^www\./, '') + (tail.length > 40 ? tail.slice(0, 39) + '…' : tail)
160
+ } catch { return u }
161
+ }
162
+
151
163
  const parseMeta = (row) => {
152
164
  if (!row?.meta) return {}
153
165
  try { return JSON.parse(row.meta) || {} } catch { return {} }
@@ -157,6 +169,12 @@ const parseMeta = (row) => {
157
169
  * Página del permalink (§7.3). Es una tarjeta y un enlace, y se dice así: no
158
170
  * pretende ser un visor. Lleva la imagen solo si el propio blob es una imagen o
159
171
  * si tiene una miniatura pública enlazada (`thumbnailCid`).
172
+ *
173
+ * El texto se pinta ENTERO: recortarlo aquí dejaba tarjetas cortadas a mitad de
174
+ * frase, y quien decide cuánto texto hay es quien lo publicó, no esta página.
175
+ * Si el contenido trae enlaces (`meta.links` — en un eco, la fuente de la que
176
+ * habla), se pintan: sin ellos la tarjeta cuenta la noticia y esconde de dónde
177
+ * salió.
160
178
  */
161
179
  export function previewHtml ({ cid, row, base, appUrl, owner, index, imageCid = null, imageFallback = null }) {
162
180
  const m = parseMeta(row)
@@ -171,6 +189,11 @@ export function previewHtml ({ cid, row, base, appUrl, owner, index, imageCid =
171
189
  // El enlace de "abrir" lleva la referencia en el #fragment: el servidor de la
172
190
  // app nunca la ve (CLAUDE.md §SEO). Sin `owner` no hay referencia que armar.
173
191
  const open = owner ? `${appUrl.replace(/\/+$/, '')}/#${owner}/${cid}` : null
192
+ // Solo http(s): `meta` lo escribe quien publica, y un `javascript:` en un href
193
+ // sería un agujero abierto de par en par.
194
+ const links = (Array.isArray(m.links) ? m.links : [])
195
+ .map((u) => String(u)).filter((u) => /^https?:\/\//i.test(u)).slice(0, 4)
196
+ const openLabel = `Abrir en ${hostOf(appUrl) || 'la app'}`
174
197
  return `<!doctype html>
175
198
  <html lang="es">
176
199
  <head>
@@ -198,9 +221,12 @@ ${image ? `<meta name="twitter:image" content="${esc(image)}">` : ''}
198
221
  main { max-width: 34rem; text-align: center; }
199
222
  img { max-width: 100%; height: auto; border-radius: .75rem; }
200
223
  h1 { font-size: 1.25rem; margin: 1rem 0 .25rem; }
201
- p { color: #9aa7b4; margin: .25rem 0 1.25rem; }
202
- a.open { display: inline-block; background: #2f6bff; color: #fff; text-decoration: none;
203
- padding: .7rem 1.4rem; border-radius: .6rem; font-weight: 600; }
224
+ p { color: #9aa7b4; margin: .25rem 0 1.25rem; text-align: left; }
225
+ ul.links { list-style: none; margin: 0 0 1.25rem; padding: 0; text-align: left;
226
+ overflow-wrap: anywhere; }
227
+ ul.links li { margin: .2rem 0; }
228
+ a { color: #6ea0ff; }
229
+ a.open { font-weight: 600; }
204
230
  small { display: block; margin-top: 1.5rem; color: #6b7787; }
205
231
  small a { color: #6b7787; }
206
232
  </style>
@@ -210,7 +236,8 @@ ${image ? `<meta name="twitter:image" content="${esc(image)}">` : ''}
210
236
  ${image ? `<img src="${esc(image)}" alt="${esc(title)}">` : ''}
211
237
  <h1>${esc(title)}</h1>
212
238
  <p>${esc(desc)}</p>
213
- ${open ? `<a class="open" href="${esc(open)}">Abrir</a>` : ''}
239
+ ${links.length ? `<ul class="links">${links.map((u) => `<li><a href="${esc(u)}" rel="noopener nofollow">${esc(shortLink(u))}</a></li>`).join('')}</ul>` : ''}
240
+ ${open ? `<a class="open" href="${esc(open)}">${esc(openLabel)}</a>` : ''}
214
241
  <small>Servido desde el node de su dueño · <a href="https://content.dotrino.com/">Dotrino</a></small>
215
242
  </main>
216
243
  </body>