add link
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>syui.ai</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/pkg/icomoon/style.css">
|
||||
<link rel="stylesheet" href="/src/styles/main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
9
public/links.json
Normal file
9
public/links.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"links": [
|
||||
{
|
||||
"name": "git",
|
||||
"url": "https://git.syui.ai/ai/log",
|
||||
"icon": "ai"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/pkg/icomoon/fonts/icomoon.eot
Normal file
BIN
public/pkg/icomoon/fonts/icomoon.eot
Normal file
Binary file not shown.
34
public/pkg/icomoon/fonts/icomoon.svg
Normal file
34
public/pkg/icomoon/fonts/icomoon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 58 KiB |
BIN
public/pkg/icomoon/fonts/icomoon.ttf
Normal file
BIN
public/pkg/icomoon/fonts/icomoon.ttf
Normal file
Binary file not shown.
BIN
public/pkg/icomoon/fonts/icomoon.woff
Normal file
BIN
public/pkg/icomoon/fonts/icomoon.woff
Normal file
Binary file not shown.
99
public/pkg/icomoon/style.css
Normal file
99
public/pkg/icomoon/style.css
Normal file
@@ -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";
|
||||
}
|
||||
@@ -297,6 +297,7 @@ function generateHtml(title: string, content: string, config: AppConfig, assets:
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${escapeHtml(title)} - ${escapeHtml(config.title)}</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/pkg/icomoon/style.css">
|
||||
<link rel="stylesheet" href="/assets/${assets.css}">
|
||||
${config.color ? `<style>:root { --btn-color: ${config.color}; }</style>` : ''}
|
||||
</head>
|
||||
@@ -309,16 +310,20 @@ function generateHtml(title: string, content: string, config: AppConfig, assets:
|
||||
</html>`
|
||||
}
|
||||
|
||||
function generateProfileHtml(profile: Profile): string {
|
||||
function generateProfileHtml(profile: Profile, webUrl?: string): string {
|
||||
const avatar = profile.avatar
|
||||
? `<img src="${profile.avatar}" class="profile-avatar" alt="">`
|
||||
: ''
|
||||
const profileLink = webUrl ? `${webUrl}/profile/${profile.did}` : null
|
||||
const handleHtml = profileLink
|
||||
? `<a href="${profileLink}" class="profile-handle-link" target="_blank" rel="noopener">@${escapeHtml(profile.handle)}</a>`
|
||||
: `@${escapeHtml(profile.handle)}`
|
||||
return `
|
||||
<div class="profile">
|
||||
${avatar}
|
||||
<div class="profile-info">
|
||||
<div class="profile-name">${escapeHtml(profile.displayName || profile.handle)}</div>
|
||||
<div class="profile-handle">@${escapeHtml(profile.handle)}</div>
|
||||
<div class="profile-handle">${handleHtml}</div>
|
||||
${profile.description ? `<div class="profile-desc">${escapeHtml(profile.description)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string, string> = {
|
||||
bluesky: `<svg viewBox="0 0 600 530" fill="currentColor"><path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.72 40.255-67.24 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></svg>`,
|
||||
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
|
||||
ai: `<span class="icon-ai"></span>`,
|
||||
git: `<span class="icon-git"></span>`,
|
||||
}
|
||||
|
||||
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 = `<img src="https://www.google.com/s2/favicons?domain=${domain}&sz=32" alt="" class="footer-link-favicon">`
|
||||
}
|
||||
return `
|
||||
<a href="${escapeHtml(link.url)}" class="footer-link-item" title="${escapeHtml(link.name)}" target="_blank" rel="noopener">
|
||||
${iconHtml}
|
||||
</a>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
return `
|
||||
<div class="footer-links">
|
||||
${items}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function generateFooterHtml(handle: string, links: FooterLink[]): string {
|
||||
const username = handle.split('.')[0] || handle
|
||||
return `
|
||||
${generateFooterLinksHtml(links)}
|
||||
<footer class="site-footer">
|
||||
<p>© ${username}</p>
|
||||
</footer>
|
||||
`
|
||||
}
|
||||
|
||||
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 `
|
||||
<header id="header">${generateHeaderHtml(config.handle)}</header>
|
||||
<main>
|
||||
<section id="profile">
|
||||
${generateTabsHtml('blog', config.handle)}
|
||||
${generateProfileHtml(profile)}
|
||||
${generateProfileHtml(profile, webUrl)}
|
||||
${generateServicesHtml(profile.did, config.handle, collections)}
|
||||
</section>
|
||||
<section id="content">
|
||||
${generatePostListHtml(posts)}
|
||||
</section>
|
||||
</main>
|
||||
${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 `
|
||||
<header id="header">${generateHeaderHtml(config.handle)}</header>
|
||||
<main>
|
||||
<section id="profile">
|
||||
${generateTabsHtml('blog', config.handle)}
|
||||
${generateProfileHtml(profile)}
|
||||
${generateProfileHtml(profile, webUrl)}
|
||||
${generateServicesHtml(profile.did, config.handle, collections)}
|
||||
</section>
|
||||
<section id="content">
|
||||
${generatePostDetailHtml(post, config.handle, config.collection, config.network, config.siteUrl)}
|
||||
</section>
|
||||
</main>
|
||||
${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')
|
||||
|
||||
@@ -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
|
||||
? `<a href="${escapeHtml(profileLink)}" class="profile-handle-link" target="_blank" rel="noopener">@${escapeHtml(profile.handle)}</a>`
|
||||
: `@${escapeHtml(profile.handle)}`
|
||||
return `
|
||||
<div class="profile">
|
||||
${profile.avatar ? `<img src="${profile.avatar}" alt="avatar" class="profile-avatar">` : ''}
|
||||
${profile.avatar ? `<img src="${escapeHtml(profile.avatar)}" alt="avatar" class="profile-avatar">` : ''}
|
||||
<div class="profile-info">
|
||||
<h1 class="profile-name">${profile.displayName || profile.handle}</h1>
|
||||
<p class="profile-handle">@${profile.handle}</p>
|
||||
${profile.description ? `<p class="profile-desc">${profile.description}</p>` : ''}
|
||||
<h1 class="profile-name">${escapeHtml(profile.displayName || profile.handle)}</h1>
|
||||
<p class="profile-handle">${handleHtml}</p>
|
||||
${profile.description ? `<p class="profile-desc">${escapeHtml(profile.description)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
61
src/main.ts
61
src/main.ts
@@ -36,10 +36,62 @@ async function loadNetworks(): Promise<Networks> {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
interface FooterLink {
|
||||
name: string
|
||||
url: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
let footerLinks: FooterLink[] = []
|
||||
|
||||
async function loadLinks(): Promise<FooterLink[]> {
|
||||
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<string, string> = {
|
||||
bluesky: `<svg viewBox="0 0 600 530" fill="currentColor"><path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.72 40.255-67.24 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></svg>`,
|
||||
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
|
||||
ai: `<span class="icon-ai"></span>`,
|
||||
git: `<span class="icon-git"></span>`,
|
||||
}
|
||||
|
||||
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 = `<img src="https://www.google.com/s2/favicons?domain=${domain}&sz=32" alt="" class="footer-link-favicon">`
|
||||
} catch {
|
||||
iconHtml = ''
|
||||
}
|
||||
}
|
||||
return `
|
||||
<a href="${escapeHtml(link.url)}" class="footer-link-item" title="${escapeHtml(link.name)}" target="_blank" rel="noopener">
|
||||
${iconHtml}
|
||||
</a>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
return `<div class="footer-links">${items}</div>`
|
||||
}
|
||||
|
||||
function renderFooter(handle: string): string {
|
||||
const parts = handle.split('.')
|
||||
const username = parts[0] || handle
|
||||
return `
|
||||
${renderFooterLinks(footerLinks)}
|
||||
<footer class="site-footer">
|
||||
<p>© ${username}</p>
|
||||
</footer>
|
||||
@@ -598,10 +650,11 @@ async function render(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
async function init(): Promise<void> {
|
||||
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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface BlogPost {
|
||||
export interface NetworkConfig {
|
||||
plc: string
|
||||
bsky: string
|
||||
web?: string
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
|
||||
Reference in New Issue
Block a user