Script d'aperçu des liens sur Yakihonne

Gerfon

npub1r6thdg3lhpzsfwkv9xszc42907pk66r4hg0y4yzaggzqyut2kjzqgerufd

hex

252b7ed04e621cc8ad4f5f80940e2aa4718571d2864a91d5544b5e357a9c4aee

nevent

nevent1qqsz22m76p8xy8xg4484lqy5pc42guv9w8fgvj53642ykh3402wy4msprpmhxue69uhhyetvv9ujuem4d36kwatvw5hx6mm9qgspa9mk5glms3gyhtxzngpv24zhlqmddp6m58j2jpw5ypqzw94tfpqnkuad9

naddr

naddr1qq2k566ntue4qc2v2prhj6jtdfp8xs28wf28yqgcwaehxw309aex2mrp0yhxwatvw4nh2mr49ekk7egzyq0fwa4z87uy2p96es56qtz4g4lcxmtgwkapuj5st4pqgqn3d26ggqcyqqq823c6357gx

Kind-30023 (Article)

2026-05-30T11:25:06Z

Par défaut la génération des aperçus des liens ne fonctionne pas sur Yakihonne.com. #yakihonne #nostrfr

Pour résoudre ce problème, j'ai trouvé l'astuce suivante :

  1. Installer l'extension Tampermonkey
  2. Dans les paramètres de l'extension autoriser les scripts utilisateur image
  3. Dans Tampermonkey créer le script suivant :
// ==UserScript==
// @name         Yakihonne - Aperçus URL carte compacte
// @namespace    local.yakihonne.preview
// @version      2.2
// @description  Affiche une carte compacte avec image complète à gauche et titre centré à droite
// @match        https://yakihonne.com/*
// @grant        GM_xmlhttpRequest
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    const processed = new Set();

    const style = document.createElement('style');
    style.textContent = `
        .yh-preview-card {
            display: flex;
            align-items: stretch;
            width: 100%;
            max-width: 100%;
            height: 96px;
            margin: 8px 0 10px 0;
            border: 1px solid #ddd;
            border-radius: 14px;
            overflow: hidden;
            background: #f7f3f1;
            text-decoration: none;
            cursor: pointer;
            box-sizing: border-box;
        }

        .yh-preview-card:hover {
            background: #f0ecea;
        }

        .yh-preview-image {
            width: 220px;
            min-width: 220px;
            height: 100%;
            overflow: hidden;
            background: #f7f3f1;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .yh-preview-image img {
            width: 100%;
            height: 100%;
            object-fit: contain;
            display: block;
            background: #f7f3f1;
        }

        .yh-preview-title {
            flex: 1;
            min-width: 0;
            padding: 10px 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            color: #111;
            font-size: 16px;
            font-weight: 700;
            line-height: 1.25;
        }

        .yh-preview-title span {
            display: -webkit-box;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
            overflow: hidden;
        }

        @media (max-width: 700px) {
            .yh-preview-image {
                width: 140px;
                min-width: 140px;
            }

            .yh-preview-title {
                font-size: 14px;
                padding: 8px;
            }
        }
    `;
    document.head.appendChild(style);

    function escapeHtml(text) {
        return String(text || '').replace(/[&<>"']/g, c => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;'
        }[c]));
    }

    function meta(doc, selector) {
        return doc.querySelector(selector)?.getAttribute('content') || '';
    }

    function absoluteUrl(value, base) {
        try {
            return new URL(value, base).href;
        } catch {
            return '';
        }
    }

    function fetchPreview(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                timeout: 15000,

                onload: res => {
                    try {
                        const doc = new DOMParser().parseFromString(
                            res.responseText,
                            'text/html'
                        );

                        const title =
                            meta(doc, 'meta[property="og:title"]') ||
                            meta(doc, 'meta[name="twitter:title"]') ||
                            doc.querySelector('title')?.textContent ||
                            url;

                        const image =
                            meta(doc, 'meta[property="og:image"]') ||
                            meta(doc, 'meta[name="twitter:image"]') ||
                            '';

                        resolve({
                            url,
                            title: title.trim(),
                            image: absoluteUrl(image, url)
                        });
                    } catch {
                        resolve(null);
                    }
                },

                onerror: () => resolve(null),
                ontimeout: () => resolve(null)
            });
        });
    }

    function createCard(data) {
        if (!data.image) return null;

        const card = document.createElement('a');

        card.className = 'yh-preview-card';
        card.href = data.url;
        card.target = '_blank';
        card.rel = 'noopener noreferrer';

        card.innerHTML = `
            <div class="yh-preview-image">
                <img src="${escapeHtml(data.image)}" loading="lazy">
            </div>
            <div class="yh-preview-title">
                <span>${escapeHtml(data.title)}</span>
            </div>
        `;

        card.addEventListener('click', function (e) {
            e.preventDefault();
            e.stopPropagation();
            window.open(data.url, '_blank', 'noopener,noreferrer');
        }, true);

        return card;
    }

    async function scanLinks() {
        const links = document.querySelectorAll('a[href^="http"]');

        for (const link of links) {
            const url = link.href;

            if (!url) continue;
            if (url.includes('yakihonne.com')) continue;
            if (url.startsWith('nostr:')) continue;
            if (link.closest('.yh-preview-card')) continue;

            const key = url + '|' + link.textContent;

            if (processed.has(key)) continue;
            processed.add(key);

            const preview = await fetchPreview(url);

            if (!preview) continue;

            const card = createCard(preview);

            if (!card) continue;

            if (
                !link.nextElementSibling ||
                !link.nextElementSibling.classList.contains('yh-preview-card')
            ) {
                link.insertAdjacentElement('afterend', card);
            }
        }
    }

    const observer = new MutationObserver(() => {
        clearTimeout(window.__yhPreviewTimer);

        window.__yhPreviewTimer = setTimeout(() => {
            scanLinks();
        }, 500);
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    setTimeout(scanLinks, 1500);
    setInterval(scanLinks, 3000);
})();

