custom element for embedding Bluesky posts and feeds mary-ext.github.io/bluesky-embed
typescript npm bluesky atcute
7
fork

Configure Feed

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

docs: remove old site

Mary bcac4166 d443cf9f

-1028
-1
packages/site/.gitignore
··· 1 - pages/
-11
packages/site/index.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>Bluesky embed</title> 7 - </head> 8 - <body> 9 - <script type="module" src="./src/main.tsx"></script> 10 - </body> 11 - </html>
-22
packages/site/package.json
··· 1 - { 2 - "type": "module", 3 - "private": true, 4 - "name": "site", 5 - "scripts": { 6 - "dev": "vite", 7 - "build": "tsc -b && vite build", 8 - "preview": "vite preview", 9 - "publish": "pnpm run build && ./scripts/publish.sh" 10 - }, 11 - "dependencies": { 12 - "@atcute/client": "^2.0.6", 13 - "bluesky-post-embed": "workspace:^", 14 - "bluesky-profile-feed-embed": "workspace:^", 15 - "internal": "workspace:^", 16 - "preact": "^10.25.1" 17 - }, 18 - "devDependencies": { 19 - "@preact/preset-vite": "^2.9.3", 20 - "vite": "^6.0.3" 21 - } 22 - }
-18
packages/site/scripts/publish.sh
··· 1 - #!/usr/bin/env bash 2 - 3 - set -euo pipefail 4 - 5 - if [[ -n $(git status --porcelain) ]]; then 6 - echo 'Working directory is not clean' 7 - git status --short 8 - exit 1 9 - fi 10 - 11 - GIT_COMMIT=$(git rev-parse HEAD) 12 - 13 - rsync -aHAX --delete --exclude=.git --exclude=.nojekyll dist/ pages/ 14 - touch pages/.nojekyll 15 - 16 - git -C pages/ add . 17 - git -C pages/ commit -m "deploy: ${GIT_COMMIT}" 18 - git -C pages/ push
-371
packages/site/src/app.tsx
··· 1 - import { useEffect, useMemo, useState } from 'preact/hooks'; 2 - 3 - import type { AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/client/lexicons'; 4 - import { fetchPost, type PostData } from 'bluesky-post-embed/core'; 5 - import { fetchProfileFeed, type ProfileFeedData } from 'bluesky-profile-feed-embed/core'; 6 - 7 - import { getPostUrl } from 'internal/utils/bsky-url.ts'; 8 - import { formatLongDate } from 'internal/utils/date.ts'; 9 - import { parseAtUri } from 'internal/utils/syntax/at-url.ts'; 10 - 11 - import { escapeHtml } from './utils/html'; 12 - import { isValidAtIdentifier, isValidRecordKey } from './utils/strings'; 13 - 14 - import BlueskyPost from './components/bluesky-post'; 15 - import BlueskyProfileFeed from './components/bluesky-profile-feed'; 16 - import CodeBlock from './components/code-block'; 17 - 18 - const DEFAULT_URL = 'https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd/post/3kj2umze7zj2n'; 19 - 20 - const App = () => { 21 - const [urlInput, setUrlInput] = useState(''); 22 - 23 - const matched = useMemo(() => extractUrl(urlInput || DEFAULT_URL), [urlInput]); 24 - 25 - return ( 26 - <div class="app"> 27 - <h1 class="header"> 28 - <code>&lt;bluesky-embed&gt;</code> 29 - </h1> 30 - 31 - <label class="input-wrapper"> 32 - <span class="label">Bluesky post or profile URL</span> 33 - <input 34 - type="url" 35 - placeholder={DEFAULT_URL} 36 - value={urlInput} 37 - onInput={(ev) => setUrlInput(ev.currentTarget.value)} 38 - class="text-input" 39 - /> 40 - </label> 41 - 42 - {!matched ? ( 43 - <main class="main"> 44 - <div class="alert">Invalid URL, did you type it correctly?</div> 45 - </main> 46 - ) : matched.type === 'post' ? ( 47 - <PostEmbedding key={urlInput} matched={matched} /> 48 - ) : matched.type === 'profile' ? ( 49 - <ProfileFeedEmbedding key={urlInput} matched={matched} /> 50 - ) : null} 51 - 52 - <footer class="footer"> 53 - <span> 54 - made with ❤️ by <a href="https://bsky.app/profile/did:plc:ia76kvnndjutgedggx2ibrem">@mary.my.id</a> 55 - </span> 56 - <span aria-hidden="true"> · </span> 57 - <span> 58 - <a href="https://github.com/mary-ext/bluesky-embed">source code</a> 59 - </span> 60 - <span aria-hidden="true"> · </span> 61 - <span>MIT License</span> 62 - </footer> 63 - </div> 64 - ); 65 - }; 66 - 67 - export default App; 68 - 69 - const CircularSpinner = () => { 70 - return ( 71 - <svg viewBox="0 0 32 32" class="circular-spinner"> 72 - <circle cx="16" cy="16" fill="none" r="14" stroke-width="4" class="background" /> 73 - <circle 74 - cx="16" 75 - cy="16" 76 - fill="none" 77 - r="14" 78 - stroke-width="4" 79 - stroke-dasharray="80px" 80 - stroke-dashoffset="60px" 81 - class="accented" 82 - /> 83 - </svg> 84 - ); 85 - }; 86 - 87 - // #region Post embedding 88 - type PostEmbeddingResult = { ok: true; data: PostData } | { ok: false; message: string }; 89 - 90 - const PostEmbedding = ({ matched }: { matched: ExtractedPostInfo }) => { 91 - const [result, setResult] = useState<PostEmbeddingResult>(); 92 - 93 - useEffect(() => { 94 - if (result) { 95 - return; 96 - } 97 - 98 - const controller = new AbortController(); 99 - const signal = controller.signal; 100 - 101 - const promise = fetchPost({ 102 - uri: `at://${matched.author}/app.bsky.feed.post/${matched.rkey}`, 103 - signal: signal, 104 - contextless: false, 105 - }); 106 - 107 - promise.then( 108 - (data) => { 109 - if (signal.aborted) { 110 - return; 111 - } 112 - 113 - setResult({ ok: true, data }); 114 - }, 115 - (err) => { 116 - if (signal.aborted) { 117 - return; 118 - } 119 - 120 - setResult({ ok: false, message: '' + err }); 121 - }, 122 - ); 123 - 124 - return () => controller.abort(); 125 - }, [matched, result]); 126 - 127 - return ( 128 - <main class="main"> 129 - {!result ? ( 130 - <CircularSpinner /> 131 - ) : !result.ok ? ( 132 - <div class="alert">{result.message}</div> 133 - ) : ( 134 - <> 135 - <BlueskyPost data={result.data} /> 136 - 137 - {result.data.thread ? ( 138 - <div class="guide"> 139 - <h4 class="guide-header">How do I embed this to my website?</h4> 140 - 141 - <div class="inform"> 142 - Doing server-side rendering? Check out examples for{' '} 143 - <a href="https://github.com/mary-ext/bluesky-embed-astro">Astro</a> and{' '} 144 - <a href="https://github.com/mary-ext/bluesky-embed-sveltekit">SvelteKit</a>. 145 - </div> 146 - 147 - <ol class="guide-instructions"> 148 - <li> 149 - <p> 150 - Insert the following scripts and stylesheets to the <code>&lt;head&gt;</code> of your 151 - website. 152 - </p> 153 - <CodeBlock code={getPrerequisitePostMarkup()} /> 154 - </li> 155 - 156 - <li> 157 - <p>Insert the following markup in wherever you want the post to be.</p> 158 - <CodeBlock code={getPostMarkup(result.data.thread.post)} /> 159 - </li> 160 - </ol> 161 - </div> 162 - ) : null} 163 - </> 164 - )} 165 - </main> 166 - ); 167 - }; 168 - 169 - const getPrerequisitePostMarkup = () => { 170 - const JSDELIVR_URL = `https://cdn.jsdelivr.net/npm/bluesky-post-embed@^1.0.0`; 171 - 172 - return `<!-- Core web component and styling --> 173 - <script type="module" src="${JSDELIVR_URL}/+esm"></script> 174 - <link rel="stylesheet" href="${JSDELIVR_URL}/dist/core.min.css"> 175 - 176 - <!-- Built-in themes --> 177 - <link rel="stylesheet" href="${JSDELIVR_URL}/themes/light.min.css" media="(prefers-color-scheme: light)"> 178 - <link rel="stylesheet" href="${JSDELIVR_URL}/themes/dim.min.css" media="(prefers-color-scheme: dark)"> 179 - 180 - <!-- Fallback/placeholder elements if JS script is taking a while to load or is failing --> 181 - <style> 182 - .bluesky-post-fallback { 183 - margin: 16px 0; 184 - border-left: 3px solid var(--divider); 185 - padding: 4px 8px; 186 - white-space: pre-wrap; 187 - overflow-wrap: break-word; 188 - } 189 - .bluesky-post-fallback p { 190 - margin: 0 0 8px 0; 191 - } 192 - </style> 193 - `; 194 - }; 195 - 196 - const getPostMarkup = (post: AppBskyFeedDefs.PostView) => { 197 - const author = post.author; 198 - const record = post.record as AppBskyFeedPost.Record; 199 - 200 - return `<bluesky-post src="${escapeHtml(post.uri)}"> 201 - <blockquote class="bluesky-post-fallback"> 202 - <p dir="auto">${escapeHtml(record.text)}</p> 203 - — ${author.displayName?.trim() ? `${escapeHtml(author.displayName)} (@${escapeHtml(author.handle)})` : `@${escapeHtml(author.handle)}`} 204 - <a href="${escapeHtml(getPostUrl(author.did, parseAtUri(post.uri).rkey))}">${formatLongDate(post.indexedAt)}</a> 205 - </blockquote> 206 - </bluesky-post> 207 - `; 208 - }; 209 - // #endregion 210 - 211 - // #region Profile feed embedding 212 - type ProfileFeedEmbeddingResult = { ok: true; data: ProfileFeedData } | { ok: false; message: string }; 213 - 214 - const ProfileFeedEmbedding = ({ matched }: { matched: ExtractedProfileInfo }) => { 215 - const [result, setResult] = useState<ProfileFeedEmbeddingResult>(); 216 - 217 - useEffect(() => { 218 - if (result) { 219 - return; 220 - } 221 - 222 - const controller = new AbortController(); 223 - const signal = controller.signal; 224 - 225 - const promise = fetchProfileFeed({ 226 - actor: matched.actor, 227 - signal: signal, 228 - includePins: true, 229 - }); 230 - 231 - promise.then( 232 - (data) => { 233 - if (signal.aborted) { 234 - return; 235 - } 236 - 237 - setResult({ ok: true, data }); 238 - }, 239 - (err) => { 240 - if (signal.aborted) { 241 - return; 242 - } 243 - 244 - setResult({ ok: false, message: '' + err }); 245 - }, 246 - ); 247 - 248 - return () => controller.abort(); 249 - }, [matched, result]); 250 - 251 - return ( 252 - <main class="main"> 253 - {!result ? ( 254 - <CircularSpinner /> 255 - ) : !result.ok ? ( 256 - <div class="alert">{result.message}</div> 257 - ) : ( 258 - <> 259 - <BlueskyProfileFeed data={result.data} /> 260 - 261 - {result.data.profile ? ( 262 - <div class="guide"> 263 - <h4 class="guide-header">How do I embed this to my website?</h4> 264 - 265 - <div class="inform"> 266 - Doing server-side rendering? Check out examples for{' '} 267 - <a href="https://github.com/mary-ext/bluesky-embed-astro">Astro</a> and{' '} 268 - <a href="https://github.com/mary-ext/bluesky-embed-sveltekit">SvelteKit</a>. 269 - </div> 270 - 271 - <ol class="guide-instructions"> 272 - <li> 273 - <p> 274 - Insert the following scripts and stylesheets to the <code>&lt;head&gt;</code> of your 275 - website. 276 - </p> 277 - <CodeBlock code={getPrerequisiteProfileFeedMarkup()} /> 278 - </li> 279 - 280 - <li> 281 - <p>Insert the following markup in wherever you want the profile feed to be.</p> 282 - <CodeBlock code={getProfileFeedMarkup(result.data.profile)} /> 283 - </li> 284 - </ol> 285 - </div> 286 - ) : null} 287 - </> 288 - )} 289 - </main> 290 - ); 291 - }; 292 - 293 - const getPrerequisiteProfileFeedMarkup = () => { 294 - const JSDELIVR_URL = `https://cdn.jsdelivr.net/npm/bluesky-profile-feed-embed@^1.0.0`; 295 - 296 - return `<!-- Core web component and styling --> 297 - <script type="module" src="${JSDELIVR_URL}/+esm"></script> 298 - <link rel="stylesheet" href="${JSDELIVR_URL}/dist/core.min.css"> 299 - 300 - <!-- Built-in themes --> 301 - <link rel="stylesheet" href="${JSDELIVR_URL}/themes/light.min.css" media="(prefers-color-scheme: light)"> 302 - <link rel="stylesheet" href="${JSDELIVR_URL}/themes/dim.min.css" media="(prefers-color-scheme: dark)"> 303 - `; 304 - }; 305 - 306 - const getProfileFeedMarkup = (profile: AppBskyActorDefs.ProfileViewDetailed) => { 307 - const url = `https://bsky.app/profile/${profile.did}`; 308 - 309 - return `<bluesky-profile-feed actor="${escapeHtml(profile.did)}" include-pins> 310 - <a target="_blank" href="${escapeHtml(url)}" class="bluesky-profile-feed-fallback"> 311 - ${ 312 - profile.displayName?.trim() 313 - ? `Posts by ${escapeHtml(profile.displayName)} (@${escapeHtml(profile.handle)})` 314 - : `Posts by @${escapeHtml(profile.handle)}` 315 - } 316 - </a> 317 - </bluesky-profile-feed> 318 - `; 319 - }; 320 - 321 - // #endregion 322 - 323 - type ExtractedPostInfo = { type: 'post'; author: string; rkey: string }; 324 - type ExtractedProfileInfo = { type: 'profile'; actor: string }; 325 - type ExtractedInfo = ExtractedPostInfo | ExtractedProfileInfo; 326 - 327 - const safeParseUrl = (str: string): URL | null => { 328 - let url: URL | null | undefined; 329 - if ('parse' in URL) { 330 - url = URL.parse(str); 331 - } else { 332 - try { 333 - // @ts-expect-error: `'parse' in URL` is giving truthy 334 - url = new URL(str); 335 - } catch {} 336 - } 337 - 338 - if (url && (url.protocol === 'https:' || url.protocol === 'http:')) { 339 - return url; 340 - } 341 - 342 - return null; 343 - }; 344 - 345 - const extractUrl = (str: string): ExtractedInfo | null => { 346 - const url = safeParseUrl(str); 347 - if (!url) { 348 - return null; 349 - } 350 - 351 - let match: RegExpExecArray | null | undefined; 352 - if (url.host === 'bsky.app' || url.host === 'staging.bsky.app' || url.host === 'main.bsky.dev') { 353 - if ((match = /^\/profile\/([^/]+)\/post\/([^/]+)\/?$/.exec(url.pathname))) { 354 - if (!isValidAtIdentifier(match[1]) || !isValidRecordKey(match[2])) { 355 - return null; 356 - } 357 - 358 - return { type: 'post', author: match[1], rkey: match[2] }; 359 - } 360 - 361 - if ((match = /^\/profile\/([^/]+)\/?$/.exec(url.pathname))) { 362 - if (!isValidAtIdentifier(match[1])) { 363 - return null; 364 - } 365 - 366 - return { type: 'profile', actor: match[1] }; 367 - } 368 - } 369 - 370 - return null; 371 - };
-24
packages/site/src/components/bluesky-post.tsx
··· 1 - import { useMemo } from 'preact/hooks'; 2 - 3 - import { renderPost, type PostData } from 'bluesky-post-embed/core'; 4 - import 'bluesky-post-embed/style.css'; 5 - 6 - const BlueskyPost = ({ data }: { data: PostData }) => { 7 - const html = useMemo(() => renderPost(data), [data]); 8 - 9 - return <bluesky-post src={data.thread?.post.uri} dangerouslySetInnerHTML={{ __html: html }}></bluesky-post>; 10 - }; 11 - 12 - export default BlueskyPost; 13 - 14 - declare module 'preact' { 15 - namespace JSX { 16 - interface BlueskyPostAttributes extends HTMLAttributes<HTMLElement> { 17 - src?: string; 18 - } 19 - 20 - interface IntrinsicElements { 21 - 'bluesky-post': BlueskyPostAttributes; 22 - } 23 - } 24 - }
-29
packages/site/src/components/bluesky-profile-feed.tsx
··· 1 - import { useMemo } from 'preact/hooks'; 2 - 3 - import { renderProfileFeed, type ProfileFeedData } from 'bluesky-profile-feed-embed/core'; 4 - import 'bluesky-profile-feed-embed/style.css'; 5 - 6 - const BlueskyProfileFeed = ({ data }: { data: ProfileFeedData }) => { 7 - const html = useMemo(() => renderProfileFeed(data), [data]); 8 - 9 - return ( 10 - <bluesky-profile-feed 11 - actor={data.profile?.did} 12 - dangerouslySetInnerHTML={{ __html: html }} 13 - ></bluesky-profile-feed> 14 - ); 15 - }; 16 - 17 - export default BlueskyProfileFeed; 18 - 19 - declare module 'preact' { 20 - namespace JSX { 21 - interface BlueskyProfileFeedAttributes extends HTMLAttributes<HTMLElement> { 22 - actor?: string; 23 - } 24 - 25 - interface IntrinsicElements { 26 - 'bluesky-profile-feed': BlueskyProfileFeedAttributes; 27 - } 28 - } 29 - }
-30
packages/site/src/components/code-block.tsx
··· 1 - const CodeBlock = ({ code }: { code: string }) => { 2 - return ( 3 - <div class="code-block"> 4 - <pre> 5 - <code>{code}</code> 6 - </pre> 7 - 8 - <div class="actions"> 9 - <button 10 - title="Copy" 11 - onClick={() => { 12 - navigator.clipboard.writeText(code).catch(() => alert(`Failed to copy to clipboard`)); 13 - }} 14 - class="copy-button" 15 - > 16 - <svg fill="none" viewBox="0 0 24 24"> 17 - <path 18 - stroke="currentColor" 19 - stroke-linecap="square" 20 - stroke-width="2" 21 - d="M15 5h4v16H5V5h4m0-2h6v4H9V3Z" 22 - /> 23 - </svg> 24 - </button> 25 - </div> 26 - </div> 27 - ); 28 - }; 29 - 30 - export default CodeBlock;
-8
packages/site/src/main.tsx
··· 1 - import { render } from 'preact'; 2 - 3 - import App from './app'; 4 - 5 - import 'bluesky-post-embed/themes/light.css'; 6 - import './styles/main.css'; 7 - 8 - render(<App />, document.body);
-193
packages/site/src/styles/main.css
··· 1 - @import './normalize.css'; 2 - 3 - .app { 4 - margin: 0 auto; 5 - padding: 36px 16px; 6 - width: 100%; 7 - max-width: calc(550px + (16 * 2px)); 8 - } 9 - 10 - .header { 11 - margin: 24px 0; 12 - } 13 - 14 - .input-wrapper { 15 - display: flex; 16 - flex-direction: column; 17 - gap: 8px; 18 - } 19 - .label { 20 - color: #4b5563; 21 - font-weight: 600; 22 - font-size: 0.875rem; 23 - line-height: 1.25rem; 24 - } 25 - .text-input { 26 - outline: 2px none #2563eb; 27 - outline-offset: -1px; 28 - border: 1px solid #9ca3af; 29 - border-radius: 4px; 30 - padding: 8px 12px; 31 - font-size: 0.875rem; 32 - line-height: 1.25rem; 33 - 34 - &::placeholder { 35 - color: #9ca3af; 36 - } 37 - &:focus { 38 - outline-style: solid; 39 - } 40 - } 41 - 42 - .alert { 43 - border: 1px solid #fca5a5; 44 - border-radius: 4px; 45 - background: #fee2e2; 46 - padding: 10px 12px; 47 - color: #991b1b; 48 - font-weight: 500; 49 - font-size: 0.875rem; 50 - line-height: 1.25rem; 51 - } 52 - .inform { 53 - border: 1px solid #bfdbfe; 54 - border-radius: 4px; 55 - background: #dbeafe; 56 - padding: 10px 12px; 57 - color: #1e40af; 58 - font-weight: 500; 59 - font-size: 0.875rem; 60 - line-height: 1.25rem; 61 - 62 - a { 63 - color: inherit; 64 - font-weight: 600; 65 - } 66 - } 67 - 68 - .circular-spinner { 69 - display: block; 70 - animation: spin 1s linear infinite; 71 - margin: 0 auto; 72 - width: 24px; 73 - height: 24px; 74 - 75 - .accented { 76 - stroke: #2563eb; 77 - } 78 - .background { 79 - stroke: #2563eb; 80 - opacity: 20%; 81 - } 82 - } 83 - @keyframes spin { 84 - to { 85 - transform: rotate(360deg); 86 - } 87 - } 88 - 89 - .main { 90 - margin: 36px 0 0 0; 91 - } 92 - 93 - .guide { 94 - margin: 36px 0 0 0; 95 - border-top: 1px solid #d1d5db; 96 - } 97 - .guide-header { 98 - margin: 36px 0 16px 0; 99 - } 100 - .guide-instructions { 101 - margin: 24px 0 0 0; 102 - padding: 0 0 0 22px; 103 - font-size: 0.875rem; 104 - line-height: 1.25rem; 105 - 106 - li + li { 107 - margin: 24px 0 0 0; 108 - } 109 - } 110 - 111 - .code-block { 112 - display: flex; 113 - gap: 12px; 114 - border: 1px solid #d1d5db; 115 - border-radius: 4px; 116 - background: #f9fafb; 117 - padding: 12px; 118 - overflow: hidden; 119 - overflow-x: auto; 120 - 121 - pre { 122 - flex-grow: 1; 123 - margin: 0; 124 - font-size: 0.75rem; 125 - line-height: 1.25rem; 126 - } 127 - 128 - .actions { 129 - position: sticky; 130 - top: 0; 131 - right: 0; 132 - 133 - button { 134 - display: flex; 135 - justify-content: center; 136 - align-items: center; 137 - cursor: pointer; 138 - box-shadow: 139 - 0 1px 3px 0 rgb(0 0 0 / 0.1), 140 - 0 1px 2px -1px rgb(0 0 0 / 0.1); 141 - border: 1px solid #d1d5db; 142 - border-radius: 4px; 143 - background: #ffffff; 144 - padding: 0; 145 - width: 32px; 146 - height: 32px; 147 - color: #4b5563; 148 - 149 - @media (pointer: fine) { 150 - opacity: 0; 151 - transition: 75ms ease-in; 152 - 153 - .code-block:hover &, 154 - .code-block:focus-within & { 155 - opacity: 1; 156 - } 157 - 158 - &:hover { 159 - border-color: #9ca3af; 160 - background: #e5e7eb; 161 - color: #1f2937; 162 - } 163 - } 164 - 165 - &:active { 166 - border-color: #9ca3af; 167 - background: #e5e7eb; 168 - color: #1f2937; 169 - } 170 - } 171 - 172 - svg { 173 - width: 16px; 174 - height: 16px; 175 - } 176 - } 177 - } 178 - 179 - .footer { 180 - display: flex; 181 - flex-wrap: wrap; 182 - gap: 0.5rem; 183 - margin: 36px 0 0 0; 184 - border-top: 1px solid #d1d5db; 185 - padding: 36px 0 0 0; 186 - color: #4b5563; 187 - font-size: 0.875rem; 188 - line-height: 1.25rem; 189 - 190 - a { 191 - color: #2563eb; 192 - } 193 - }
-199
packages/site/src/styles/normalize.css
··· 1 - /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ 2 - 3 - /* 4 - Document 5 - ======== 6 - */ 7 - 8 - /** 9 - Use a better box model (opinionated). 10 - */ 11 - 12 - *, 13 - ::before, 14 - ::after { 15 - box-sizing: border-box; 16 - } 17 - 18 - html { 19 - line-height: 1.15; /* 1. Correct the line height in all browsers. */ 20 - /* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ 21 - font-family: 'Inter', 'Roboto', ui-sans-serif, sans-serif, 'Noto Color Emoji', 'Twemoji Mozilla'; 22 - -webkit-text-size-adjust: 100%; /* 2. Prevent adjustments of font size after orientation changes in iOS. */ 23 - tab-size: 4; /* 3. Use a more readable tab size (opinionated). */ 24 - } 25 - 26 - /* 27 - Sections 28 - ======== 29 - */ 30 - 31 - body { 32 - margin: 0; /* Remove the margin in all browsers. */ 33 - } 34 - 35 - /* 36 - Text-level semantics 37 - ==================== 38 - */ 39 - 40 - /** 41 - Add the correct font weight in Chrome and Safari. 42 - */ 43 - 44 - b, 45 - strong { 46 - font-weight: bolder; 47 - } 48 - 49 - /** 50 - 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 51 - 2. Correct the odd 'em' font sizing in all browsers. 52 - */ 53 - 54 - code, 55 - kbd, 56 - samp, 57 - pre { 58 - font-size: 1em; /* 2 */ 59 - font-family: 'JetBrains Mono NL', ui-monospace, monospace; /* 1 */ 60 - } 61 - 62 - /** 63 - Add the correct font size in all browsers. 64 - */ 65 - 66 - small { 67 - font-size: 80%; 68 - } 69 - 70 - /** 71 - Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. 72 - */ 73 - 74 - sub, 75 - sup { 76 - position: relative; 77 - vertical-align: baseline; 78 - font-size: 75%; 79 - line-height: 0; 80 - } 81 - 82 - sub { 83 - bottom: -0.25em; 84 - } 85 - 86 - sup { 87 - top: -0.5em; 88 - } 89 - 90 - /* 91 - Tabular data 92 - ============ 93 - */ 94 - 95 - /** 96 - Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016) 97 - */ 98 - 99 - table { 100 - border-color: currentcolor; 101 - } 102 - 103 - /* 104 - Forms 105 - ===== 106 - */ 107 - 108 - /** 109 - 1. Change the font styles in all browsers. 110 - 2. Remove the margin in Firefox and Safari. 111 - */ 112 - 113 - button, 114 - input, 115 - optgroup, 116 - select, 117 - textarea { 118 - margin: 0; /* 2 */ 119 - font-size: 100%; /* 1 */ 120 - line-height: 1.15; /* 1 */ 121 - font-family: inherit; /* 1 */ 122 - } 123 - 124 - /** 125 - Correct the inability to style clickable types in iOS and Safari. 126 - */ 127 - 128 - button, 129 - [type='button'], 130 - [type='reset'], 131 - [type='submit'] { 132 - -webkit-appearance: button; 133 - } 134 - 135 - /** 136 - Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. 137 - */ 138 - 139 - legend { 140 - padding: 0; 141 - } 142 - 143 - /** 144 - Add the correct vertical alignment in Chrome and Firefox. 145 - */ 146 - 147 - progress { 148 - vertical-align: baseline; 149 - } 150 - 151 - /** 152 - Correct the cursor style of increment and decrement buttons in Safari. 153 - */ 154 - 155 - ::-webkit-inner-spin-button, 156 - ::-webkit-outer-spin-button { 157 - height: auto; 158 - } 159 - 160 - /** 161 - 1. Correct the odd appearance in Chrome and Safari. 162 - 2. Correct the outline style in Safari. 163 - */ 164 - 165 - [type='search'] { 166 - -webkit-appearance: textfield; /* 1 */ 167 - outline-offset: -2px; /* 2 */ 168 - } 169 - 170 - /** 171 - Remove the inner padding in Chrome and Safari on macOS. 172 - */ 173 - 174 - ::-webkit-search-decoration { 175 - -webkit-appearance: none; 176 - } 177 - 178 - /** 179 - 1. Correct the inability to style clickable types in iOS and Safari. 180 - 2. Change font properties to 'inherit' in Safari. 181 - */ 182 - 183 - ::-webkit-file-upload-button { 184 - -webkit-appearance: button; /* 1 */ 185 - font: inherit; /* 2 */ 186 - } 187 - 188 - /* 189 - Interactive 190 - =========== 191 - */ 192 - 193 - /* 194 - Add the correct display in Chrome and Safari. 195 - */ 196 - 197 - summary { 198 - display: list-item; 199 - }
-3
packages/site/src/utils/html.ts
··· 1 - export const escapeHtml = (text: string): string => { 2 - return text.replace(/[<"&]/g, (c) => '&#' + c.charCodeAt(0) + ';'); 3 - };
-22
packages/site/src/utils/strings.ts
··· 1 - export const RECORD_KEY_RE = /^(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}$/; 2 - 3 - export const isValidRecordKey = (str: string): boolean => { 4 - return str.length >= 1 && str.length <= 512 && RECORD_KEY_RE.test(str); 5 - }; 6 - 7 - export const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$/; 8 - 9 - export const isValidDid = (str: string): boolean => { 10 - return str.length >= 7 && str.length <= 2048 && DID_RE.test(str); 11 - }; 12 - 13 - export const HANDLE_RE = 14 - /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; 15 - 16 - export const isValidHandle = (str: string): boolean => { 17 - return str.length >= 3 && str.length <= 253 && HANDLE_RE.test(str); 18 - }; 19 - 20 - export const isValidAtIdentifier = (str: string): boolean => { 21 - return isValidDid(str) || isValidHandle(str); 22 - };
-1
packages/site/src/vite-env.d.ts
··· 1 - /// <reference types="vite/client" />
-31
packages/site/tsconfig.app.json
··· 1 - { 2 - "compilerOptions": { 3 - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 - "target": "ES2020", 5 - "useDefineForClassFields": true, 6 - "module": "ESNext", 7 - "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 - "skipLibCheck": true, 9 - "paths": { 10 - "react": ["./node_modules/preact/compat/"], 11 - "react-dom": ["./node_modules/preact/compat/"], 12 - }, 13 - 14 - /* Bundler mode */ 15 - "moduleResolution": "bundler", 16 - "allowImportingTsExtensions": true, 17 - "isolatedModules": true, 18 - "moduleDetection": "force", 19 - "noEmit": true, 20 - "jsx": "react-jsx", 21 - "jsxImportSource": "preact", 22 - 23 - /* Linting */ 24 - "strict": true, 25 - "noUnusedLocals": true, 26 - "noUnusedParameters": true, 27 - "noFallthroughCasesInSwitch": true, 28 - "noUncheckedSideEffectImports": true, 29 - }, 30 - "include": ["src"], 31 - }
-4
packages/site/tsconfig.json
··· 1 - { 2 - "files": [], 3 - "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], 4 - }
-24
packages/site/tsconfig.node.json
··· 1 - { 2 - "compilerOptions": { 3 - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 - "target": "ES2022", 5 - "lib": ["ES2023"], 6 - "module": "ESNext", 7 - "skipLibCheck": true, 8 - 9 - /* Bundler mode */ 10 - "moduleResolution": "bundler", 11 - "allowImportingTsExtensions": true, 12 - "isolatedModules": true, 13 - "moduleDetection": "force", 14 - "noEmit": true, 15 - 16 - /* Linting */ 17 - "strict": true, 18 - "noUnusedLocals": true, 19 - "noUnusedParameters": true, 20 - "noFallthroughCasesInSwitch": true, 21 - "noUncheckedSideEffectImports": true, 22 - }, 23 - "include": ["vite.config.ts"], 24 - }
-11
packages/site/vite.config.ts
··· 1 - import { defineConfig } from 'vite'; 2 - import preact from '@preact/preset-vite'; 3 - 4 - // https://vite.dev/config/ 5 - export default defineConfig({ 6 - base: './', 7 - build: { 8 - target: 'esnext', 9 - }, 10 - plugins: [preact()], 11 - });
-25
pnpm-lock.yaml
··· 167 167 specifier: 'catalog:' 168 168 version: 5.8.1(patch_hash=6qynve6ufonlwufsl6x7wujmdu) 169 169 170 - packages/site: 171 - dependencies: 172 - '@atcute/client': 173 - specifier: ^2.0.6 174 - version: 2.0.6 175 - bluesky-post-embed: 176 - specifier: workspace:^ 177 - version: link:../bluesky-post-embed 178 - bluesky-profile-feed-embed: 179 - specifier: workspace:^ 180 - version: link:../bluesky-profile-feed-embed 181 - internal: 182 - specifier: workspace:^ 183 - version: link:../internal 184 - preact: 185 - specifier: ^10.25.1 186 - version: 10.25.1 187 - devDependencies: 188 - '@preact/preset-vite': 189 - specifier: ^2.9.3 190 - version: 2.9.3(@babel/core@7.26.0)(preact@10.25.1)(vite@6.0.3(@types/node@22.10.1)(terser@5.37.0)) 191 - vite: 192 - specifier: ^6.0.3 193 - version: 6.0.3(@types/node@22.10.1)(terser@5.37.0) 194 - 195 170 packages/svelte-site: 196 171 dependencies: 197 172 '@atcute/bluesky':
-1
pnpm-workspace.yaml
··· 1 1 packages: 2 2 - packages/internal 3 - - packages/site 4 3 - packages/svelte-site 5 4 - packages/bluesky-post-embed 6 5 - packages/bluesky-profile-card-embed