dotrino-content 0.3.6 → 0.3.8
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.
- package/package.json +1 -1
- package/src/ops.js +13 -0
- 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.
|
|
3
|
+
"version": "0.3.8",
|
|
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/ops.js
CHANGED
|
@@ -47,11 +47,24 @@ const fail = (rid, code, error) => ({ rid, ok: false, code, error })
|
|
|
47
47
|
* llegue: ni campos de más ni textos sin fin.
|
|
48
48
|
* @returns {{name?:string,title?:string,description?:string}|null}
|
|
49
49
|
*/
|
|
50
|
+
/** Cuántos enlaces lleva una tarjeta, y cuánto puede medir cada uno. */
|
|
51
|
+
const MAX_LINKS = 4
|
|
52
|
+
const MAX_LINK_LEN = 300
|
|
53
|
+
|
|
50
54
|
function cleanMeta (src) {
|
|
51
55
|
if (!src || typeof src !== 'object') return null
|
|
52
56
|
const out = Object.fromEntries(['name', 'title', 'description']
|
|
53
57
|
.map((k) => [k, typeof src[k] === 'string' ? src[k].trim().slice(0, 300) : null])
|
|
54
58
|
.filter(([, v]) => v))
|
|
59
|
+
// Los ENLACES del contenido (en un eco, la fuente de la que habla). Solo http(s)
|
|
60
|
+
// y contados: `meta` lo escribe quien publica y la tarjeta lo pinta como `href`,
|
|
61
|
+
// así que aquí se filtra en vez de creerse.
|
|
62
|
+
const links = (Array.isArray(src.links) ? src.links : [])
|
|
63
|
+
.filter((u) => typeof u === 'string')
|
|
64
|
+
.map((u) => u.trim())
|
|
65
|
+
.filter((u) => /^https?:\/\//i.test(u) && u.length <= MAX_LINK_LEN)
|
|
66
|
+
.slice(0, MAX_LINKS)
|
|
67
|
+
if (links.length) out.links = links
|
|
55
68
|
return Object.keys(out).length ? out : null
|
|
56
69
|
}
|
|
57
70
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
${
|
|
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>
|