Normalement vous devriez obtenir le résultat suivant : image

原始 JSON

{
  "kind": 30023,
  "id": "252b7ed04e621cc8ad4f5f80940e2aa4718571d2864a91d5544b5e357a9c4aee",
  "pubkey": "1e9776a23fb84504bacc29a02c55457f836d6875ba1e4a905d420402716ab484",
  "created_at": 1780140383,
  "tags": [
    [
      "client",
      "Yakihonne",
      "31990:20986fb83e775d96d188ca5c9df10ce6d613e0eb7e5768a0f0b12b37cdac21b3:1700732875747"
    ],
    [
      "published_at",
      "1780140306"
    ],
    [
      "d",
      "jkS_3PaLPGyjKjBsAGrTr"
    ],
    [
      "image",
      "https://image.nostr.build/ffca2401b910554614a62e99121ca6fc92ab243bc5a93ca68920534847bf0c2a.jpg"
    ],
    [
      "title",
      "Script d'aperçu des liens sur Yakihonne"
    ],
    [
      "summary",
      ""
    ],
    [
      "t",
      "yakihonne"
    ],
    [
      "t",
      "nostrfr"
    ]
  ],
  "content": "Par défaut la génération des aperçus des liens ne fonctionne pas sur [Yakihonne.com](https://yakihonne.com). #yakihonne #nostrfr\n\nPour résoudre ce problème, j'ai trouvé l'astuce suivante :\n\n1. Installer l'extension [Tampermonkey](https://www.tampermonkey.net/)\n2. Dans les paramètres de l'extension autoriser les scripts utilisateur\n![image](https://image.nostr.build/a813062f4869d4715ffc995423459156e418206bd1838e45d19ab97eec89674e.png)\n3. Dans Tampermonkey créer le script suivant :\n```\n// ==UserScript==\n// @name         Yakihonne - Aperçus URL carte compacte\n// @namespace    local.yakihonne.preview\n// @version      2.2\n// @description  Affiche une carte compacte avec image complète à gauche et titre centré à droite\n// @match        https://yakihonne.com/*\n// @grant        GM_xmlhttpRequest\n// @connect      *\n// ==/UserScript==\n\n(function () {\n    'use strict';\n\n    const processed = new Set();\n\n    const style = document.createElement('style');\n    style.textContent = `\n        .yh-preview-card {\n            display: flex;\n            align-items: stretch;\n            width: 100%;\n            max-width: 100%;\n            height: 96px;\n            margin: 8px 0 10px 0;\n            border: 1px solid #ddd;\n            border-radius: 14px;\n            overflow: hidden;\n            background: #f7f3f1;\n            text-decoration: none;\n            cursor: pointer;\n            box-sizing: border-box;\n        }\n\n        .yh-preview-card:hover {\n            background: #f0ecea;\n        }\n\n        .yh-preview-image {\n            width: 220px;\n            min-width: 220px;\n            height: 100%;\n            overflow: hidden;\n            background: #f7f3f1;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .yh-preview-image img {\n            width: 100%;\n            height: 100%;\n            object-fit: contain;\n            display: block;\n            background: #f7f3f1;\n        }\n\n        .yh-preview-title {\n            flex: 1;\n            min-width: 0;\n            padding: 10px 14px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            text-align: center;\n            color: #111;\n            font-size: 16px;\n            font-weight: 700;\n            line-height: 1.25;\n        }\n\n        .yh-preview-title span {\n            display: -webkit-box;\n            -webkit-line-clamp: 3;\n            -webkit-box-orient: vertical;\n            overflow: hidden;\n        }\n\n        @media (max-width: 700px) {\n            .yh-preview-image {\n                width: 140px;\n                min-width: 140px;\n            }\n\n            .yh-preview-title {\n                font-size: 14px;\n                padding: 8px;\n            }\n        }\n    `;\n    document.head.appendChild(style);\n\n    function escapeHtml(text) {\n        return String(text || '').replace(/[\u0026\u003c\u003e\"']/g, c =\u003e ({\n            '\u0026': '\u0026amp;',\n            '\u003c': '\u0026lt;',\n            '\u003e': '\u0026gt;',\n            '\"': '\u0026quot;',\n            \"'\": '\u0026#039;'\n        }[c]));\n    }\n\n    function meta(doc, selector) {\n        return doc.querySelector(selector)?.getAttribute('content') || '';\n    }\n\n    function absoluteUrl(value, base) {\n        try {\n            return new URL(value, base).href;\n        } catch {\n            return '';\n        }\n    }\n\n    function fetchPreview(url) {\n        return new Promise(resolve =\u003e {\n            GM_xmlhttpRequest({\n                method: 'GET',\n                url,\n                timeout: 15000,\n\n                onload: res =\u003e {\n                    try {\n                        const doc = new DOMParser().parseFromString(\n                            res.responseText,\n                            'text/html'\n                        );\n\n                        const title =\n                            meta(doc, 'meta[property=\"og:title\"]') ||\n                            meta(doc, 'meta[name=\"twitter:title\"]') ||\n                            doc.querySelector('title')?.textContent ||\n                            url;\n\n                        const image =\n                            meta(doc, 'meta[property=\"og:image\"]') ||\n                            meta(doc, 'meta[name=\"twitter:image\"]') ||\n                            '';\n\n                        resolve({\n                            url,\n                            title: title.trim(),\n                            image: absoluteUrl(image, url)\n                        });\n                    } catch {\n                        resolve(null);\n                    }\n                },\n\n                onerror: () =\u003e resolve(null),\n                ontimeout: () =\u003e resolve(null)\n            });\n        });\n    }\n\n    function createCard(data) {\n        if (!data.image) return null;\n\n        const card = document.createElement('a');\n\n        card.className = 'yh-preview-card';\n        card.href = data.url;\n        card.target = '_blank';\n        card.rel = 'noopener noreferrer';\n\n        card.innerHTML = `\n            \u003cdiv class=\"yh-preview-image\"\u003e\n                \u003cimg src=\"${escapeHtml(data.image)}\" loading=\"lazy\"\u003e\n            \u003c/div\u003e\n            \u003cdiv class=\"yh-preview-title\"\u003e\n                \u003cspan\u003e${escapeHtml(data.title)}\u003c/span\u003e\n            \u003c/div\u003e\n        `;\n\n        card.addEventListener('click', function (e) {\n            e.preventDefault();\n            e.stopPropagation();\n            window.open(data.url, '_blank', 'noopener,noreferrer');\n        }, true);\n\n        return card;\n    }\n\n    async function scanLinks() {\n        const links = document.querySelectorAll('a[href^=\"http\"]');\n\n        for (const link of links) {\n            const url = link.href;\n\n            if (!url) continue;\n            if (url.includes('yakihonne.com')) continue;\n            if (url.startsWith('nostr:')) continue;\n            if (link.closest('.yh-preview-card')) continue;\n\n            const key = url + '|' + link.textContent;\n\n            if (processed.has(key)) continue;\n            processed.add(key);\n\n            const preview = await fetchPreview(url);\n\n            if (!preview) continue;\n\n            const card = createCard(preview);\n\n            if (!card) continue;\n\n            if (\n                !link.nextElementSibling ||\n                !link.nextElementSibling.classList.contains('yh-preview-card')\n            ) {\n                link.insertAdjacentElement('afterend', card);\n            }\n        }\n    }\n\n    const observer = new MutationObserver(() =\u003e {\n        clearTimeout(window.__yhPreviewTimer);\n\n        window.__yhPreviewTimer = setTimeout(() =\u003e {\n            scanLinks();\n        }, 500);\n    });\n\n    observer.observe(document.body, {\n        childList: true,\n        subtree: true\n    });\n\n    setTimeout(scanLinks, 1500);\n    setInterval(scanLinks, 3000);\n})();\n```\nNormalement vous devriez obtenir le résultat suivant :\n![image](https://image.nostr.build/80215eff16bc26c43511435debf8400e67b42ac6b8e32f2e8f9fd9c9cc23d275.png)\n",
  "sig": "871346bf57951bffd4c0e2f329c6f7b0a6c49c9c9eb1d574530d1f3f80c4eee73217ae6e77a7b207793cdd3b9a64f983cee181148d9fb8cca03d2f11f0087973"
}