add comment

This commit is contained in:
2026-01-15 23:26:34 +09:00
parent 9980e596ca
commit 8945601aa1
16 changed files with 703 additions and 135 deletions

View File

@@ -1,38 +1,8 @@
import * as fs from 'fs'
import * as path from 'path'
import { marked, Renderer } from 'marked'
// Types
interface AppConfig {
title: string
handle: string
collection: string
network: string
color?: string
}
interface Networks {
[key: string]: {
plc: string
bsky: string
}
}
interface Profile {
did: string
handle: string
displayName?: string
description?: string
avatar?: string
}
interface BlogPost {
uri: string
cid: string
title: string
content: string
createdAt: string
}
import type { AppConfig, Profile, BlogPost, Networks } from '../src/types.ts'
import { escapeHtml } from '../src/lib/utils.ts'
// Highlight.js for syntax highlighting (core + common languages only)
let hljs: typeof import('highlight.js/lib/core').default
@@ -100,14 +70,6 @@ function setupMarked() {
})
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr)
return date.toLocaleDateString('ja-JP', {
@@ -445,10 +407,23 @@ function generatePostListHtml(posts: BlogPost[]): string {
return `<ul class="post-list">${items}</ul>`
}
function generatePostDetailHtml(post: BlogPost, handle: string, collection: string): string {
// Map network to app URL for discussion links
function getAppUrl(network: string): string {
if (network === 'syu.is') {
return 'https://syu.is'
}
return 'https://bsky.app'
}
function generatePostDetailHtml(post: BlogPost, handle: string, collection: string, network: string, siteUrl?: string): string {
const rkey = post.uri.split('/').pop() || ''
const jsonUrl = `/at/${handle}/${collection}/${rkey}/`
const content = marked.parse(post.content) as string
// Use siteUrl from config, or construct from handle
const baseSiteUrl = siteUrl || `https://${handle}`
const postUrl = `${baseSiteUrl}/post/${rkey}/`
const appUrl = getAppUrl(network)
const searchUrl = `${appUrl}/search?q=${encodeURIComponent(postUrl)}`
return `
<article class="post-detail">
@@ -461,6 +436,15 @@ function generatePostDetailHtml(post: BlogPost, handle: string, collection: stri
</header>
<div class="post-content">${content}</div>
</article>
<div class="discussion-section">
<a href="${searchUrl}" target="_blank" rel="noopener" class="discuss-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2.546 20.2A1.5 1.5 0 0 0 4 22h.5l2.83-.892A9.96 9.96 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2z"/>
</svg>
Discuss on Bluesky
</a>
<div id="discussion-posts" class="discussion-posts" data-post-url="${escapeHtml(postUrl)}" data-app-url="${escapeHtml(appUrl)}"></div>
</div>
`
}
@@ -500,7 +484,7 @@ function generatePostPageContent(profile: Profile, post: BlogPost, config: AppCo
${generateServicesHtml(profile.did, config.handle, collections)}
</section>
<section id="content">
${generatePostDetailHtml(post, config.handle, config.collection)}
${generatePostDetailHtml(post, config.handle, config.collection, config.network, config.siteUrl)}
</section>
</main>
${generateFooterHtml(config.handle)}
@@ -595,27 +579,20 @@ async function generate() {
const localPosts = localDid ? loadPostsFromFiles(localDid, config.collection) : []
console.log(`Found ${localPosts.length} posts from local`)
// Merge: API is the source of truth for what exists
// - If post exists in API and local: use local (may have edits)
// - If post exists in API only: use API
// - If post exists in local only: skip (was deleted from API)
// Merge: API is the source of truth
// - If post exists in API: always use API (has latest edits)
// - If post exists in local only: keep if not deleted (for posts beyond API limit)
const apiRkeys = new Set(apiPosts.map(p => p.uri.split('/').pop()))
const localRkeys = new Set(localPosts.map(p => p.uri.split('/').pop()))
// Local posts that still exist in API
const validLocalPosts = localPosts.filter(p => apiRkeys.has(p.uri.split('/').pop()))
// API posts that don't exist locally
const newApiPosts = apiPosts.filter(p => !localRkeys.has(p.uri.split('/').pop()))
// Local posts that don't exist in API (older posts beyond 100 limit)
// Note: these might be deleted posts, so we keep them cautiously
const oldLocalPosts = localPosts.filter(p => !apiRkeys.has(p.uri.split('/').pop()))
posts = [...validLocalPosts, ...newApiPosts].sort((a, b) =>
posts = [...apiPosts, ...oldLocalPosts].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
const deletedCount = localPosts.length - validLocalPosts.length
if (deletedCount > 0) {
console.log(`Skipped ${deletedCount} deleted posts (exist locally but not in API)`)
}
console.log(`Total ${posts.length} posts (${validLocalPosts.length} local + ${newApiPosts.length} new from API)`)
console.log(`Total ${posts.length} posts (${apiPosts.length} from API + ${oldLocalPosts.length} old local)`)
// Create output directory
const distDir = path.join(process.cwd(), 'dist')