Fork of Chiri for Astro for my blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at bf36f9845fe379082327c2b570eae111d1e0590a 237 lines 9.9 kB view raw
1import { visit } from 'unist-util-visit' 2 3/** 4 * A remark plugin that converts custom directives to embedded media HTML elements 5 * Supports: link cards, Spotify, YouTube, Bilibili, X posts, and GitHub repository cards 6 */ 7const embedHandlers = { 8 // Link Card 9 link: (node) => { 10 const url = node.attributes?.url 11 if (!url) { 12 return false 13 } 14 15 // Create the LinkCard HTML structure - all metadata will be fetched by JavaScript 16 return ` 17 <div class="link-card-wrapper"> 18 <a href="${url}" class="link-card" target="_blank" rel="noopener noreferrer" data-url="${url}"> 19 <div class="link-card-content"> 20 <div class="link-card-url"></div> 21 <p class="link-card-title" style="display: none;"></p> 22 <p class="link-card-description" style="display: none;"></p> 23 </div> 24 <div class="link-card-image-outer"> 25 <div class="link-card-image" style="display: none;"> 26 <img src="" alt="" loading="lazy" /> 27 </div> 28 </div> 29 </a> 30 </div> 31 ` 32 }, 33 34 // Spotify 35 spotify: (node) => { 36 const url = node.attributes?.url ?? '' 37 if (!url) { 38 return false 39 } 40 if (!/^https:\/\/open\.spotify\.com\//.test(url)) { 41 return false 42 } 43 let embedUrl = url.replace('open.spotify.com/', 'open.spotify.com/embed/') 44 if (!embedUrl.includes('utm_source=')) { 45 embedUrl += (embedUrl.includes('?') ? '&' : '?') + 'utm_source=generator' 46 } 47 48 let height = '152' 49 if ( 50 url.includes('/album/') || 51 url.includes('/playlist/') || 52 url.includes('/artist/') || 53 url.includes('/show/') 54 ) { 55 height = '352' 56 } 57 58 return ` 59 <figure> 60 <iframe 61 style="border-radius:12px" 62 src="${embedUrl}" 63 width="100%" 64 height="${height}" 65 frameBorder="0" 66 allowfullscreen="" 67 allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" 68 loading="lazy" 69 ></iframe> 70 </figure> 71 ` 72 }, 73 74 // Youtube 75 youtube: (node) => { 76 let videoId = node.attributes?.id ?? '' 77 const url = node.attributes?.url ?? '' 78 79 if (!videoId && url) { 80 const match = url.match(/(?:v=|\/embed\/|youtu\.be\/)([\w-]{11})/) 81 if (match) videoId = match[1] 82 } 83 84 if (!videoId) { 85 return false 86 } 87 88 return ` 89 <figure> 90 <iframe 91 style="border-radius:6px" 92 src="https://www.youtube.com/embed/${videoId}" 93 title="YouTube video player" 94 loading="lazy" 95 frameborder="0" 96 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 97 allowfullscreen 98 ></iframe> 99 </figure> 100 ` 101 }, 102 103 // Bilibili 104 bilibili: (node) => { 105 let bvid = node.attributes?.id ?? '' 106 const url = node.attributes?.url ?? '' 107 if (!bvid && url) { 108 const match = url.match(/\/BV([\w]+)/) 109 if (match) bvid = 'BV' + match[1] 110 } 111 if (!bvid) { 112 return false 113 } 114 115 return ` 116 <figure> 117 <iframe 118 style="border-radius:6px" 119 src="//player.bilibili.com/player.html?isOutside=true&bvid=${bvid}&p=1&autoplay=0&muted=0" 120 title="Bilibili video player" 121 loading="lazy" 122 scrolling="no" 123 border="0" 124 frameborder="no" 125 framespacing="0" 126 allowfullscreen="true" 127 ></iframe> 128 </figure> 129 ` 130 }, 131 132 // X Post Card 133 x: (node) => { 134 const xUrl = node.attributes?.url ?? '' 135 if (!xUrl) { 136 return false 137 } 138 139 const twitterUrl = xUrl.replace(/(\w+:\/\/)?x\.com\//g, '$1twitter.com/') 140 const uniqueId = `x-card-${Math.random().toString(36).slice(2, 11)}` 141 142 return ` 143 <figure class="x-card"> 144 <blockquote class="twitter-tweet" data-dnt="true" id="${uniqueId}"> 145 <a href="${twitterUrl}"></a> 146 </blockquote> 147 </figure> 148 ` 149 }, 150 151 // Github Repository Card 152 github: (node) => { 153 const repo = node.attributes?.repo ?? '' 154 if (!repo) { 155 console.warn(`Missing GitHub repository`) 156 return false 157 } 158 159 const [owner, name] = repo.split('/') 160 if (!owner || !name) { 161 console.warn(`Invalid GitHub repository format: "${repo}"`) 162 return false 163 } 164 165 return ` 166 <a href="https://github.com/${repo}" class="gc-container" target="_blank" rel="noopener noreferrer" data-repo="${repo}"> 167 <div class="gc-title-bar"> 168 <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div> 169 <span class="gc-repo-title"> 170 <span>${owner}<span class="gc-slash" aria-hidden="true">/</span><strong>${name}</strong></span> 171 </span> 172 <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 173 <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path> 174 </svg> 175 </div> 176 <p class="gc-repo-description">--</p> 177 <div class="gc-info-bar"> 178 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 179 <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path> 180 </svg> 181 <span class="gc-stars-count" aria-label="Stars count">--</span> 182 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 183 <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path> 184 </svg> 185 <span class="gc-forks-count" aria-label="Forks count">--</span> 186 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 187 <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path> 188 </svg> 189 <span class="gc-license-info" aria-label="License">--</span> 190 </div> 191 </a> 192 ` 193 }, 194 195 // NeoDB Card 196 neodb: (node) => { 197 const url = node.attributes?.url ?? '' 198 if (!url) { 199 return false 200 } 201 202 const neodbUrlPattern = 203 /neodb\.social\/(movie|book|music|album|game|tv\/season|tv|podcast)\/([\w-]+)/ 204 const match = url.match(neodbUrlPattern) 205 const category = match ? match[1] : 'other' 206 207 const isSquare = category === 'music' || category === 'album' || category === 'podcast' 208 const skeletonClass = isSquare ? 'music' : 'other' 209 210 return `<div class="neodb-card-container" data-url="${url}"> 211 <div class="neodb-card neodb-loading ${skeletonClass}"> 212 </div> 213</div>` 214 } 215} 216 217export default function remarkEmbeddedMedia() { 218 return (tree) => { 219 visit(tree, ['leafDirective', 'containerDirective', 'textDirective'], (node) => { 220 const handler = embedHandlers[node.name] 221 if (!handler) { 222 return 223 } 224 225 const htmlContent = handler(node) 226 if (!htmlContent) { 227 return 228 } 229 230 node.type = 'html' 231 node.value = htmlContent 232 delete node.name 233 delete node.attributes 234 delete node.children 235 }) 236 } 237}