diff options
| author | eltrac <hi@eltr.ac> | 2026-03-12 14:52:11 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-03-12 14:52:11 +0800 |
| commit | 087ea995eea21df2dee4b2f99ed86e72e58cd1b6 (patch) | |
| tree | 014c87cad1f55d2a4920b90c5b0153b49960380f /layouts | |
| parent | a1dbdfc7ae1c2a29ccffea6bb99d200ce292017d (diff) | |
feat(shortcode): social card
from https://github.com/thumbsupdotme/social-cards
by thumbsupdotme
Diffstat (limited to 'layouts')
| -rw-r--r-- | layouts/_shortcodes/fedicard.html | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/layouts/_shortcodes/fedicard.html b/layouts/_shortcodes/fedicard.html new file mode 100644 index 00000000..ab24542b --- /dev/null +++ b/layouts/_shortcodes/fedicard.html @@ -0,0 +1,148 @@ +{{- $url := .Get 0 -}} +{{- $parsed := urls.Parse $url -}} +{{- $parts := split $parsed.Path "/" -}} +{{- $id := index $parts (sub (len $parts) 1) -}} +{{- $instance := printf "%s://%s" $parsed.Scheme $parsed.Host -}} +{{- $apiUrl := printf "%s/api/v1/statuses/%s" $instance $id -}} +{{- $data := resources.GetRemote $apiUrl -}} +{{- $json := $data | transform.Unmarshal -}} +{{- $author := $json.account -}} + +{{- $publishedAt := time $json.created_at -}} + +{{- $avatarSrc := $author.avatar -}} +{{- with resources.GetRemote $author.avatar -}} + {{- with .Resize "88x88 q85 webp" -}} + {{- $avatarSrc = .RelPermalink -}} + {{- end -}} +{{- end -}} + +{{- /* Count image attachments */ -}} +{{- $imgCount := 0 -}} +{{- range $json.media_attachments -}}{{- if eq .type "image" -}}{{- $imgCount = add $imgCount 1 -}}{{- end -}}{{- end -}} +{{- $cId := printf "sc-%s" (md5 $url) -}} +{{- $dId := printf "sd-%s" (md5 $url) -}} + +<div class="social-card-final cnt-undo rd bg-neutral-50 p-4 md:px-8 md:py-6 shadow" style="position: relative; overflow: hidden;"> + <div style="display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 8px;"> + <div style="display: flex; align-items: center; gap: 12px;"> + <img src="{{ $avatarSrc }}" style="width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; object-fit: cover;" /> + <div style="display: flex; flex-direction: column; line-height: 1.2;"> + <span style="font-weight: 700; color: var(--color-neso-fg1); font-size: 1rem;">{{ $author.display_name }}</span> + <span style="color: var(--color-neso-fg3); font-size: 0.9rem;">@{{ $author.acct }}</span> + </div> + </div> + <a href="{{ $url }}" target="_blank" style="color: #6364ff; align-self: flex-start; margin-top: 2px; transition: transform 0.2s ease-in-out; display: flex; flex-shrink: 0;" onmouseover="this.style.transform='scale(1.2)'" onmouseout="this.style.transform='scale(1)'"> +<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 196.52 196.52"> + <path fill="#a730b8" d="M47.9242 72.7966a18.2278 18.2278 0 0 1-7.7959 7.7597l42.7984 42.9653 10.3182-5.2291zm56.4524 56.6704-10.3182 5.2291 21.686 21.7708a18.2278 18.2278 0 0 1 7.7975-7.7608z"/> + <path fill="#5496be" d="M129.6645 102.0765l1.7865 11.4272 27.4149-13.8942a18.2278 18.2278 0 0 1-4.9719-9.8124zm-14.0658 7.1282-57.2891 29.0339a18.2278 18.2278 0 0 1 4.9728 9.8133l54.1027-27.4194z"/> + <path fill="#ce3d1a" d="M69.5312 91.6539l8.1618 8.1933 29.269-57.1387a18.2278 18.2278 0 0 1-9.787-5.0219zm-7.1897 14.0363-14.0022 27.3353a18.2278 18.2278 0 0 1 9.786 5.0214l12.3775-24.1639z"/> + <path fill="#d0188f" d="M39.8906 80.6763a18.2278 18.2278 0 0 1-10.8655 1.7198l8.1762 52.2981a18.2278 18.2278 0 0 1 10.8645-1.7198z"/> + <path fill="#5b36e9" d="M63.3259 148.3109a18.2278 18.2278 0 0 1-1.7322 10.8629l52.2893 8.3907a18.2278 18.2278 0 0 1 1.7322-10.8629z"/> + <path fill="#30b873" d="M134.9148 146.9182a18.2278 18.2278 0 0 1 9.788 5.0224l24.1345-47.117a18.2278 18.2278 0 0 1-9.7875-5.0229z"/> + <path fill="#ebe305" d="M126.1329 33.1603a18.2278 18.2278 0 0 1-7.7975 7.7608l37.3765 37.5207a18.2278 18.2278 0 0 1 7.7969-7.7608z"/> + <path fill="#f47601" d="M44.7704 51.6279a18.2278 18.2278 0 0 1 4.9723 9.8123l47.2478-23.9453a18.2278 18.2278 0 0 1-4.9718-9.8113z"/> + <path fill="#57c115" d="M118.2491 40.9645a18.2278 18.2278 0 0 1-10.8511 1.8123l4.1853 26.8 11.42 1.8324zm-4.2333 44.1927 9.8955 63.3631a18.2278 18.2278 0 0 1 10.88-1.6278l-9.355-59.9035z"/> + <path fill="#dbb210" d="M49.7763 61.6412a18.2278 18.2278 0 0 1-1.694 10.8686l26.8206 4.3077 5.2715-10.2945zm45.9677 7.382-5.272 10.2955 63.3713 10.1777a18.2278 18.2278 0 0 1 1.7606-10.8593z"/> + <path fill="#ffca00" d="M93.4385 23.8419a1 1 0 1 0 33.0924 1.8025 1 1 0 1 0-33.0924-1.8025"/> + <path fill="#64ff00" d="M155.314 85.957a1 1 0 1 0 33.0923 1.8025 1 1 0 1 0-33.0923-1.8025"/> + <path fill="#00a3ff" d="M115.3466 163.9824a1 1 0 1 0 33.0923 1.8025 1 1 0 1 0-33.0923-1.8025"/> + <path fill="#9500ff" d="M28.7698 150.0898a1 1 0 1 0 33.0923 1.8025 1 1 0 1 0-33.0923-1.8025"/> + <path fill="#ff0000" d="M15.2298 63.4781a1 1 0 1 0 33.0923 1.8025 1 1 0 1 0-33.0923-1.8025"/> +</svg> + </a> + </div> + + {{- $bodyText := $json.content -}} + {{- $bodyText = $bodyText | replaceRE `(<p>[\s]*(<a [^>]*class="mention hashtag"[^>]*>#<span>[^<]+</span></a>[\s]*)+</p>[\s]*)+$` "" -}} + {{- $bodyText = $bodyText | replaceRE `<span class="invisible">[^<]*</span>` "" -}} + {{- $bodyText = $bodyText | replaceRE `<a ` `<a style="text-decoration:none;" ` -}} + {{- $bodyText = $bodyText | replaceRE `<p>` `<p style="margin:0 0 0.9em 0;">` -}} + {{- $bodyText = $bodyText | replaceRE `<p [^>]+>[\s]*</p>` "" -}} + + <div style="font-size: 1.05rem; line-height: 1.6; margin-bottom: 12px; color: var(--color-neso-fg1);"> + {{ $bodyText | safeHTML }} + </div> + + {{- if gt $imgCount 0 -}} + {{- if le $imgCount 2 -}} + {{- /* 1–2 images: flex row (2 images split 50/50, cropped) */ -}} + <div style="display: flex; gap: 8px; margin-top: 8px; margin-bottom: 12px; overflow: hidden;"> + {{- range $json.media_attachments -}} + {{- if eq .type "image" -}} + {{- $imgSrc := .preview_url -}} + {{- with resources.GetRemote .preview_url -}} + {{- with .Process "q85 webp" -}} + {{- $imgSrc = .RelPermalink -}} + {{- end -}} + {{- end -}} + <img src="{{ $imgSrc }}" alt="{{ .description }}" onclick="window.__scLb&&window.__scLb(this.src)" style="flex: 1 1 0%; min-width: 0; border-radius: 8px; display: block; margin: 0 !important; cursor: pointer;{{ if gt $imgCount 1 }} height: 250px; object-fit: cover;{{ else }} height: auto; width: 100%;{{ end }}" loading="lazy"> + {{- end -}} + {{- end -}} + </div> + {{- else -}} + {{- /* 3+ images: scroll-snap carousel */ -}} + <style>#{{ $cId }}::-webkit-scrollbar{display:none}</style> + <div style="margin: 8px 0 4px; overflow: hidden;"> + <div id="{{ $cId }}" style="display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none; border-radius: 8px;"> + {{- range $json.media_attachments -}} + {{- if eq .type "image" -}} + {{- $imgSrc := .preview_url -}} + {{- with resources.GetRemote .preview_url -}} + {{- with .Process "q85 webp" -}} + {{- $imgSrc = .RelPermalink -}} + {{- end -}} + {{- end -}} + <img src="{{ $imgSrc }}" alt="{{ .description }}" onclick="window.__scLb&&window.__scLb(this.src)" style="scroll-snap-align: start; flex-shrink: 0; width: 100%; height: 280px; object-fit: cover; border-radius: 8px; cursor: pointer; display: block; margin: 0 !important;" loading="lazy"> + {{- end -}} + {{- end -}} + </div> + {{- $imgAttachments := slice -}} + {{- range $json.media_attachments -}}{{- if eq .type "image" -}}{{- $imgAttachments = $imgAttachments | append . -}}{{- end -}}{{- end -}} + <div id="{{ $dId }}" style="display: flex; justify-content: center; gap: 6px; margin-top: 8px; margin-bottom: 12px;"> + {{- range $i, $_ := $imgAttachments -}} + <button onclick="(function(){var c=document.getElementById('{{ $cId }}');c.scrollTo({left:c.children[{{ $i }}].offsetLeft,behavior:'smooth'});})()" aria-label="Image {{ add $i 1 }}" style="width: 8px; height: 8px; border-radius: 50%; border: none; background: {{ if eq $i 0 }}var(--color-neso-fg2){{ else }}var(--color-neso-deco2){{ end }}; cursor: pointer; padding: 0; transition: background 0.2s;"></button> + {{- end -}} + </div> + </div> + <script>(function(){var c=document.getElementById('{{ $cId }}'),ds=document.getElementById('{{ $dId }}').querySelectorAll('button');c.addEventListener('scroll',function(){var i=Math.round(c.scrollLeft/c.offsetWidth);ds.forEach(function(d,j){d.style.background=j===i?'var(--color-neso-fg2)':'var(--color-neso-deco2)';});},{passive:true});})()</script> + {{- end -}} + {{- end -}} + + {{- /* Link preview card — use original image URL, skip processing (CDN images can return 403) */ -}} + {{- with $json.card -}} + {{- if or (eq .type "link") (eq .type "photo") (eq .type "video") -}} + <a href="{{ .url }}" target="_blank" rel="noopener noreferrer" style="display: block; margin-top: 8px; margin-bottom: 16px; border: 1px solid var(--color-neso-deco2); border-radius: 10px; overflow: hidden; text-decoration: none; color: inherit;"> + {{- if .image -}} + <img src="{{ .image }}" alt="{{ .title }}" style="width: 100%; height: 180px; object-fit: cover; display: block; margin: 0 !important;"> + {{- end -}} + <div style="padding: 10px 14px;"> + <div style="font-weight: 600; font-size: 0.95rem; color: var(--color-neso-fg1); margin-bottom: 3px;">{{ .title }}</div> + {{- if .description -}}<div style="font-size: 0.85rem; color: var(--color-neso-fg3); margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">{{ .description }}</div>{{- end -}} + <div style="font-size: 0.8rem; color: var(--color-neso-fg3);">{{ (urls.Parse .url).Host }}</div> + </div> + </a> + {{- end -}} + {{- end -}} + + <div style="border-top: 1px solid var(--color-neso-deco3); padding-top: 12px; margin-top: 16px; display: flex; justify-content: space-between; align-items: center;"> + <time class="social-card-time" datetime="{{ $publishedAt.Format "2006-01-02T15:04:05Z" }}" style="color: var(--color-neso-fg3); font-size: 0.9rem;">{{ $publishedAt.Format "Jan 2, 2006 at 3:04 PM UTC" }}</time> + <a href="{{ $url }}" target="_blank" style="color: #6364ff; font-weight: 600; text-decoration: none; font-size: 0.9rem; transition: color 0.2s ease-in-out;" onmouseover="this.style.color='#8A8BFF'" onmouseout="this.style.color='#6364ff'">View on Fediverse</a> + </div> +</div> + +{{- /* Shared lightbox — injected once per page via DOM API (no innerHTML) */ -}} +<script> +if (!window.__scLb) { + var _lbImg = document.createElement('img'); + _lbImg.style.cssText = 'max-width:92vw;max-height:92vh;object-fit:contain;border-radius:8px;cursor:default;'; + _lbImg.addEventListener('click', function(e) { e.stopPropagation(); }); + var _lbOv = document.createElement('div'); + _lbOv.style.cssText = 'display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.88);cursor:zoom-out;align-items:center;justify-content:center;'; + _lbOv.appendChild(_lbImg); + _lbOv.addEventListener('click', function() { _lbOv.style.display = 'none'; document.body.style.overflow = ''; }); + document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { _lbOv.style.display = 'none'; document.body.style.overflow = ''; } }); + document.body.appendChild(_lbOv); + window.__scLb = function(src) { _lbImg.src = src; _lbOv.style.display = 'flex'; document.body.style.overflow = 'hidden'; }; +} +</script> |
