diff --git a/scripts/generate.ts b/scripts/generate.ts index 7b19fbb..d0f64f7 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -2,7 +2,9 @@ import * as fs from 'fs' import * as path from 'path' import { marked, Renderer } from 'marked' import type { AppConfig, Profile, BlogPost, Networks } from '../src/types.ts' -import { escapeHtml } from '../src/lib/utils.ts' +import { escapeHtml, formatDate } from '../src/lib/utils.ts' +import { LANG_ICON, BUILTIN_ICONS } from '../src/lib/icons.ts' +import { MAX_SEARCH_LENGTH } from '../src/lib/constants.ts' // Highlight.js for syntax highlighting (core + common languages only) let hljs: typeof import('highlight.js/lib/core').default @@ -70,15 +72,6 @@ function setupMarked() { }) } -function formatDate(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('ja-JP', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - // API functions async function resolveHandle(handle: string, bskyUrl: string): Promise { const res = await fetch(`${bskyUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`) @@ -396,12 +389,10 @@ function generateTabsHtml(activeTab: 'blog' | 'browser', handle: string): string } function generateLangSelectorHtml(): string { - const langIcon = `` - return `
@@ -484,8 +475,6 @@ function generatePostDetailHtml(post: BlogPost, handle: string, collection: stri const postUrl = `${baseSiteUrl}/post/${rkey}/` const appUrl = getAppUrl(network) // Convert to search-friendly format (domain/post/rkey_prefix without https://) - // Keep total length around 20 chars to avoid URL truncation in posts - const MAX_SEARCH_LENGTH = 20 const urlObj = new URL(postUrl) const pathParts = urlObj.pathname.split('/').filter(Boolean) const basePath = urlObj.host + '/' + (pathParts[0] || '') + '/' @@ -550,14 +539,6 @@ function loadLinks(): FooterLink[] { } } -// Built-in SVG icons for common services -const BUILTIN_ICONS: Record = { - bluesky: ``, - github: ``, - ai: ``, - git: ``, -} - function generateFooterLinksHtml(links: FooterLink[]): string { if (links.length === 0) return '' diff --git a/src/components/browser.ts b/src/components/browser.ts index 899ed4d..26ed08b 100644 --- a/src/components/browser.ts +++ b/src/components/browser.ts @@ -86,16 +86,3 @@ export function mountHeader( }) } } - -// Keep old function for compatibility -export function mountBrowser( - container: HTMLElement, - currentHandle: string, - onSubmit: (handle: string) => void -): void { - mountHeader(container, currentHandle, false, undefined, { - onBrowse: onSubmit, - onLogin: () => {}, - onLogout: () => {} - }) -} diff --git a/src/components/discussion.ts b/src/components/discussion.ts index 6eb1e94..f8a30a7 100644 --- a/src/components/discussion.ts +++ b/src/components/discussion.ts @@ -1,5 +1,6 @@ import { searchPostsForUrl } from '../lib/api.js' -import { escapeHtml } from '../lib/utils.js' +import { escapeHtml, formatDate } from '../lib/utils.js' +import { MAX_SEARCH_LENGTH, DISCUSSION_POST_LIMIT } from '../lib/constants.js' // Map network to app URL export function getAppUrl(network: string): string { @@ -9,15 +10,6 @@ export function getAppUrl(network: string): string { return 'https://bsky.app' } -function formatDate(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('ja-JP', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - function getPostUrl(uri: string, appUrl: string): string { // at://did:plc:xxx/app.bsky.feed.post/rkey -> {appUrl}/profile/did:plc:xxx/post/rkey const parts = uri.replace('at://', '').split('/') @@ -29,8 +21,6 @@ function getPostUrl(uri: string, appUrl: string): string { export function renderDiscussionLink(postUrl: string, appUrl: string = 'https://bsky.app'): string { // Convert full URL to search-friendly format (domain/post/rkey_prefix without https://) - // Keep total length around 20 chars to avoid URL truncation in posts - const MAX_SEARCH_LENGTH = 20 let searchQuery = postUrl try { const urlObj = new URL(postUrl) @@ -74,7 +64,7 @@ export async function loadDiscussionPosts(container: HTMLElement, postUrl: strin return } - const postsHtml = posts.slice(0, 10).map(post => { + const postsHtml = posts.slice(0, DISCUSSION_POST_LIMIT).map(post => { const author = post.author const avatar = author.avatar || '' const displayName = author.displayName || author.handle diff --git a/src/components/posts.ts b/src/components/posts.ts index 360103e..230c4c5 100644 --- a/src/components/posts.ts +++ b/src/components/posts.ts @@ -1,18 +1,9 @@ import type { BlogPost } from '../types.js' import { putRecord } from '../lib/auth.js' import { renderMarkdown } from '../lib/markdown.js' -import { escapeHtml } from '../lib/utils.js' +import { escapeHtml, formatDate } from '../lib/utils.js' import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js' -function formatDate(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('ja-JP', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - export function mountPostList(container: HTMLElement, posts: BlogPost[]): void { if (posts.length === 0) { container.innerHTML = '

No posts yet

' diff --git a/src/lib/api.ts b/src/lib/api.ts index 62b2c40..809e1f5 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,6 @@ import { AtpAgent } from '@atproto/api' import type { Profile, BlogPost, NetworkConfig } from '../types.js' +import { FALLBACK_PLCS, FALLBACK_BSKY_ENDPOINTS, SEARCH_TIMEOUT_MS } from './constants.js' const agents: Map = new Map() @@ -24,18 +25,6 @@ export function getAgent(service: string): AtpAgent { return agents.get(service)! } -// Fallback PLC directories -const FALLBACK_PLCS = [ - 'https://plc.directory', - 'https://plc.syu.is', -] - -// Fallback endpoints for handle/profile resolution -const FALLBACK_ENDPOINTS = [ - 'https://public.api.bsky.app', - 'https://bsky.syu.is', -] - export async function resolvePds(did: string): Promise { // Try current PLC first, then fallbacks const plcs = [getPlc(), ...FALLBACK_PLCS.filter(p => p !== getPlc())] @@ -64,7 +53,7 @@ export async function resolveHandle(handle: string): Promise { return res.data.did } catch { // Try fallback endpoints - for (const endpoint of FALLBACK_ENDPOINTS) { + for (const endpoint of FALLBACK_BSKY_ENDPOINTS) { if (endpoint === getBsky()) continue // Skip if same as current try { const agent = getAgent(endpoint) @@ -80,7 +69,7 @@ export async function resolveHandle(handle: string): Promise { export async function getProfile(actor: string): Promise { // Try current network first - const endpoints = [getBsky(), ...FALLBACK_ENDPOINTS.filter(e => e !== getBsky())] + const endpoints = [getBsky(), ...FALLBACK_BSKY_ENDPOINTS.filter(e => e !== getBsky())] for (const endpoint of endpoints) { try { @@ -273,7 +262,7 @@ export async function searchPostsForUrl(url: string): Promise { searchQueries.map(async query => { try { const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout + const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS) const res = await fetch( `${endpoint}/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(query)}&limit=20`, diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..8a21aba --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,19 @@ +// API limits +export const API_RECORD_LIMIT = 100 +export const POST_LIST_LIMIT = 50 +export const DISCUSSION_POST_LIMIT = 10 + +// Search +export const MAX_SEARCH_LENGTH = 20 +export const SEARCH_TIMEOUT_MS = 5000 + +// Fallback endpoints +export const FALLBACK_PLCS = [ + 'https://plc.directory', + 'https://plc.syu.is', +] + +export const FALLBACK_BSKY_ENDPOINTS = [ + 'https://public.api.bsky.app', + 'https://bsky.syu.is', +] diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 0000000..17743ee --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,17 @@ +// Shared icon definitions + +export const LANG_ICON = `` + +export const DISCUSS_ICON = `` + +export const LOGIN_ICON = `` + +export const LOGOUT_ICON = `` + +// Footer link icons +export const BUILTIN_ICONS: Record = { + bluesky: ``, + github: ``, + ai: ``, + git: ``, +} diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 6c6444e..d586c35 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -1,5 +1,6 @@ import { marked, Renderer } from 'marked' import hljs from 'highlight.js/lib/core' +import { escapeHtml } from './utils.js' // Import only common languages import javascript from 'highlight.js/lib/languages/javascript' @@ -53,13 +54,6 @@ renderer.code = function({ text, lang }: { text: string; lang?: string }) { return `
${highlighted}
` } -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') -} - marked.setOptions({ breaks: true, gfm: true, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cf2a61a..2eb58e1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,3 +5,12 @@ export function escapeHtml(str: string): string { .replace(/>/g, '>') .replace(/"/g, '"') } + +export function formatDate(dateStr: string): string { + const date = new Date(dateStr) + return date.toLocaleDateString('ja-JP', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) +} diff --git a/src/main.ts b/src/main.ts index 26cf135..65ed7ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ import { mountPostForm } from './components/postform.js' import { loadDiscussionPosts } from './components/discussion.js' import { parseRoute, type Route } from './lib/router.js' import { escapeHtml } from './lib/utils.js' +import { LANG_ICON, BUILTIN_ICONS } from './lib/icons.js' import type { AppConfig, Networks } from './types.js' let authSession: AuthSession | null = null @@ -56,12 +57,6 @@ async function loadLinks(): Promise { } } -const BUILTIN_ICONS: Record = { - bluesky: ``, - github: ``, - ai: ``, - git: ``, -} function renderFooterLinks(links: FooterLink[]): string { if (links.length === 0) return '' @@ -146,12 +141,10 @@ function renderLangSelector(): string {
` }).join('') - const langIcon = `` - return `
${options}