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 8d960b1e8e0b3ed7a306ea7803f5af66419b7b37 231 lines 9.8 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 (url.includes('/album/') || url.includes('/playlist/') || url.includes('/artist/') || url.includes('/show/')) { 50 height = '352' 51 } 52 53 return ` 54 <figure> 55 <iframe 56 style="border-radius:12px" 57 src="${embedUrl}" 58 width="100%" 59 height="${height}" 60 frameBorder="0" 61 allowfullscreen="" 62 allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" 63 loading="lazy" 64 ></iframe> 65 </figure> 66 ` 67 }, 68 69 // Youtube 70 youtube: (node) => { 71 let videoId = node.attributes?.id ?? '' 72 const url = node.attributes?.url ?? '' 73 74 if (!videoId && url) { 75 const match = url.match(/(?:v=|\/embed\/|youtu\.be\/)([\w-]{11})/) 76 if (match) videoId = match[1] 77 } 78 79 if (!videoId) { 80 return false 81 } 82 83 return ` 84 <figure> 85 <iframe 86 style="border-radius:6px" 87 src="https://www.youtube.com/embed/${videoId}" 88 title="YouTube video player" 89 loading="lazy" 90 frameborder="0" 91 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 92 allowfullscreen 93 ></iframe> 94 </figure> 95 ` 96 }, 97 98 // Bilibili 99 bilibili: (node) => { 100 let bvid = node.attributes?.id ?? '' 101 const url = node.attributes?.url ?? '' 102 if (!bvid && url) { 103 const match = url.match(/\/BV([\w]+)/) 104 if (match) bvid = 'BV' + match[1] 105 } 106 if (!bvid) { 107 return false 108 } 109 110 return ` 111 <figure> 112 <iframe 113 style="border-radius:6px" 114 src="//player.bilibili.com/player.html?isOutside=true&bvid=${bvid}&p=1&autoplay=0&muted=0" 115 title="Bilibili video player" 116 loading="lazy" 117 scrolling="no" 118 border="0" 119 frameborder="no" 120 framespacing="0" 121 allowfullscreen="true" 122 ></iframe> 123 </figure> 124 ` 125 }, 126 127 // X Post Card 128 x: (node) => { 129 const xUrl = node.attributes?.url ?? '' 130 if (!xUrl) { 131 return false 132 } 133 134 const twitterUrl = xUrl.replace(/(\w+:\/\/)?x\.com\//g, '$1twitter.com/') 135 const uniqueId = `x-card-${Math.random().toString(36).slice(2, 11)}` 136 137 return ` 138 <figure class="x-card"> 139 <blockquote class="twitter-tweet" data-dnt="true" id="${uniqueId}"> 140 <a href="${twitterUrl}"></a> 141 </blockquote> 142 </figure> 143 ` 144 }, 145 146 // Github Repository Card 147 github: (node) => { 148 const repo = node.attributes?.repo ?? '' 149 if (!repo) { 150 console.warn(`Missing GitHub repository`) 151 return false 152 } 153 154 const [owner, name] = repo.split('/') 155 if (!owner || !name) { 156 console.warn(`Invalid GitHub repository format: "${repo}"`) 157 return false 158 } 159 160 return ` 161 <a href="https://github.com/${repo}" class="gc-container" target="_blank" rel="noopener noreferrer" data-repo="${repo}"> 162 <div class="gc-title-bar"> 163 <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div> 164 <span class="gc-repo-title"> 165 <span>${owner}<span class="gc-slash" aria-hidden="true">/</span><strong>${name}</strong></span> 166 </span> 167 <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 168 <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> 169 </svg> 170 </div> 171 <p class="gc-repo-description">--</p> 172 <div class="gc-info-bar"> 173 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 174 <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> 175 </svg> 176 <span class="gc-stars-count" aria-label="Stars count">--</span> 177 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 178 <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> 179 </svg> 180 <span class="gc-forks-count" aria-label="Forks count">--</span> 181 <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> 182 <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> 183 </svg> 184 <span class="gc-license-info" aria-label="License">--</span> 185 </div> 186 </a> 187 ` 188 }, 189 190 // NeoDB Card 191 neodb: (node) => { 192 const url = node.attributes?.url ?? '' 193 if (!url) { 194 return false 195 } 196 197 const neodbUrlPattern = /neodb\.social\/(movie|book|music|album|game|tv\/season|tv|podcast)\/([\w-]+)/ 198 const match = url.match(neodbUrlPattern) 199 const category = match ? match[1] : 'other' 200 201 const isSquare = category === 'music' || category === 'album' || category === 'podcast' 202 const skeletonClass = isSquare ? 'music' : 'other' 203 204 return `<div class="neodb-card-container" data-url="${url}"> 205 <div class="neodb-card neodb-loading ${skeletonClass}"> 206 </div> 207</div>` 208 } 209} 210 211export default function remarkEmbeddedMedia() { 212 return (tree) => { 213 visit(tree, ['leafDirective', 'containerDirective', 'textDirective'], (node) => { 214 const handler = embedHandlers[node.name] 215 if (!handler) { 216 return 217 } 218 219 const htmlContent = handler(node) 220 if (!htmlContent) { 221 return 222 } 223 224 node.type = 'html' 225 node.value = htmlContent 226 delete node.name 227 delete node.attributes 228 delete node.children 229 }) 230 } 231}