diff --git a/index.html b/index.html index deceb33..939a9c9 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ syui.ai + diff --git a/public/links.json b/public/links.json new file mode 100644 index 0000000..6449462 --- /dev/null +++ b/public/links.json @@ -0,0 +1,9 @@ +{ + "links": [ + { + "name": "git", + "url": "https://git.syui.ai/ai/log", + "icon": "ai" + } + ] +} diff --git a/public/networks.json b/public/networks.json index f806e10..62de6a3 100644 --- a/public/networks.json +++ b/public/networks.json @@ -1,10 +1,12 @@ { "bsky.social": { "plc": "https://plc.directory", - "bsky": "https://public.api.bsky.app" + "bsky": "https://public.api.bsky.app", + "web": "https://bsky.app" }, "syu.is": { "plc": "https://plc.syu.is", - "bsky": "https://bsky.syu.is" + "bsky": "https://bsky.syu.is", + "web": "https://syu.is" } } diff --git a/public/pkg/icomoon/fonts/icomoon.eot b/public/pkg/icomoon/fonts/icomoon.eot new file mode 100644 index 0000000..95caa7b Binary files /dev/null and b/public/pkg/icomoon/fonts/icomoon.eot differ diff --git a/public/pkg/icomoon/fonts/icomoon.svg b/public/pkg/icomoon/fonts/icomoon.svg new file mode 100644 index 0000000..665487e --- /dev/null +++ b/public/pkg/icomoon/fonts/icomoon.svg @@ -0,0 +1,34 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/pkg/icomoon/fonts/icomoon.ttf b/public/pkg/icomoon/fonts/icomoon.ttf new file mode 100644 index 0000000..c0fa4d6 Binary files /dev/null and b/public/pkg/icomoon/fonts/icomoon.ttf differ diff --git a/public/pkg/icomoon/fonts/icomoon.woff b/public/pkg/icomoon/fonts/icomoon.woff new file mode 100644 index 0000000..e31da24 Binary files /dev/null and b/public/pkg/icomoon/fonts/icomoon.woff differ diff --git a/public/pkg/icomoon/style.css b/public/pkg/icomoon/style.css new file mode 100644 index 0000000..580d1b4 --- /dev/null +++ b/public/pkg/icomoon/style.css @@ -0,0 +1,99 @@ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?mxezzh'); + src: url('fonts/icomoon.eot?mxezzh#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?mxezzh') format('truetype'), + url('fonts/icomoon.woff?mxezzh') format('woff'), + url('fonts/icomoon.svg?mxezzh#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-git:before { + content: "\e901"; +} +.icon-cube:before { + content: "\e900"; +} +.icon-game:before { + content: "\e9d5"; +} +.icon-card:before { + content: "\e9d6"; +} +.icon-book:before { + content: "\e9d7"; +} +.icon-git1:before { + content: "\e9d3"; +} +.icon-moji_a:before { + content: "\e9c3"; +} +.icon-archlinux:before { + content: "\e9c4"; +} +.icon-archlinuxjp:before { + content: "\e9c5"; +} +.icon-syui:before { + content: "\e9c6"; +} +.icon-phoenix-power:before { + content: "\e9c7"; +} +.icon-phoenix-world:before { + content: "\e9c8"; +} +.icon-power:before { + content: "\e9c9"; +} +.icon-phoenix:before { + content: "\e9ca"; +} +.icon-honeycomb:before { + content: "\e9cb"; +} +.icon-ai:before { + content: "\e9cc"; +} +.icon-robot:before { + content: "\e9cd"; +} +.icon-sandar:before { + content: "\e9ce"; +} +.icon-moon:before { + content: "\e9cf"; +} +.icon-home:before { + content: "\e9d0"; +} +.icon-cloud:before { + content: "\e9d1"; +} +.icon-api:before { + content: "\e9d2"; +} +.icon-aibadge:before { + content: "\ebf8"; +} +.icon-aiterm:before { + content: "\ebf7"; +} diff --git a/scripts/generate.ts b/scripts/generate.ts index 61e6dfb..b51bcff 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -297,6 +297,7 @@ function generateHtml(title: string, content: string, config: AppConfig, assets: ${escapeHtml(title)} - ${escapeHtml(config.title)} + ${config.color ? `` : ''} @@ -309,16 +310,20 @@ function generateHtml(title: string, content: string, config: AppConfig, assets: ` } -function generateProfileHtml(profile: Profile): string { +function generateProfileHtml(profile: Profile, webUrl?: string): string { const avatar = profile.avatar ? `` : '' + const profileLink = webUrl ? `${webUrl}/profile/${profile.did}` : null + const handleHtml = profileLink + ? `@${escapeHtml(profile.handle)}` + : `@${escapeHtml(profile.handle)}` return `
${avatar}
${escapeHtml(profile.displayName || profile.handle)}
-
@${escapeHtml(profile.handle)}
+
${handleHtml}
${profile.description ? `
${escapeHtml(profile.description)}
` : ''}
@@ -457,46 +462,98 @@ function generatePostDetailHtml(post: BlogPost, handle: string, collection: stri ` } -function generateFooterHtml(handle: string): string { +interface FooterLink { + name: string + url: string + icon?: string +} + +function loadLinks(): FooterLink[] { + const linksPath = path.join(process.cwd(), 'public/links.json') + if (!fs.existsSync(linksPath)) return [] + try { + const data = JSON.parse(fs.readFileSync(linksPath, 'utf-8')) + return data.links || [] + } catch { + return [] + } +} + +// 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 '' + + const items = links.map(link => { + let iconHtml = '' + if (link.icon && BUILTIN_ICONS[link.icon]) { + iconHtml = BUILTIN_ICONS[link.icon] + } else { + // Fallback to favicon + const domain = new URL(link.url).hostname + iconHtml = `` + } + return ` + + ${iconHtml} + + ` + }).join('') + + return ` + + ` +} + +function generateFooterHtml(handle: string, links: FooterLink[]): string { const username = handle.split('.')[0] || handle return ` + ${generateFooterLinksHtml(links)} ` } -function generateIndexPageContent(profile: Profile, posts: BlogPost[], config: AppConfig, collections: string[]): string { +function generateIndexPageContent(profile: Profile, posts: BlogPost[], config: AppConfig, collections: string[], links: FooterLink[], webUrl?: string): string { return `
${generateTabsHtml('blog', config.handle)} - ${generateProfileHtml(profile)} + ${generateProfileHtml(profile, webUrl)} ${generateServicesHtml(profile.did, config.handle, collections)}
${generatePostListHtml(posts)}
- ${generateFooterHtml(config.handle)} + ${generateFooterHtml(config.handle, links)} ` } -function generatePostPageContent(profile: Profile, post: BlogPost, config: AppConfig, collections: string[]): string { +function generatePostPageContent(profile: Profile, post: BlogPost, config: AppConfig, collections: string[], links: FooterLink[], webUrl?: string): string { return `
${generateTabsHtml('blog', config.handle)} - ${generateProfileHtml(profile)} + ${generateProfileHtml(profile, webUrl)} ${generateServicesHtml(profile.did, config.handle, collections)}
${generatePostDetailHtml(post, config.handle, config.collection, config.network, config.siteUrl)}
- ${generateFooterHtml(config.handle)} + ${generateFooterHtml(config.handle, links)} ` } @@ -615,8 +672,14 @@ async function generate() { // Load collections for services display const collections = loadCollections(did) + // Load footer links + const links = loadLinks() + + // Get web URL for profile links + const webUrl = network.web + // Generate index page - const indexContent = generateIndexPageContent(profile, posts, config, collections) + const indexContent = generateIndexPageContent(profile, posts, config, collections, links, webUrl) const indexHtml = generateHtml(config.title, indexContent, config, assets) fs.writeFileSync(path.join(distDir, 'index.html'), indexHtml) console.log('Generated: /index.html') @@ -649,7 +712,7 @@ async function generate() { fs.mkdirSync(postDir, { recursive: true }) } - const postContent = generatePostPageContent(profile, post, config, collections) + const postContent = generatePostPageContent(profile, post, config, collections, links, webUrl) const postHtml = generateHtml(post.title, postContent, config, assets) fs.writeFileSync(path.join(postDir, 'index.html'), postHtml) console.log(`Generated: /post/${rkey}/index.html`) @@ -677,7 +740,7 @@ async function generate() { console.log('Generated: /_redirects') // Copy static files - const filesToCopy = ['favicon.png', 'favicon.svg', 'config.json', 'networks.json', 'client-metadata.json'] + const filesToCopy = ['favicon.png', 'favicon.svg', 'config.json', 'networks.json', 'client-metadata.json', 'links.json'] for (const file of filesToCopy) { const src = path.join(process.cwd(), 'public', file) const dest = path.join(distDir, file) @@ -693,6 +756,13 @@ async function generate() { fs.cpSync(wellKnownSrc, wellKnownDest, { recursive: true }) } + // Copy pkg directory (icomoon fonts, etc.) + const pkgSrc = path.join(process.cwd(), 'public/pkg') + const pkgDest = path.join(distDir, 'pkg') + if (fs.existsSync(pkgSrc)) { + fs.cpSync(pkgSrc, pkgDest, { recursive: true }) + } + // Copy favicons from content const faviconSrc = getFaviconDir(did) const faviconDest = path.join(distDir, 'favicons') diff --git a/src/components/profile.ts b/src/components/profile.ts index 6ba2aad..4434a2b 100644 --- a/src/components/profile.ts +++ b/src/components/profile.ts @@ -1,18 +1,23 @@ import type { Profile } from '../types.js' +import { escapeHtml } from '../lib/utils.js' -export function renderProfile(profile: Profile): string { +export function renderProfile(profile: Profile, webUrl?: string): string { + const profileLink = webUrl ? `${webUrl}/profile/${profile.did}` : null + const handleHtml = profileLink + ? `@${escapeHtml(profile.handle)}` + : `@${escapeHtml(profile.handle)}` return `
- ${profile.avatar ? `avatar` : ''} + ${profile.avatar ? `avatar` : ''}
-

${profile.displayName || profile.handle}

-

@${profile.handle}

- ${profile.description ? `

${profile.description}

` : ''} +

${escapeHtml(profile.displayName || profile.handle)}

+

${handleHtml}

+ ${profile.description ? `

${escapeHtml(profile.description)}

` : ''}
` } -export function mountProfile(container: HTMLElement, profile: Profile): void { - container.innerHTML = renderProfile(profile) +export function mountProfile(container: HTMLElement, profile: Profile, webUrl?: string): void { + container.innerHTML = renderProfile(profile, webUrl) } diff --git a/src/main.ts b/src/main.ts index 1efa29e..c2c4e60 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,10 +36,62 @@ async function loadNetworks(): Promise { return res.json() } +interface FooterLink { + name: string + url: string + icon?: string +} + +let footerLinks: FooterLink[] = [] + +async function loadLinks(): Promise { + try { + const res = await fetch('/links.json') + if (!res.ok) return [] + const data = await res.json() + return data.links || [] + } catch { + return [] + } +} + +const BUILTIN_ICONS: Record = { + bluesky: ``, + github: ``, + ai: ``, + git: ``, +} + +function renderFooterLinks(links: FooterLink[]): string { + if (links.length === 0) return '' + + const items = links.map(link => { + let iconHtml = '' + if (link.icon && BUILTIN_ICONS[link.icon]) { + iconHtml = BUILTIN_ICONS[link.icon] + } else { + try { + const domain = new URL(link.url).hostname + iconHtml = `` + } catch { + iconHtml = '' + } + } + return ` + + ${iconHtml} + + ` + }).join('') + + return `` +} + function renderFooter(handle: string): string { const parts = handle.split('.') const username = parts[0] || handle return ` + ${renderFooterLinks(footerLinks)}

© ${username}

@@ -598,10 +650,11 @@ async function render(): Promise { case 'post': try { const profile = await getProfile(config.handle) + const webUrl = networks[config.network]?.web profileEl.innerHTML = renderTabs('blog', isLoggedIn) const profileContentEl = document.createElement('div') profileEl.appendChild(profileContentEl) - mountProfile(profileContentEl, profile) + mountProfile(profileContentEl, profile, webUrl) const servicesHtml = await renderServices(config.handle) profileContentEl.insertAdjacentHTML('beforeend', servicesHtml) @@ -623,10 +676,11 @@ async function render(): Promise { default: try { const profile = await getProfile(config.handle) + const webUrl = networks[config.network]?.web profileEl.innerHTML = renderTabs('blog', isLoggedIn) const profileContentEl = document.createElement('div') profileEl.appendChild(profileContentEl) - mountProfile(profileContentEl, profile) + mountProfile(profileContentEl, profile, webUrl) const servicesHtml = await renderServices(config.handle) profileContentEl.insertAdjacentHTML('beforeend', servicesHtml) @@ -642,9 +696,10 @@ async function render(): Promise { } async function init(): Promise { - const [configData, networksData] = await Promise.all([loadConfig(), loadNetworks()]) + const [configData, networksData, linksData] = await Promise.all([loadConfig(), loadNetworks(), loadLinks()]) config = configData networks = networksData + footerLinks = linksData // Set page title document.title = config.title || 'ailog' diff --git a/src/styles/main.css b/src/styles/main.css index 1360f99..a006c61 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -217,6 +217,16 @@ body { margin-bottom: 8px; } +.profile-handle-link { + color: #666; + text-decoration: none; +} + +.profile-handle-link:hover { + color: var(--btn-color); + text-decoration: underline; +} + .profile-desc { font-size: 14px; color: #444; @@ -796,9 +806,56 @@ body { } } +/* Footer Links */ +.footer-links { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 40px; + padding: 20px 0; +} + +.footer-link-item { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: #666; + text-decoration: none; + transition: color 0.2s; +} + +.footer-link-item:hover { + color: var(--btn-color); +} + +.footer-link-item svg { + width: 24px; + height: 24px; +} + +.footer-link-item [class^="icon-"] { + font-size: 24px; +} + +.footer-link-favicon { + width: 24px; + height: 24px; +} + +@media (prefers-color-scheme: dark) { + .footer-link-item { + color: #888; + } + .footer-link-item:hover { + color: var(--btn-color); + } +} + /* Footer */ .site-footer { - margin-top: 60px; + margin-top: 20px; padding: 20px 0; text-align: center; font-size: 13px; diff --git a/src/types.ts b/src/types.ts index 00d85fc..040d7d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export interface BlogPost { export interface NetworkConfig { plc: string bsky: string + web?: string } export interface AppConfig {