mirror of
https://github.com/dergigi/boris.git
synced 2026-02-17 13:04:59 +01:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c6232e029 | ||
|
|
f6c562e9be | ||
|
|
a92b14e877 | ||
|
|
b69a956247 | ||
|
|
82a8dcf6eb | ||
|
|
8e19e22289 | ||
|
|
e167b57810 | ||
|
|
ba3b82e6b5 | ||
|
|
b5edfbb2c9 | ||
|
|
48048f877a | ||
|
|
bd1afc54c3 |
58
CHANGELOG.md
58
CHANGELOG.md
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.23] - 2025-01-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Deep-link refresh redirect issue for nostr-native articles
|
||||
- Limited `/a/:naddr` rewrite to bot user-agents only in Vercel configuration
|
||||
- Real browsers now hit the SPA directly, preventing redirect to root path
|
||||
- Bot crawlers still receive proper OpenGraph metadata for social sharing
|
||||
|
||||
### Added
|
||||
|
||||
- Version and git commit information in Settings footer
|
||||
- Displays app version and short commit hash with link to GitHub
|
||||
- Build-time metadata injection via Vite configuration
|
||||
- Subtle footer styling with selectable text
|
||||
|
||||
### Changed
|
||||
|
||||
- Article OG handler now uses proper RelayPool.request() API
|
||||
- Aligned with applesauce RelayPool interface
|
||||
- Removed deprecated open/close methods
|
||||
- Fixed TypeScript linting errors
|
||||
|
||||
### Technical
|
||||
|
||||
- Added debug logging for route state and article OG handler
|
||||
- Gated by `?debug=1` query parameter for production testing
|
||||
- Structured logging for troubleshooting deep-link issues
|
||||
- Temporary debug components for validation
|
||||
|
||||
## [0.6.22] - 2025-10-16
|
||||
|
||||
### Added
|
||||
|
||||
- Dynamic OpenGraph and Twitter Card meta tags for article deep-links
|
||||
- Social media platforms display article title, author, cover image, and summary when sharing `/a/{naddr}` links
|
||||
- Serverless endpoint fetches article metadata from Nostr relays (kind:30023) and author profiles (kind:0)
|
||||
- User-agent detection serves appropriate content to crawlers vs browsers
|
||||
- Falls back to default social preview image when articles have no cover image
|
||||
- Social preview image for homepage and article links
|
||||
- Added `boris-social-1200.png` as default OpenGraph image (1200x630)
|
||||
- Homepage now includes social preview image in meta tags
|
||||
|
||||
### Changed
|
||||
|
||||
- Article deep-links now properly preserve URL when loading in browser
|
||||
- Uses `history.replaceState()` to maintain correct article path
|
||||
- Browser navigation works correctly on refresh and new tab opens
|
||||
|
||||
### Fixed
|
||||
|
||||
- Vercel rewrite configuration for article routes
|
||||
- Routes `/a/:naddr` to serverless OG endpoint for dynamic meta tags
|
||||
- Regular SPA routing preserved for browser navigation
|
||||
|
||||
## [0.6.21] - 2025-10-16
|
||||
|
||||
### Added
|
||||
@@ -1696,7 +1751,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Optimize relay usage following applesauce-relay best practices
|
||||
- Use applesauce-react event models for better profile handling
|
||||
|
||||
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.21...HEAD
|
||||
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.23...HEAD
|
||||
[0.6.23]: https://github.com/dergigi/boris/compare/v0.6.22...v0.6.23
|
||||
[0.6.21]: https://github.com/dergigi/boris/compare/v0.6.20...v0.6.21
|
||||
[0.6.20]: https://github.com/dergigi/boris/compare/v0.6.19...v0.6.20
|
||||
[0.6.19]: https://github.com/dergigi/boris/compare/v0.6.18...v0.6.19
|
||||
|
||||
@@ -58,15 +58,14 @@ async function fetchEventsFromRelays(
|
||||
timeoutMs: number
|
||||
): Promise<NostrEvent[]> {
|
||||
const events: NostrEvent[] = []
|
||||
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => resolve(), timeoutMs)
|
||||
|
||||
relayPool.req(relayUrls, filter).subscribe({
|
||||
next: (msg) => {
|
||||
if (msg.type === 'EVENT') {
|
||||
events.push(msg.event)
|
||||
}
|
||||
|
||||
// `request` emits NostrEvent objects directly
|
||||
relayPool.request(relayUrls, filter).subscribe({
|
||||
next: (event) => {
|
||||
events.push(event)
|
||||
},
|
||||
error: () => resolve(),
|
||||
complete: () => {
|
||||
@@ -92,9 +91,8 @@ async function fetchArticleMetadata(naddr: string): Promise<ArticleMetadata | nu
|
||||
|
||||
const pointer = decoded.data as AddressPointer
|
||||
|
||||
// Connect to relays
|
||||
// Determine relay URLs
|
||||
const relayUrls = pointer.relays && pointer.relays.length > 0 ? pointer.relays : RELAYS
|
||||
relayUrls.forEach(url => relayPool.open(url))
|
||||
|
||||
// Fetch article and profile in parallel
|
||||
const [articleEvents, profileEvents] = await Promise.all([
|
||||
@@ -142,7 +140,7 @@ async function fetchArticleMetadata(naddr: string): Promise<ArticleMetadata | nu
|
||||
console.error('Failed to fetch article metadata:', err)
|
||||
return null
|
||||
} finally {
|
||||
relayPool.close()
|
||||
// No explicit close needed; pool manages connections internally
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +213,17 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
const userAgent = req.headers['user-agent'] as string | undefined
|
||||
const isCrawlerRequest = isCrawler(userAgent)
|
||||
|
||||
const debugEnabled = req.query.debug === '1' || req.headers['x-boris-debug'] === '1'
|
||||
if (debugEnabled) {
|
||||
console.log('[article-og] request', JSON.stringify({
|
||||
naddr,
|
||||
ua: userAgent || null,
|
||||
isCrawlerRequest,
|
||||
path: req.url || null
|
||||
}))
|
||||
res.setHeader('X-Boris-Debug', '1')
|
||||
}
|
||||
|
||||
// If it's a regular browser (not a bot), serve HTML that loads SPA
|
||||
// Use history.replaceState to set the URL before the SPA boots
|
||||
if (!isCrawlerRequest) {
|
||||
@@ -233,6 +242,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
history.replaceState(null, '', '${articlePath}');
|
||||
}
|
||||
</script>
|
||||
${debugEnabled ? `<script>console.debug('article-og', { mode: 'browser', naddr: '${naddr}', path: location.pathname, referrer: document.referrer });</script>` : ''}
|
||||
<script>
|
||||
// Redirect to index.html which will load the SPA
|
||||
// The history state is already set, so SPA will see the correct URL
|
||||
@@ -246,6 +256,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
if (debugEnabled) {
|
||||
console.log('[article-og] response', JSON.stringify({ mode: 'browser', naddr }))
|
||||
}
|
||||
return res.status(200).send(html)
|
||||
}
|
||||
|
||||
@@ -254,6 +267,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
const cached = memoryCache.get(naddr)
|
||||
if (cached && cached.expires > now) {
|
||||
setCacheHeaders(res)
|
||||
if (debugEnabled) {
|
||||
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: true }))
|
||||
}
|
||||
return res.status(200).send(cached.html)
|
||||
}
|
||||
|
||||
@@ -269,6 +285,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
|
||||
// Send response
|
||||
setCacheHeaders(res)
|
||||
if (debugEnabled) {
|
||||
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: false }))
|
||||
}
|
||||
return res.status(200).send(html)
|
||||
} catch (err) {
|
||||
console.error('Error generating article OG HTML:', err)
|
||||
@@ -276,6 +295,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
// Fallback to basic HTML with SPA boot
|
||||
const html = generateHtml(naddr, null)
|
||||
setCacheHeaders(res, 3600)
|
||||
if (debugEnabled) {
|
||||
console.log('[article-og] response', JSON.stringify({ mode: 'bot-fallback', naddr }))
|
||||
}
|
||||
return res.status(200).send(html)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.6.22",
|
||||
"version": "0.6.24",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
||||
import Bookmarks from './components/Bookmarks'
|
||||
import RouteDebug from './components/RouteDebug'
|
||||
import Toast from './components/Toast'
|
||||
import { useToast } from './hooks/useToast'
|
||||
import { useOnlineStatus } from './hooks/useOnlineStatus'
|
||||
@@ -303,6 +304,7 @@ function App() {
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen p-0 max-w-none m-0 relative">
|
||||
<AppRoutes relayPool={relayPool} showToast={showToast} />
|
||||
<RouteDebug />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
{toastMessage && (
|
||||
|
||||
30
src/components/RouteDebug.tsx
Normal file
30
src/components/RouteDebug.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation, useMatch } from 'react-router-dom'
|
||||
|
||||
export default function RouteDebug() {
|
||||
const location = useLocation()
|
||||
const matchArticle = useMatch('/a/:naddr')
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
if (params.get('debug') !== '1') return
|
||||
|
||||
const info: Record<string, unknown> = {
|
||||
pathname: location.pathname,
|
||||
search: location.search || null,
|
||||
matchedArticleRoute: Boolean(matchArticle),
|
||||
referrer: document.referrer || null
|
||||
}
|
||||
|
||||
if (location.pathname === '/') {
|
||||
// Unexpected during deep-link refresh tests
|
||||
console.warn('[RouteDebug] unexpected root redirect', info)
|
||||
} else {
|
||||
console.debug('[RouteDebug]', info)
|
||||
}
|
||||
}, [location, matchArticle])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global __APP_VERSION__, __GIT_COMMIT__, __GIT_COMMIT_URL__ */
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { faTimes, faUndo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
@@ -167,6 +168,21 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, relayPoo
|
||||
<PWASettings settings={localSettings} onUpdate={handleUpdate} onClose={onClose} />
|
||||
<RelaySettings relayStatuses={relayStatuses} onClose={onClose} />
|
||||
</div>
|
||||
<div className="text-xs opacity-60 mt-4 px-4 pb-3 select-text">
|
||||
<span>Version {typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev'}</span>
|
||||
{typeof __GIT_COMMIT__ !== 'undefined' && __GIT_COMMIT__ ? (
|
||||
<span>
|
||||
{' '}·
|
||||
{typeof __GIT_COMMIT_URL__ !== 'undefined' && __GIT_COMMIT_URL__ ? (
|
||||
<a href={__GIT_COMMIT_URL__} target="_blank" rel="noopener noreferrer">
|
||||
<code>{__GIT_COMMIT__.slice(0, 7)}</code>
|
||||
</a>
|
||||
) : (
|
||||
<code>{__GIT_COMMIT__.slice(0, 7)}</code>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
7
src/vite-env.d.ts
vendored
7
src/vite-env.d.ts
vendored
@@ -8,3 +8,10 @@ declare module '*.svg?raw' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
// Build-time defines injected by Vite in vite.config.ts
|
||||
declare const __APP_VERSION__: string
|
||||
declare const __GIT_COMMIT__: string
|
||||
declare const __GIT_BRANCH__: string
|
||||
declare const __BUILD_TIME__: string
|
||||
declare const __GIT_COMMIT_URL__: string
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/a/:naddr",
|
||||
"has": [
|
||||
{
|
||||
"type": "header",
|
||||
"key": "user-agent",
|
||||
"value": ".*(bot|crawl|spider|slurp|facebook|twitter|linkedin|whatsapp|telegram|slack|discord|preview).*"
|
||||
}
|
||||
],
|
||||
"destination": "/api/article-og?naddr=:naddr"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +1,75 @@
|
||||
/* eslint-env node */
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
function getGitMetadata() {
|
||||
const envSha = process.env.VERCEL_GIT_COMMIT_SHA || ''
|
||||
const envRef = process.env.VERCEL_GIT_COMMIT_REF || ''
|
||||
let commit = envSha
|
||||
let branch = envRef
|
||||
try {
|
||||
if (!commit) commit = execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (!branch) branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return { commit, branch }
|
||||
}
|
||||
|
||||
function getPackageVersion() {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url)).toString())
|
||||
return pkg.version as string
|
||||
} catch {
|
||||
return '0.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
const { commit, branch } = getGitMetadata()
|
||||
const version = getPackageVersion()
|
||||
const buildTime = new Date().toISOString()
|
||||
|
||||
function getCommitUrl(commit: string): string {
|
||||
if (!commit) return ''
|
||||
const provider = process.env.VERCEL_GIT_PROVIDER || ''
|
||||
const owner = process.env.VERCEL_GIT_REPO_OWNER || ''
|
||||
const slug = process.env.VERCEL_GIT_REPO_SLUG || ''
|
||||
if (provider.toLowerCase() === 'github' && owner && slug) {
|
||||
return `https://github.com/${owner}/${slug}/commit/${commit}`
|
||||
}
|
||||
try {
|
||||
const remote = execSync('git config --get remote.origin.url', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
||||
if (remote.includes('github.com')) {
|
||||
// git@github.com:owner/repo.git or https://github.com/owner/repo.git
|
||||
const https = remote.startsWith('git@')
|
||||
? `https://github.com/${remote.split(':')[1]}`
|
||||
: remote
|
||||
const cleaned = https.replace(/\.git$/, '')
|
||||
return `${cleaned}/commit/${commit}`
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const commitUrl = getCommitUrl(commit)
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(version),
|
||||
__GIT_COMMIT__: JSON.stringify(commit),
|
||||
__GIT_BRANCH__: JSON.stringify(branch),
|
||||
__BUILD_TIME__: JSON.stringify(buildTime),
|
||||
__GIT_COMMIT_URL__: JSON.stringify(commitUrl)
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
|
||||
Reference in New Issue
Block a user