Skip to content

Commit 0cdf65c

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent 5bb8e1d commit 0cdf65c

8 files changed

Lines changed: 199 additions & 29 deletions

File tree

‎app/[lang]/(home)/hero.tsx‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ import { Download, Github, Book, HeartHandshake } from 'lucide-react';
55
import SectionWrap from './section-wrap';
66
import Image from 'next/image';
77

8+
const productShowcaseImage = '/notegen-product-showcase.png';
9+
10+
export function NoteGenProductShowcase({
11+
className = '',
12+
priority = false,
13+
}: {
14+
className?: string;
15+
priority?: boolean;
16+
}) {
17+
return (
18+
<Image
19+
src={productShowcaseImage}
20+
alt="NoteGen"
21+
width={2586}
22+
height={1684}
23+
className={className}
24+
priority={priority}
25+
/>
26+
);
27+
}
28+
829
// 格式化数字显示函数
930
function formatNumber(num: number): string {
1031
if (num >= 1000) {
@@ -79,13 +100,7 @@ export default function HomeHero({ githubStats }: HomeHeroProps) {
79100
</Button>
80101
</div>
81102
</div>
82-
<Image
83-
src="https://s2.loli.net/2025/12/22/jlpEP2c6ogwHhIA.png"
84-
alt="NoteGen"
85-
width={1280}
86-
height={720}
87-
className="w-full lg:w-[640px] object-contain"
88-
/>
103+
<NoteGenProductShowcase className="w-full lg:w-[640px] object-contain" priority />
89104
</SectionWrap>
90105
);
91106
}

‎app/[lang]/self-hosted/page.tsx‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { getPageAlternates, normalizeLang, siteConfig } from '@/lib/seo';
2727
import { isSelfHostedEnabled } from '@/lib/self-hosted';
2828
import { cn } from '@/lib/utils';
2929
import { notFound } from 'next/navigation';
30+
import { MarkdownSyncDemo } from '@/components/self-hosted/markdown-sync-demo';
3031

3132
const repositoryUrl = 'https://github.com/codexu/note-gen-server';
3233
const issueUrl = `${repositoryUrl}/issues`;
@@ -74,7 +75,7 @@ const content = {
7475
['NoteGen Server', '同步 API、WebSocket、认证和账号管理 Web 同源运行。'],
7576
['PostgreSQL 17', '保存账号、设备、同步游标、对象版本和实例配置。'],
7677
['持久化数据卷', 'PostgreSQL 与附件数据独立于容器生命周期保存。'],
77-
['GHCR 镜像', 'edge 跟随主开发分支,版本标签用于固定实验版本。'],
78+
['GHCR 镜像', 'latest 跟随 release 分支发布,完整版本标签用于稳定复现。'],
7879
],
7980
},
8081
feedback: {
@@ -126,7 +127,7 @@ const content = {
126127
['NoteGen Server', 'Sync API, WebSocket, authentication, and the account portal run on one origin.'],
127128
['PostgreSQL 17', 'Stores accounts, devices, sync cursors, object versions, and instance configuration.'],
128129
['Persistent volumes', 'Database and attachment data remain outside the container lifecycle.'],
129-
['GHCR image', 'edge follows the main development branch; version tags pin experimental releases.'],
130+
['GHCR image', 'latest follows releases from the release branch; full version tags provide reproducible deployments.'],
130131
],
131132
},
132133
feedback: {
@@ -202,10 +203,6 @@ export async function generateMetadata({
202203
title: t.meta.title,
203204
description: t.meta.description,
204205
},
205-
robots: {
206-
index: false,
207-
follow: false,
208-
},
209206
};
210207
}
211208

