feat(site): add 17 real-world workflows showcase page and top-level nav entry

This commit is contained in:
tianma-if committed 2026-10-25 10:12:35 +08:00
1 parent fae1c227d0
commit e2279127b6
15 files changed
+1446 -4

No files matched your search

+5
View File
@@ -45,5 +45,10 @@ export default defineConfig({
},
vite: {
plugins: [tailwindcss()],
server: {
fs: {
allow: ["../.."],
},
},
},
});
@@ -0,0 +1,328 @@
---
import { Icon } from "astro-icon/components";
import Container from "@/components/container.astro";
import Layout from "@/layouts/Layout.astro";
import { bestPracticesPageCopy, showcaseCaseAssets } from "@/data/best-practices";
import { getLocalizedPath, getSiteLocale } from "@/i18n";
const locale = getSiteLocale(Astro.url.pathname);
const copy = bestPracticesPageCopy[locale];
const pageUrl = new URL(Astro.url.pathname, Astro.site).toString();
const allCases = copy.categories.flatMap((category) => category.cases);
const jsonLd = [
{
"@context": "https://schema.org",
"@type": "CollectionPage",
name: copy.title,
description: copy.description,
url: pageUrl,
inLanguage: locale,
isPartOf: {
"@type": "WebSite",
name: "EdgeEver",
url: new URL(getLocalizedPath(locale, "/"), Astro.site).toString(),
},
},
{
"@context": "https://schema.org",
"@type": "ItemList",
name: copy.title,
numberOfItems: allCases.length,
itemListElement: allCases.map((item, idx) => ({
"@type": "ListItem",
position: idx + 1,
name: `${item.number}. ${item.title}`,
description: item.lead,
url: `${pageUrl}#case-${item.number}`,
})),
},
];
---
<Layout title={copy.title} description={copy.description} jsonLd={jsonLd}>
<Container>
<main class="mx-auto max-w-6xl py-10 md:py-16">
<!-- Hero Header -->
<header class="max-w-3xl">
<span class="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-bold tracking-widest text-emerald-700 bg-emerald-50 rounded-full border border-emerald-200/70 uppercase">
<Icon name="bx:bx-grid-alt" class="w-3.5 h-3.5 text-emerald-600" />
<span>{copy.eyebrow}</span>
</span>
<h1 class="mt-3.5 text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight text-slate-900 leading-[1.15]">
{copy.title}
</h1>
<p class="mt-4 text-sm sm:text-base md:text-lg leading-relaxed text-slate-600">
{copy.description}
</p>
<!-- Action Buttons -->
<div class="mt-6 flex flex-wrap items-center gap-3">
<a
href="https://demo.edgeever.org"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-full text-xs sm:text-sm font-semibold text-white bg-brand hover:bg-brand-hover shadow-sm transition-all hover:-translate-y-0.5"
>
<Icon name="bx:bx-show" class="w-4 h-4" />
<span>{copy.demoCta}</span>
</a>
<a
href={getLocalizedPath(locale, "/blog/ai-agent-deploy-cloudflare")}
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-xs sm:text-sm font-semibold text-brand-text bg-brand/5 border border-brand/20 hover:border-brand/40 hover:bg-brand/10 transition-all"
>
<Icon name="simple-icons:cloudflare" class="w-3.5 h-3.5" />
<span>{copy.deployCta}</span>
</a>
<a
href={copy.githubDocHref}
target="_blank"
rel="noopener"
class="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-full text-xs sm:text-sm font-semibold text-slate-700 bg-white border border-slate-200/90 hover:border-slate-300 hover:bg-slate-50 transition-all"
>
<Icon name="bx:bxl-github" class="w-4 h-4 text-slate-800" />
<span>{copy.githubDocCta}</span>
</a>
</div>
</header>
<!-- Sticky Category Bar & Scenario Directory -->
<nav
aria-label={copy.eyebrow}
class="sticky top-3 z-30 mt-8 rounded-2xl border border-slate-200/90 bg-white/90 backdrop-blur-md p-2.5 sm:p-3 shadow-sm"
>
<div class="flex items-center justify-between gap-2 overflow-x-auto no-scrollbar">
<div class="flex items-center gap-1.5 sm:gap-2 shrink-0">
{
copy.categories.map((cat) => (
<a
href={`#cat-${cat.id}`}
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-slate-700 hover:text-emerald-700 hover:bg-emerald-50/80 border border-transparent hover:border-emerald-200/60 transition-colors whitespace-nowrap"
>
<span>{cat.badge}</span>
<span class="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-mono text-slate-500">
{cat.cases.length}
</span>
</a>
))
}
</div>
<span class="hidden lg:inline-flex items-center gap-1.5 text-[11px] text-slate-400 pr-2 shrink-0">
<Icon name="bx:bx-zoom-in" class="w-3.5 h-3.5 text-emerald-600" />
<span>{copy.zoomHint}</span>
</span>
</div>
</nav>
<!-- Compact 17-Scenario Quick Index -->
<div class="mt-4 rounded-2xl border border-slate-200/70 bg-slate-50/70 p-4 sm:p-5">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-2">
{
allCases.map((item) => (
<a
href={`#case-${item.number}`}
class="group flex items-center gap-2 py-1 text-xs sm:text-[13px] text-slate-600 hover:text-emerald-700 transition-colors truncate"
>
<span class="inline-flex h-5 w-6 shrink-0 items-center justify-center rounded bg-white border border-slate-200/80 font-mono text-[10.5px] font-bold text-emerald-700 group-hover:border-emerald-300 group-hover:bg-emerald-50 transition-colors">
{item.number}
</span>
<span class="truncate font-medium">{item.title}</span>
</a>
))
}
</div>
</div>
<!-- Categories & Cases -->
<div class="mt-10 sm:mt-14 space-y-14 sm:space-y-20">
{
copy.categories.map((category, catIdx) => (
<section id={`cat-${category.id}`} class="scroll-mt-24">
<div class="border-b border-slate-200/80 pb-4 mb-6 sm:mb-8">
<span class="inline-block px-2.5 py-0.5 text-[11px] font-bold text-emerald-700 bg-emerald-100/70 rounded-md">
{category.badge}
</span>
<h2 class="mt-2 text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
{category.title}
</h2>
<p class="mt-1.5 text-xs sm:text-sm md:text-base text-slate-600">
{category.description}
</p>
</div>
<div class="space-y-8 sm:space-y-10">
{category.cases.map((caseItem, caseIdx) => {
const assetConfig = showcaseCaseAssets[caseItem.id];
const isFirstCase = catIdx === 0 && caseIdx === 0;
const gridClass =
assetConfig.layout === "grid-3"
? "grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-5 items-start"
: assetConfig.layout === "grid-2"
? "grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 items-start"
: "flex flex-col gap-5 sm:gap-6";
return (
<article
id={`case-${caseItem.number}`}
class="scroll-mt-24 rounded-2xl sm:rounded-3xl border border-slate-200/90 bg-white p-5 sm:p-7 md:p-8 shadow-xs transition-shadow hover:shadow-md"
>
<!-- Case Header -->
<div class="max-w-4xl">
<div class="flex items-center gap-2.5">
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-lg bg-emerald-600 text-white font-mono text-xs font-bold">
#{caseItem.number}
</span>
<h3 class="text-lg sm:text-xl md:text-2xl font-bold text-slate-900 tracking-tight">
{caseItem.title}
</h3>
</div>
<p class="mt-3 text-xs sm:text-sm md:text-base leading-relaxed text-slate-600">
{caseItem.lead}
</p>
<ul class="mt-3 space-y-1.5 text-xs sm:text-sm text-slate-600">
{caseItem.bullets.map((bullet) => (
<li class="flex items-start gap-2 leading-relaxed">
<span class="mt-1.5 h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0" />
<span>{bullet}</span>
</li>
))}
</ul>
{caseItem.links && caseItem.links.length > 0 && (
<div class="mt-3.5 flex flex-wrap items-center gap-2">
{caseItem.links.map((link) => (
<a
href={link.href}
target="_blank"
rel="noopener"
class="inline-flex items-center gap-1 px-3 py-1 rounded-lg text-xs font-semibold text-emerald-700 bg-emerald-50 hover:bg-emerald-100/80 border border-emerald-200/70 transition-colors"
>
<span>{link.label}</span>
<Icon name="bx:bx-link-external" class="w-3.5 h-3.5" />
</a>
))}
</div>
)}
</div>
<!-- Case Screenshots -->
<div class={`mt-5 sm:mt-6 ${gridClass}`}>
{caseItem.media.map((mediaItem, mediaIdx) => {
const imgAsset = assetConfig.images[mediaIdx];
if (!imgAsset) return null;
return (
<figure class="flex flex-col rounded-xl sm:rounded-2xl border border-slate-200/80 bg-white overflow-hidden shadow-2xs">
<figcaption class="flex items-center justify-between gap-2 px-4 py-2.5 border-b border-slate-200/70 bg-slate-50/80 text-xs sm:text-sm font-semibold text-slate-700">
<span class="truncate">{mediaItem.caption}</span>
<span class="text-[11px] font-normal text-slate-400 shrink-0 flex items-center gap-1">
<Icon name="bx:bx-fullscreen" class="w-3.5 h-3.5" />
</span>
</figcaption>
<button
type="button"
data-lightbox-src={imgAsset.src}
data-lightbox-alt={mediaItem.alt}
data-lightbox-caption={`${caseItem.number}. ${caseItem.title} — ${mediaItem.caption}`}
class="relative block w-full overflow-hidden bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50 cursor-zoom-in"
aria-label={`${mediaItem.caption} - ${copy.zoomHint}`}
>
<img
src={imgAsset.src}
width={imgAsset.width}
height={imgAsset.height}
alt={mediaItem.alt}
loading={isFirstCase && mediaIdx === 0 ? "eager" : "lazy"}
decoding="async"
class="w-full h-auto block mx-auto"
/>
</button>
</figure>
);
})}
</div>
</article>
);
})}
</div>
</section>
))
}
</div>
</main>
</Container>
<!-- Image Lightbox Dialog -->
<dialog
id="showcase-lightbox"
class="m-auto max-h-[92vh] max-w-[94vw] rounded-2xl border border-slate-700/80 bg-slate-950/95 p-2 sm:p-4 text-white shadow-2xl backdrop:bg-slate-950/75 backdrop:backdrop-blur-xs"
>
<div class="flex flex-col gap-2.5 max-h-[88vh]">
<div class="flex items-center justify-between gap-4 px-2 pt-1">
<p id="showcase-lightbox-caption" class="text-xs sm:text-sm font-medium text-slate-200 truncate"></p>
<button
type="button"
id="showcase-lightbox-close"
aria-label={copy.closePreview}
class="inline-flex items-center gap-1 rounded-lg bg-slate-800 hover:bg-slate-700 px-2.5 py-1 text-xs font-semibold text-slate-200 transition-colors shrink-0 cursor-pointer"
>
<span>ESC</span>
<Icon name="bx:bx-x" class="w-4 h-4" />
</button>
</div>
<div class="overflow-auto flex items-center justify-center rounded-xl bg-slate-900/60 p-1">
<img
id="showcase-lightbox-img"
src=""
alt=""
class="max-h-[80vh] max-w-[90vw] w-auto h-auto object-contain rounded-lg"
/>
</div>
</div>
</dialog>
</Layout>
<script is:inline>
(function () {
function initLightbox() {
var dialog = document.getElementById("showcase-lightbox");
var img = document.getElementById("showcase-lightbox-img");
var caption = document.getElementById("showcase-lightbox-caption");
var closeBtn = document.getElementById("showcase-lightbox-close");
if (!dialog || !img || !caption) return;
document.querySelectorAll("[data-lightbox-src]").forEach(function (btn) {
btn.addEventListener("click", function () {
var src = btn.getAttribute("data-lightbox-src");
var alt = btn.getAttribute("data-lightbox-alt") || "";
var cap = btn.getAttribute("data-lightbox-caption") || alt;
if (!src) return;
img.setAttribute("src", src);
img.setAttribute("alt", alt);
caption.textContent = cap;
if (typeof dialog.showModal === "function") {
dialog.showModal();
}
});
});
if (closeBtn) {
closeBtn.addEventListener("click", function () {
dialog.close();
});
}
dialog.addEventListener("click", function (e) {
if (e.target === dialog) {
dialog.close();
}
});
}
initLightbox();
document.addEventListener("astro:page-load", initLightbox);
})();
</script>
+15 -3
View File
@@ -1,17 +1,17 @@
---
import { Icon } from "astro-icon/components";
import { getSiteLocale, siteCopy } from "@/i18n";
import { getLocalizedPath, getSiteLocale, siteCopy } from "@/i18n";
const locale = getSiteLocale(Astro.url.pathname);
const bento = siteCopy[locale].bento;
---
<section class="mt-12 md:mt-24 relative overflow-hidden" aria-labelledby="bento-features">
<section id="features" class="mt-12 md:mt-24 relative overflow-hidden scroll-mt-6" aria-labelledby="bento-features">
<!-- Subtle Glow background -->
<div class="absolute top-1/3 left-1/2 -translate-x-1/2 w-[700px] h-[400px] bg-emerald-500/5 rounded-full blur-3xl -z-10 pointer-events-none"></div>
<!-- Header -->
<div class="max-w-3xl mb-8 md:mb-16 text-center mx-auto px-4">
<div class="max-w-3xl mb-8 md:mb-14 text-center mx-auto px-4">
<span class="inline-block px-3.5 py-1 text-xs font-bold tracking-widest text-emerald-600 bg-emerald-50 rounded-full border border-emerald-200/60 uppercase">
{bento.eyebrow}
</span>
@@ -21,6 +21,18 @@ const bento = siteCopy[locale].bento;
<p class="text-slate-600 mt-3 sm:mt-4 text-xs sm:text-sm md:text-base leading-relaxed max-w-2xl mx-auto">
{bento.subheading}
</p>
<div class="mt-5 flex justify-center">
<a
href={getLocalizedPath(locale, "/guides/best-practices")}
class="group inline-flex items-center gap-2 px-4 py-2 rounded-full text-xs sm:text-sm font-semibold text-emerald-800 bg-emerald-50/90 hover:bg-emerald-100/90 border border-emerald-200/80 hover:border-emerald-300 shadow-2xs transition-all hover:-translate-y-0.5"
>
<Icon name="bx:bx-images" class="w-4 h-4 text-emerald-600 shrink-0" />
<span>{bento.showcaseCta}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-3.5 h-3.5 text-emerald-600 transition-transform group-hover:translate-x-0.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</a>
</div>
</div>
<!-- Bento Grid Container -->
+4
View File
@@ -30,6 +30,10 @@ const copy = siteCopy[locale].nav;
{locale === "zh-CN" ? "特别致谢" : locale === "ja" ? "謝辞" : locale === "pl" ? "Podziękowania" : "Thanks"}
</a>
<span class="text-slate-300">•</span>
<a href={getLocalizedPath(locale, "/guides/best-practices")} class="inline-flex min-h-11 sm:min-h-0 items-center hover:text-emerald-700 transition-colors">
{copy.bestPractices}
</a>
<span class="text-slate-300">•</span>
<a href={getLocalizedPath(locale, "/self-hosted-evernote-alternative")} class="inline-flex min-h-11 sm:min-h-0 items-center hover:text-emerald-700 transition-colors">
{copy.selfHostedAlternative}
</a>
+1 -1
View File
@@ -18,7 +18,7 @@ const copy = siteCopy[locale].guides;
</p>
</div>
<div class="mt-6 md:mt-8 grid gap-4 sm:grid-cols-2 md:grid-cols-3">
<div class="mt-6 md:mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{
copy.items.map((item) => (
<a
@@ -12,6 +12,10 @@ const menuitems = [
title: copy.features,
path: `${getLocalizedPath(locale, "/")}#features`,
},
{
title: copy.bestPractices,
path: getLocalizedPath(locale, "/guides/best-practices"),
},
{
title: copy.deploy,
children: [
File diff suppressed because it is too large. Load diff
+8
View File
@@ -22,6 +22,7 @@ export const jaSiteCopy = {
memosMigration: "Memos から移行",
notionMigration: "Notion から移行",
flomoMigration: "Flomo から移行",
bestPractices: "実機デモ",
advancedPlay: "AI Agent の使い方",
blog: "ブログ",
backToBlog: "ブログ一覧へ",
@@ -54,6 +55,7 @@ export const jaSiteCopy = {
heading: "セルフホストの知識管理と AI のために作り直した",
subheading:
"広告、端末数の制限、高いサブスクはやめましょう。速さ、開放性、自分で守れるプライバシーを前提にしています。",
showcaseCta: "17 の実機デモとベストプラクティスを見る",
card1: {
badge: "クラシック UI",
title: "三ペインはそのまま、より軽く速く",
@@ -270,6 +272,12 @@ export const jaSiteCopy = {
href: "/blog/evernote-migration-guide",
cta: "移行ガイドを読む",
},
{
title: "17 の実機デモとベストプラクティス",
summary: "三ペイン執筆、マルチチャネル取り込み、AI 図解・多次元テーブル、ローカル ACP 連携、ポスター作成の実機例。",
href: "/guides/best-practices",
cta: "実機デモを見る",
},
{
title: "AI Agent の応用",
summary: "MCP で、実ノートから知識マップ、タグ整理、一段上の個人知識ワークフローを作れます。",
+8
View File
@@ -22,6 +22,7 @@ export const plSiteCopy = {
memosMigration: "Migracja z Memos",
notionMigration: "Migracja z Notion",
flomoMigration: "Migracja z Flomo",
bestPractices: "Prezentacja",
advancedPlay: "Praca z agentami AI",
blog: "Blog",
backToBlog: "Wróć do bloga",
@@ -54,6 +55,7 @@ export const plSiteCopy = {
heading: "Zbudowany od nowa z myślą o samodzielnym hostingu wiedzy i pracy z AI",
subheading:
"Bez reklam, limitów urządzeń i drogich subskrypcji. Każdy szczegół służy szybkiemu, otwartemu i prywatnemu „drugiemu mózgowi”.",
showcaseCta: "Zobacz 17 praktycznych scenariuszy i zrzutów ekranu",
card1: {
badge: "Klasyczny interfejs",
title: "Klasyczny układ trzech paneli – szybszy i lżejszy",
@@ -270,6 +272,12 @@ export const plSiteCopy = {
href: "/blog/evernote-migration-guide",
cta: "Przeczytaj poradnik migracji",
},
{
title: "17 praktycznych scenariuszy",
summary: "Wizualny przegląd pisania w trzech panelach, wycinania treści, diagramów i tabel AI, lokalnych agentów ACP i plakatów.",
href: "/guides/best-practices",
cta: "Zobacz przykłady",
},
{
title: "Zaawansowana praca z agentami AI",
summary: "Zamieniaj prawdziwe notatki w mapy wiedzy, plany porządkowania tagów i bardziej zaawansowane osobiste przepływy wiedzy dzięki MCP.",
+16
View File
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bestPracticesPageCopy, showcaseCaseAssets } from "./data/best-practices.ts";
import { alternativePageCopy } from "./data/self-hosted-evernote-alternative.ts";
import {
getLocalizedPath,
@@ -88,4 +89,19 @@ describe("site locale routing", () => {
expect(siteCopy.pl.layout.ogLocale).toBe("pl_PL");
expect(siteCopy.pl.nav.backToBlog).toBe("Wróć do bloga");
});
test("keeps best practices showcase copy and media counts aligned across locales", () => {
for (const locale of siteLocales) {
expect(collectKeys(bestPracticesPageCopy[locale])).toEqual(
collectKeys(bestPracticesPageCopy["zh-CN"]),
);
const cases = bestPracticesPageCopy[locale].categories.flatMap((c) => c.cases);
expect(cases).toHaveLength(17);
for (const item of cases) {
const assets = showcaseCaseAssets[item.id];
expect(assets).toBeDefined();
expect(item.media).toHaveLength(assets.images.length);
}
}
});
});
+16
View File
@@ -114,6 +114,7 @@ export const siteCopy = {
memosMigration: "从 Memos 迁移",
notionMigration: "从 Notion 迁移",
flomoMigration: "从 Flomo 迁移",
bestPractices: "真机演示",
advancedPlay: "搭配AI Agent的玩法",
blog: "博客",
backToBlog: "返回博客",
@@ -144,6 +145,7 @@ export const siteCopy = {
eyebrow: "WHY EDGEEVER",
heading: "重新定义自托管知识工作区",
subheading: "告别商业笔记的臃肿广告、设备限制与昂贵订阅。每一个设计细节,都旨在打造流畅、开放、隐私可控且原生支持 AI 的第二大脑。",
showcaseCta: "查看 17 个真机案例演示与最佳实践",
card1: {
badge: "经典重现",
title: "经典三栏与专注模式,熟悉更轻快",
@@ -359,6 +361,12 @@ export const siteCopy = {
href: "/blog/evernote-migration-guide",
cta: "查看迁移指南",
},
{
title: "17 个真机场景与实操指南",
summary: "浏览三栏写作、微信与全渠道剪藏、AI 导图/信息图/多维表格、本机 ACP 协同及海报排版实机演示。",
href: "/guides/best-practices",
cta: "查看实机案例",
},
{
title: "AI Agent 进阶玩法",
summary: "用 MCP 读取真实笔记,生成知识地图、标签建议和个人资料整理工作流。",
@@ -389,6 +397,7 @@ export const siteCopy = {
memosMigration: "Migrate from Memos",
notionMigration: "Migrate from Notion",
flomoMigration: "Migrate from Flomo",
bestPractices: "Showcase",
advancedPlay: "AI Agent plays",
blog: "Blog",
backToBlog: "Back to blog",
@@ -419,6 +428,7 @@ export const siteCopy = {
eyebrow: "WHY EDGEEVER",
heading: "Rebuilt for Self-Hosted Knowledge & AI Workflows",
subheading: "Say goodbye to ads, device caps, and expensive subscriptions. Every detail is crafted for a fast, open, and private second brain.",
showcaseCta: "Explore 17 real-world workflows & screenshots",
card1: {
badge: "Classic UI",
title: "Classic Three-Pane Layout, Faster & Lighter",
@@ -634,6 +644,12 @@ export const siteCopy = {
href: "/blog/evernote-migration-guide",
cta: "Read migration guide",
},
{
title: "17 Real-World Workflows",
summary: "Visual walkthrough of three-pane editing, omnichannel clipping, AI diagrams & databases, local ACP agents, and poster design.",
href: "/guides/best-practices",
cta: "View showcase",
},
{
title: "AI Agent advanced play",
summary: "Turn real notes into knowledge maps, tag cleanup plans, and higher-level personal knowledge workflows through MCP.",
@@ -0,0 +1,5 @@
---
import BestPracticesShowcase from "@/components/best-practices-showcase.astro";
---
<BestPracticesShowcase />
@@ -0,0 +1,5 @@
---
import BestPracticesShowcase from "@/components/best-practices-showcase.astro";
---
<BestPracticesShowcase />
@@ -0,0 +1,5 @@
---
import BestPracticesShowcase from "@/components/best-practices-showcase.astro";
---
<BestPracticesShowcase />
@@ -0,0 +1,5 @@
---
import BestPracticesShowcase from "@/components/best-practices-showcase.astro";
---
<BestPracticesShowcase />