Configurable link in bio starter kit easily deployed with Deno Deploy.
at main 5.5 kB view raw
1import type { Config, Link, Section } from "./types.ts"; 2 3// Utility functions 4function escapeHtml(text: string): string { 5 const map: Record<string, string> = { 6 "&": "&amp;", 7 "<": "&lt;", 8 ">": "&gt;", 9 '"': "&quot;", 10 "'": "&#039;", 11 }; 12 return text.replace(/[&<>"']/g, (m) => map[m]); 13} 14 15// Component rendering functions 16function renderLinkCard(link: Link, config: Config): string { 17 const bgColor = link.cardBgColor || config.theme.defaultCardBgColor; 18 const borderColor = link.cardBorder || config.theme.defaultCardBorder; 19 const shimmerClass = link.featured ? "shimmer-card" : ""; 20 21 const imageContent = link.image 22 ? `<img class="w-16 h-16 ml-4 rounded-full object-cover" 23 src="${link.image}" 24 alt="${escapeHtml(link.title)}" 25 loading="lazy">` 26 : `<div class="w-16 h-16 ml-4 rounded-full bg-white/20 flex items-center justify-center text-2xl font-bold"> 27 ${escapeHtml(link.title.charAt(0).toUpperCase())} 28 </div>`; 29 30 return ` 31 <div class="group"> 32 <a href="${link.href}" target="_blank" rel="noopener noreferrer"> 33 <div class="shadow-xl text-white border-2 min-h-28 rounded-2xl flex items-center hover:scale-105 transition-transform duration-300 overflow-hidden relative ${shimmerClass}" 34 style="background-color: ${bgColor}; border-color: ${borderColor}; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);"> 35 <figure class="flex-shrink-0 relative z-10"> 36 ${imageContent} 37 </figure> 38 <div class="flex-1 p-4 relative z-10"> 39 <h2 class="text-xl md:text-2xl font-semibold">${ 40 escapeHtml(link.title) 41 }</h2> 42 <p class="lg:text-xl">${link.description}</p> 43 </div> 44 </div> 45 </a> 46 </div> 47 `; 48} 49 50function renderSection(section: Section, config: Config): string { 51 const links = section.links.map((link) => renderLinkCard(link, config)).join( 52 "\n", 53 ); 54 55 if (section.title) { 56 return ` 57 <div class="md:text-xl px-4 h-20 bg-white shadow-md flex items-center justify-center rounded-2xl font-semibold border-2 border-black"> 58 ${escapeHtml(section.title)} 59 </div> 60 ${links} 61 `; 62 } 63 64 return links; 65} 66 67export function renderPage(config: Config): string { 68 const allSections = config.sections.map((section) => 69 renderSection(section, config) 70 ).join("\n"); 71 72 return `<!DOCTYPE html> 73<html lang="en"> 74<head> 75 <meta charset="utf-8"> 76 <meta name="viewport" content="width=device-width, initial-scale=1"> 77 <meta name="description" content="${escapeHtml(config.site.description)}"> 78 79 <!-- Open Graph --> 80 <meta property="og:type" content="website"> 81 <meta property="og:title" content="${escapeHtml(config.site.title)}"> 82 <meta property="og:description" content="${ 83 escapeHtml(config.site.description) 84 }"> 85 <meta property="og:image" content="${config.images.profile}"> 86 87 <title>${escapeHtml(config.site.title)}</title> 88 89 <!-- Tailwind CSS CDN --> 90 <script src="https://cdn.tailwindcss.com"></script> 91 92 <style> 93 html { 94 background-color: ${config.theme.gradientTo}; 95 min-height: 100vh; 96 } 97 98 body { 99 background: linear-gradient(to bottom, ${config.theme.gradientFrom}, ${config.theme.gradientVia}, ${config.theme.gradientTo}); 100 background-attachment: fixed; 101 min-height: 100vh; 102 } 103 104 @keyframes shimmer { 105 100% { 106 transform: translateX(100%); 107 } 108 } 109 110 .shimmer-card::before { 111 content: ""; 112 position: absolute; 113 inset: 0; 114 background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.3), transparent); 115 transform: translateX(-100%); 116 pointer-events: none; 117 z-index: 1; 118 animation: shimmer 3s; 119 } 120 121 .group:hover .shimmer-card::before { 122 animation: shimmer 3s infinite; 123 } 124 125 .divider { 126 display: flex; 127 flex-direction: row; 128 align-items: center; 129 margin: 1rem 0; 130 } 131 132 .divider::before, 133 .divider::after { 134 content: ""; 135 flex: 1; 136 height: 1px; 137 background-color: rgba(0, 0, 0, 0.1); 138 } 139 140 .divider::before { 141 margin-right: 1rem; 142 } 143 144 .divider::after { 145 margin-left: 1rem; 146 } 147 </style> 148</head> 149 150<body class="min-h-screen"> 151 <div class="w-full max-w-2xl min-h-screen mx-auto pt-4 px-4 relative"> 152 <!-- Banner --> 153 <img class="object-cover object-left-bottom w-full h-40 shadow-xl rounded-xl" 154 src="${config.images.banner}" 155 alt="Banner" /> 156 157 <!-- Profile Picture --> 158 <div class="absolute top-24 left-8"> 159 <div class="w-24 h-24 rounded-full border-4 border-white flex items-center justify-center overflow-hidden"> 160 <img class="w-full h-full object-cover" 161 src="${config.images.profile}" 162 alt="Profile" /> 163 </div> 164 </div> 165 166 <!-- Content --> 167 <div class="w-full mx-auto px-2 space-y-4 mt-8"> 168 <!-- Sections --> 169 ${allSections} 170 171 <!-- Footer --> 172 <div class="w-full h-16 py-4 text-center font-bold" style="color: ${config.footer.textColor};"> 173 ${escapeHtml(config.footer.text)} 174 <a href="#" onclick="scrollToTop(); return false;" class="underline">${ 175 escapeHtml(config.footer.linkText) 176 }</a> 177 </div> 178 </div> 179 </div> 180 181 <script> 182 function scrollToTop() { 183 window.scrollTo({ 184 top: 0, 185 behavior: 'smooth' 186 }); 187 } 188 </script> 189</body> 190</html>`; 191}