@@ -252,12 +249,16 @@ export default async function SelfHostedPage({
252249
</div>
253250
</div>
254251

255-
<Card className="max-w-5xl">
252+
<MarkdownSyncDemo lang={language} />
253+
254+
<Card className="max-w-5xl border-amber-500/40 bg-amber-500/10">
256255
<CardHeader>
257-
<CardTitle>{t.notice.title}</CardTitle>
258-
<CardDescription>{t.notice.description}</CardDescription>
256+
<CardTitle className="text-amber-900 dark:text-amber-200">{t.notice.title}</CardTitle>
257+
<CardDescription className="text-amber-950/70 dark:text-amber-100/70">
258+
{t.notice.description}
259+
</CardDescription>
259260
<CardAction>
260-
<Badge variant="secondary">
261+
<Badge className="border-amber-500/40 bg-amber-500/15 text-amber-900 hover:bg-amber-500/20 dark:text-amber-200" variant="outline">
261262
<CircleAlertIcon data-icon="inline-start" />
262263
Preview
263264
</Badge>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useReducedMotion } from "framer-motion";
5+
6+
import { NoteGenProductShowcase } from "@/app/[lang]/(home)/hero";
7+
8+
type DemoLanguage = "cn" | "en";
9+
10+
const demoCopy = {
11+
cn: {
12+
section: "晚餐安排",
13+
sentence: "湖滨餐厅已预约 19:00 靠窗座位。",
14+
mobileTitle: "周末杭州行程",
15+
mobileIntro: "两天以西湖、象山和城西为主,减少跨区往返。",
16+
mobileOverview: "行程概览",
17+
mobilePoints: ["周六:西湖慢走、象山校区", "周日:法喜寺、天目里"],
18+
mobileSection: "晚餐安排",
19+
mobileDay: "周六 · 西湖与象山",
20+
mobileSchedule: [
21+
["09:00–12:00", "西湖慢走", "从曲院风荷出发,沿北山街走到孤山。"],
22+
["12:15–13:30", "午餐", "在孤山附近简单吃面,避开热门餐厅排队。"],
23+
["15:00–17:30", "中国美院象山校区", "从西湖打车前往象山,重点看建筑与公共空间。"],
24+
["19:00", "杭帮菜晚餐", "选择收藏里的餐厅,提前预约靠窗座位。"],
25+
],
26+
mobileNextDay: "周日 · 法喜寺与天目里",
27+
remoteDevice: "iPhone",
28+
},
29+
en: {
30+
section: "Dinner plan",
31+
sentence: "Window table booked by the lake for 19:00.",
32+
mobileTitle: "Weekend in Hangzhou",
33+
mobileIntro: "Two relaxed days around West Lake, Xiangshan, and the west side.",
34+
mobileOverview: "Itinerary overview",
35+
mobilePoints: ["Saturday: West Lake and Xiangshan", "Sunday: Faxi Temple and Tianmuli"],
36+
mobileSection: "Dinner plan",
37+
mobileDay: "Saturday · West Lake and Xiangshan",
38+
mobileSchedule: [
39+
["09:00–12:00", "West Lake walk", "Start at Quyuan Garden and follow Beishan Road to Solitary Hill."],
40+
["12:15–13:30", "Lunch", "Grab noodles near Solitary Hill and skip the busiest queues."],
41+
["15:00–17:30", "Xiangshan campus", "Explore the architecture and public spaces around the campus."],
42+
["19:00", "Hangzhou dinner", "Use the saved restaurant and reserve a window table."],
43+
],
44+
mobileNextDay: "Sunday · Faxi Temple and Tianmuli",
45+
remoteDevice: "iPhone",
46+
},
47+
} as const;
48+
49+
export function MarkdownSyncDemo({ lang }: { lang: DemoLanguage }) {
50+
const copy = demoCopy[lang];
51+
const reduceMotion = useReducedMotion();
52+
const [tick, setTick] = useState(0);
53+
const sentenceLength = Array.from(copy.sentence).length;
54+
55+
useEffect(() => {
56+
if (reduceMotion) return;
57+
const timer = window.setInterval(() => setTick((current) => current + 1), 100);
58+
return () => window.clearInterval(timer);
59+
}, [reduceMotion]);
60+
61+
const cycleLength = sentenceLength + 25;
62+
const cycle = reduceMotion ? sentenceLength + 10 : tick % cycleLength;
63+
const mobileCount = cycle < sentenceLength
64+
? cycle
65+
: cycle < sentenceLength + 15
66+
? sentenceLength
67+
: 0;
68+
const desktopCount = cycle < 5
69+
? 0
70+
: cycle < sentenceLength + 5
71+
? Math.min(cycle - 5, sentenceLength)
72+
: cycle < sentenceLength + 15
73+
? sentenceLength
74+
: 0;
75+
const mobileText = Array.from(copy.sentence).slice(0, mobileCount).join("");
76+
const desktopText = Array.from(copy.sentence).slice(0, desktopCount).join("");
77+
78+
return (
79+
<div className="relative mx-auto w-full max-w-6xl overflow-visible bg-background">
80+
<NoteGenProductShowcase
81+
className="block h-auto w-full object-contain"
82+
priority
83+
/>
84+
85+
<div className="absolute bottom-[11%] left-[30.6%] top-[53.5%] w-[31.5%] overflow-hidden bg-background pr-[2%]">
86+
<h4 className="mb-[2.2%] text-[clamp(6px,0.9vw,13px)] font-semibold">{copy.section}</h4>
87+
<p className="min-h-[1.5em] text-[clamp(5px,0.72vw,10px)] leading-relaxed text-muted-foreground">
88+
{desktopText}
89+
{desktopCount > 0 && desktopCount < sentenceLength ? <RemoteCursor label={copy.remoteDevice} /> : null}
90+
</p>
91+
<div className="mt-[6%] flex flex-col gap-[3%] text-[clamp(4px,0.57vw,8px)] leading-relaxed text-muted-foreground">
92+
{copy.mobileSchedule.map(([time, item, description]) => (
93+
<div key={time}>
94+
<p className="font-semibold text-foreground"><span>{time}</span> {item}</p>
95+
<p className="mt-[1.5%]">{description}</p>
96+
</div>
97+
))}
98+
</div>
99+
<h4 className="mt-[5%] border-t pt-[3%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileNextDay}</h4>
100+
</div>
101+
102+
<div className="absolute bottom-[9.7%] left-[77.2%] top-[35.1%] w-[19.7%] overflow-hidden bg-background px-[1.5%] py-[2.4%]">
103+
<div className="flex h-full flex-col">
104+
<h3 className="text-[clamp(7px,1.05vw,15px)] font-bold tracking-tight">{copy.mobileTitle}</h3>
105+
<p className="mt-[6%] text-[clamp(5px,0.65vw,9px)] leading-relaxed text-muted-foreground">{copy.mobileIntro}</p>
106+
<h4 className="mt-[8%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileOverview}</h4>
107+
<ul className="mt-[3%] flex list-disc flex-col gap-1 pl-[7%] text-[clamp(5px,0.6vw,8px)] leading-relaxed text-muted-foreground">
108+
{copy.mobilePoints.map((point) => <li key={point}>{point}</li>)}
109+
</ul>
110+
<h4 className="mt-[10%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileSection}</h4>
111+
<p className="mt-[4%] min-h-[3em] text-[clamp(5px,0.65vw,9px)] leading-relaxed text-muted-foreground">
112+
{mobileText}
113+
{mobileCount > 0 && mobileCount < sentenceLength ? <LocalCaret /> : null}
114+
</p>
115+
<h4 className="mt-[8%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileDay}</h4>
116+
<div className="mt-[3%] flex flex-col gap-1.5 text-[clamp(4px,0.55vw,8px)] leading-relaxed text-muted-foreground">
117+
{copy.mobileSchedule.map(([time, item]) => (
118+
<p key={time}><span className="font-semibold text-foreground">{time}</span> {item}</p>
119+
))}
120+
</div>
121+
<h4 className="mt-[8%] border-t pt-[5%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileNextDay}</h4>
122+
</div>
123+
</div>
124+
</div>
125+
);
126+
}
127+
128+
function RemoteCursor({ label }: { label: string }) {
129+
const color = "#2563eb";
130+
131+
return (
132+
<span
133+
className="relative inline-block h-[1.25em] w-0 -ml-px border-l-2 align-text-bottom opacity-50 transition-opacity duration-150 hover:opacity-100"
134+
style={{ borderColor: color }}
135+
aria-label={`${label} cursor`}
136+
>
137+
<span
138+
className="absolute bottom-[calc(100%+2px)] left-[-2px] max-w-32 overflow-hidden whitespace-nowrap rounded-[4px_4px_4px_0] px-[5px] py-px text-[clamp(5px,0.6vw,9px)] font-semibold leading-tight text-white"
139+
style={{ backgroundColor: color }}
140+
>
141+
{label}
142+
</span>
143+
</span>
144+
);
145+
}
146+
147+
function LocalCaret() {
148+
return (
149+
<span
150+
aria-hidden="true"
151+
className="ml-px inline-block h-[1.15em] w-px animate-pulse bg-foreground align-text-bottom"
152+
/>
153+
);
154+
}

‎content/docs/self-hosted.en.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Use Docker Compose for production-like or long-running experiments. Source mode
2121

2222
The default Compose stack starts NoteGen Server and PostgreSQL 17. Two separate volumes store the database and attachments. The service listens only on `127.0.0.1:3789` on the host by default.
2323

24-
The official image is `ghcr.io/codexu/note-gen-server` and supports `linux/amd64` and `linux/arm64`. The current experimental build uses the `edge` tag, so the server does not need Node.js, pnpm, or a local source build.
24+
The official image is `ghcr.io/codexu/note-gen-server` and supports `linux/amd64` and `linux/arm64`. Compose defaults to the `latest` image published from the release branch, so the server does not need Node.js, pnpm, or a local source build.
2525

2626
### 1. Prepare the server
2727

@@ -58,7 +58,7 @@ Edit `.env` and verify at least these values:
5858
| `AUTH_SECRET` | The second random value; do not casually rotate it after launch |
5959
| `PUBLIC_BASE_URL` | The exact address used by NoteGen clients, without a path or trailing slash |
6060
| `SERVER_BIND_ADDRESS` | Keep `127.0.0.1` and expose the service through a reverse proxy |
61-
| `NOTEGEN_SERVER_IMAGE` | Defaults to experimental `edge`; use a `sha-<commit>` tag to reproduce a specific build |
61+
| `NOTEGEN_SERVER_IMAGE` | Defaults to `latest`; pin a full version such as `0.1.0` for reproducible deployments |
6262

6363
For a local-only trial:
6464

@@ -83,7 +83,7 @@ docker compose ps
8383
By default, Compose pulls:
8484

8585
```text
86-
ghcr.io/codexu/note-gen-server:edge
86+
ghcr.io/codexu/note-gen-server:latest
8787
```
8888

8989
On first startup, the server waits for PostgreSQL and applies pending database migrations. Once both services report `healthy`, check readiness:
@@ -141,7 +141,7 @@ See [Sync](/en/docs/settings/sync) for Git, S3, WebDAV, and cloud-drive options.
141141

142142
## Upgrade the instance
143143

144-
The `edge` image follows active development and may include configuration or database changes. Before upgrading, review repository notes and create a consistent backup of PostgreSQL and attachment data.
144+
The `latest` image follows releases from the release branch and may still include configuration or database changes. Before upgrading, review repository notes and create a consistent backup of PostgreSQL and attachment data.
145145

146146
```bash
147147
cd note-gen-server
@@ -158,7 +158,7 @@ curl --fail http://127.0.0.1:3789/health/ready
158158
docker compose logs --tail=200 server
159159
```
160160

161-
To pin the current experimental build, replace `edge` in `NOTEGEN_SERVER_IMAGE` with its `sha-<commit>` tag. Version tags can be used after a formal release is published.
161+
To pin the current version, replace `latest` in `NOTEGEN_SERVER_IMAGE` with a full version tag such as `ghcr.io/codexu/note-gen-server:0.1.0`. A `sha-<commit>` tag can also reproduce a specific build for troubleshooting.
162162

163163
## Data and backups
164164

‎content/docs/self-hosted.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ NoteGen Server 为 NoteGen 提供账号、设备关联、实时同步、团队
2121

2222
默认 Compose 会启动 NoteGen Server 和 PostgreSQL 17,并使用两个独立的数据卷保存数据库与附件。服务默认只监听宿主机的 `127.0.0.1:3789`。
2323

24-
官方镜像地址为 `ghcr.io/codexu/note-gen-server`,支持 `linux/amd64` 和 `linux/arm64`。当前实验版本使用 `edge` 标签,无需在服务器上安装 Node.js、pnpm 或自行编译源码。
24+
官方镜像地址为 `ghcr.io/codexu/note-gen-server`,支持 `linux/amd64` 和 `linux/arm64`。Compose 默认使用 release 分支发布的 `latest` 标签,无需在服务器上安装 Node.js、pnpm 或自行编译源码。
2525

2626
### 1. 准备服务器
2727

@@ -58,7 +58,7 @@ openssl rand -hex 32
5858
| `AUTH_SECRET` | 第二个随机值;上线后不要随意更换 |
5959
| `PUBLIC_BASE_URL` | NoteGen 客户端实际访问的地址,不带路径和末尾斜杠 |
6060
| `SERVER_BIND_ADDRESS` | 保持 `127.0.0.1`,由反向代理提供公网入口 |
61-
| `NOTEGEN_SERVER_IMAGE` | 默认使用实验性的 `edge` 镜像;需要复现特定构建时改为 `sha-<commit>` 标签 |
61+
| `NOTEGEN_SERVER_IMAGE` | 默认使用 `latest`;需要稳定复现时固定完整版本标签,例如 `0.1.0` |
6262

6363
仅在本机测试时,可以使用:
6464

@@ -83,7 +83,7 @@ docker compose ps
8383
Compose 默认拉取:
8484

8585
```text
86-
ghcr.io/codexu/note-gen-server:edge
86+
ghcr.io/codexu/note-gen-server:latest
8787
```
8888

8989
首次启动会等待 PostgreSQL 就绪,并自动执行尚未应用的数据库迁移。两个服务都显示为 `healthy` 后,再检查就绪接口:
@@ -141,7 +141,7 @@ Caddy 会自动申请和续期 HTTPS 证书。确认浏览器能够通过 `https
141141

142142
## 升级实例
143143

144-
`edge` 镜像跟随当前开发版本,可能包含配置或数据库变化。升级前先阅读仓库说明,并对 PostgreSQL 与附件数据建立同一时间点的配套备份。
144+
`latest` 镜像跟随 release 分支的最新发布,仍可能包含配置或数据库变化。升级前先阅读仓库说明,并对 PostgreSQL 与附件数据建立同一时间点的配套备份。
145145

146146
```bash
147147
cd note-gen-server
@@ -158,7 +158,7 @@ curl --fail http://127.0.0.1:3789/health/ready
158158
docker compose logs --tail=200 server
159159
```
160160

161-
如果需要固定当前实验构建,请在 `.env` 中把 `NOTEGEN_SERVER_IMAGE` 从 `edge` 改为对应的 `sha-<commit>` 标签。正式版本发布后也可以固定版本标签。
161+
如果需要固定当前版本,请在 `.env` 中把 `NOTEGEN_SERVER_IMAGE` 从 `latest` 改为完整版本标签,例如 `ghcr.io/codexu/note-gen-server:0.1.0`。排查特定构建时也可以使用 `sha-<commit>` 标签。
162162

163163
## 数据与备份
164164

‎lib/self-hosted.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Node, Root } from 'fumadocs-core/page-tree';
22

3-
export const isSelfHostedEnabled = process.env.NODE_ENV === 'development';
3+
export const isSelfHostedEnabled = true;
44

55
export function isSelfHostedDocsPath(slugs?: string[]) {
66
return slugs?.[0] === 'self-hosted';

‎middleware.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ export default function middleware(request: NextRequest) {
2828

2929
export const config = {
3030
matcher: [
31-
'/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|google08ca08cc719acf5f.html|baidu_verify_codeva-KnH6wuoxrB.html).*)',
31+
'/((?!api|_next/static|_next/image|favicon.ico|notegen-product-showcase.png|robots.txt|sitemap.xml|google08ca08cc719acf5f.html|baidu_verify_codeva-KnH6wuoxrB.html).*)',
3232
],
3333
};
666 KB
Loading

0 commit comments

Comments
 (0)