Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/deploy-download-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,38 @@ on:
paths:
- 'workers/**'
- '.github/workflows/deploy-download-worker.yml'
pull_request:
paths:
- 'workers/**'
- '.github/workflows/deploy-download-worker.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: download-worker
group: download-worker-${{ github.ref }}
cancel-in-progress: false

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Test download worker
run: node --test workers/*.test.js

deploy:
if: github.event_name != 'pull_request'
needs: test
runs-on: ubuntu-latest

steps:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Desktop installers are served through a Cloudflare Worker at:

The Worker resolves the latest `memohai/Memoh` release, redirects to a same-domain versioned URL, then caches the release asset at Cloudflare edge on first download. Users never download from a GitHub release URL directly.

Release assets are discovered by the stable platform suffix in each route and proxied through the `browser_download_url` returned by GitHub. Do not reconstruct asset URLs or couple the Worker to a product-name prefix such as `Memoh` or `Memoh-Local`.

Deploy the Worker after configuring the `memoh.ai/downloads/desktop/*` route:

```bash
Expand Down
15 changes: 8 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- app-meta-start -->
<title>MemohAI - The Agent Orchestrator</title>
<meta name="description" content="Memoh is a self-hosted agent orchestrator for hackers and AI developers, with container isolation, long-term memory, high concurrency, and zero telemetry." />
<!-- 静态 meta 与 scripts/create-spa-fallback.mjs 的 pages.home 保持一致;未知路由(404.html)也用这套兜底 -->
<title>Memoh — The multi-agent platform</title>
<meta name="description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://memoh.ai/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="MemohAI" />
<meta property="og:title" content="MemohAI - The Agent Orchestrator" />
<meta property="og:description" content="Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry." />
<meta property="og:site_name" content="Memoh" />
<meta property="og:title" content="Memoh — The multi-agent platform" />
<meta property="og:description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta property="og:url" content="https://memoh.ai/" />
<meta property="og:image" content="https://memoh.ai/logo.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="MemohAI - The Agent Orchestrator" />
<meta name="twitter:description" content="Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry." />
<meta name="twitter:title" content="Memoh — The multi-agent platform" />
<meta name="twitter:description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta name="twitter:image" content="https://memoh.ai/logo.png" />
<!-- app-meta-end -->
<link rel="preconnect" href="https://fonts.googleapis.com">
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build && node scripts/create-spa-fallback.mjs",
"preview": "vite preview"
"preview": "vite preview",
"test:worker": "node --test workers/*.test.js"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
Expand Down
19 changes: 14 additions & 5 deletions scripts/create-spa-fallback.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ const distDir = resolve('dist')
const indexPath = resolve(distDir, 'index.html')
const metaPattern = /<!-- app-meta-start -->[\s\S]*?<!-- app-meta-end -->/

// 静态 meta 必须与 src/locales 里的 seo.* 文案保持同一定位;home 同时是 index.html 的兜底内容
const pages = {
home: {
title: 'MemohAI - The Agent Orchestrator',
description: 'Memoh is a self-hosted agent orchestrator for hackers and AI developers, with container isolation, long-term memory, high concurrency, and zero telemetry.',
socialDescription: 'Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry.',
title: 'Memoh — The multi-agent platform',
description: 'Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there.',
socialDescription: 'Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there.',
url: 'https://memoh.ai/',
},
// /desktop 已重定向到 /download,但保留静态入口让旧链接拿到 200 而不是 404
desktop: {
title: 'Memoh Desktop - Download for macOS, Windows & Linux',
description: 'Download the Memoh desktop app for macOS, Windows, and Linux. Run your agent workspace natively on your computer.',
socialDescription: 'Download Memoh Desktop for macOS, Windows, and Linux.',
url: 'https://memoh.ai/desktop',
},
download: {
title: 'Download Memoh Desktop - macOS, Windows & Linux',
description: 'Pick the Memoh desktop installer for your system. Available for macOS (Apple Silicon & Intel), Windows, and Linux.',
socialDescription: 'Download Memoh Desktop for macOS, Windows, and Linux.',
url: 'https://memoh.ai/download',
},
waitlist: {
title: 'Join the Memoh waitlist',
description: 'Join the Memoh waitlist for early access to cloud agent workspaces.',
Expand Down Expand Up @@ -49,7 +57,7 @@ const renderMeta = ({ title, description, socialDescription, url }) => {
<meta name="robots" content="index, follow" />
<link rel="canonical" href="${escapeHtml(url)}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="MemohAI" />
<meta property="og:site_name" content="Memoh" />
<meta property="og:title" content="${escapeHtml(title)}" />
<meta property="og:description" content="${escapeHtml(socialDescription)}" />
<meta property="og:url" content="${escapeHtml(url)}" />
Expand All @@ -73,7 +81,8 @@ const indexHtml = await readFile(indexPath, 'utf8')
await writeFile(indexPath, withMeta(indexHtml, pages.home))
await writeFile(resolve(distDir, '404.html'), withMeta(indexHtml, pages.home))

for (const route of ['desktop', 'waitlist', 'blogs']) {
// 每个 SPA 路由都必须在这里有静态入口,否则 GitHub Pages 用 404.html 兜底、返回 404 状态码(SEO 直接判死)
for (const route of ['desktop', 'download', 'waitlist', 'blogs']) {
const routeDir = resolve(distDir, route)
await mkdir(routeDir, { recursive: true })
await writeFile(resolve(routeDir, 'index.html'), withMeta(indexHtml, pages[route]))
Expand Down
14 changes: 6 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/'
class="flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
<span aria-hidden="true" class="telegram-icon h-4 w-4"></span>
</a>
<a href="https://x.com/memohai" target="_blank" rel="noopener noreferrer" aria-label="X"
<!-- 官方 X 账号是 @memoh_ai;x.com/memohai 是别人占用或无此账号(404),别改回去 -->
<a href="https://x.com/memoh_ai" target="_blank" rel="noopener noreferrer" aria-label="X"
class="flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
<Twitter class="w-4 h-4" />
</a>
</div>
</div>

<!-- Link columns (right) -->
<div class="grid grid-cols-2 sm:grid-cols-3 gap-8 sm:gap-12">
<!-- Link columns (right) — Legal 列撤下后只剩两列,勿改回 sm:grid-cols-3 -->
<div class="grid grid-cols-2 gap-8 sm:gap-12">
<div class="flex flex-col gap-3">
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/55">{{ t('footer.product') }}</span>
<router-link to="/download" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.download') }}</router-link>
Expand All @@ -100,11 +101,8 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/'
<router-link to="/blogs" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.blogs') }}</router-link>
<a href="https://github.com/memohai/Memoh" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.github') }}</a>
</div>
<div class="flex flex-col gap-3 col-span-2 sm:col-span-1">
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/55">{{ t('footer.legal') }}</span>
<a href="https://memoh.ai" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('footer.privacy') }}</a>
<a href="https://memoh.ai" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('footer.terms') }}</a>
</div>
<!-- Legal 列整列撤下:Privacy/Terms 页面不存在,此前两个链接都指向首页占位。
页面就绪后恢复此列及 footer.privacy / footer.terms 文案。 -->
</div>
</div>

Expand Down
3 changes: 1 addition & 2 deletions src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const { isDark, toggle: toggleDark } = useThemePreference()

const { locale } = useI18n()
const docsUrl = computed(() => locale.value === 'zh' ? 'https://docs.memoh.ai/zh' : 'https://docs.memoh.ai')
const pricingUrl = 'https://memoh.ai/pricing'

const isLangOpen = ref(false)
const langMenuRef = ref<HTMLElement | null>(null)
Expand Down Expand Up @@ -69,7 +68,7 @@ const selectLang = (lang: string) => {
</RouterLink>
<nav class="hidden md:flex items-center gap-1 sm:gap-2">
<a :href="docsUrl" target="_blank" rel="noopener noreferrer" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass">{{ $t('nav.docs') }}</a>
<a :href="pricingUrl" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass">{{ $t('nav.pricing') }}</a>
<!-- Pricing 入口先撤下:/pricing 路由不存在,点进去是空白页。定价页上线时连同 nav.pricing 文案一起恢复。 -->
<RouterLink to="/blogs" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass" :active-class="overlay ? 'text-white bg-white/10' : 'text-foreground bg-accent'">{{ $t('nav.blogs') }}</RouterLink>
</nav>
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"nav": {
"docs": "Docs",
"github": "GitHub",
"pricing": "Pricing",
"blogs": "Blogs",
"supermarket": "Supermarket",
"desktop": "Desktop",
Expand Down Expand Up @@ -219,5 +218,14 @@
"egg": {
"hint": "Pet me",
"petted": "Petted {n} times"
},
"notFound": {
"seo": {
"title": "Page not found — Memoh",
"description": "The page you are looking for does not exist."
},
"title": "Page not found",
"desc": "The link may be broken or the page has moved.",
"back": "Back to home"
}
}
22 changes: 15 additions & 7 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
"nav": {
"docs": "文档",
"github": "GitHub",
"pricing": "Pricing",
"blogs": "Blogs",
"blogs": "博客",
"supermarket": "应用超市",
"desktop": "桌面版",
"download": "下载"
Expand Down Expand Up @@ -58,7 +57,7 @@
"hero": {
"title": "陪着你的%TA%",
"subtitle": "Memoh 是多 Agent 平台,每个%TA%都住在自己的电脑里\n在这里一直等着你",
"ctaPrimary": "Join waitlist",
"ctaPrimary": "加入等候名单",
"ctaSecondary": "GitHub",
"cta": "开始",
"desktop": "桌面版",
Expand Down Expand Up @@ -167,15 +166,15 @@
},
"cta_bottom": {
"title": "试试 Memoh",
"btn": "Join waitlist",
"btn": "加入等候名单",
"docs": "查看文档"
},
"waitlist": {
"seo": {
"title": "加入 Memoh Waitlist",
"description": "加入 Memoh waitlist,获取云端 Agent 工作区的早期访问资格。"
},
"kicker": "Early access",
"kicker": "抢先体验",
"title": "加入 Memoh waitlist。",
"subtitle": "留下邮箱,等 hosted Memoh 开放更多名额时我们会联系你。",
"notes": {
Expand All @@ -197,7 +196,7 @@
"subtitle": "一个邮箱就够了。",
"email": "邮箱",
"placeholder": "邮箱地址",
"submit": "Join waitlist",
"submit": "加入等候名单",
"submitting": "提交中...",
"joined": "已加入",
"success": "你已经加入 waitlist,我们会后续联系你。",
Expand All @@ -211,13 +210,22 @@
"privacy": "隐私政策",
"terms": "服务条款",
"tagline": "多 Agent 平台\n每个%TA%都有自己的电脑,一直在",
"getStarted": "Join waitlist",
"getStarted": "加入等候名单",
"product": "产品",
"resources": "资源",
"legal": "法律"
},
"egg": {
"hint": "摸摸我",
"petted": "已经摸了 {n} 下"
},
"notFound": {
"seo": {
"title": "页面不存在 — Memoh",
"description": "你要找的页面不存在。"
},
"title": "页面不存在",
"desc": "链接可能已失效,或者页面被移动了。",
"back": "回到首页"
}
}
37 changes: 37 additions & 0 deletions src/pages/NotFoundPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import { RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useHead, useSeoMeta } from '@unhead/vue'
import { ArrowLeft } from 'lucide-vue-next'

const { locale, t } = useI18n()

useSeoMeta({
title: () => t('notFound.seo.title'),
description: () => t('notFound.seo.description'),
// 404 页不应被索引;GitHub Pages 本身对未知路径就返回 404 状态,这里再显式 noindex 兜底
robots: 'noindex, follow',
})

useHead({
htmlAttrs: {
lang: computed(() => locale.value),
},
})
</script>

<template>
<main class="w-full max-w-[1080px] mx-auto px-4 md:px-8 flex flex-col items-center text-center py-32 md:py-44">
<p class="font-serif font-medium text-7xl md:text-8xl text-foreground/20 select-none" aria-hidden="true">404</p>
<h1 class="mt-6 font-serif font-medium text-3xl md:text-4xl text-foreground">{{ t('notFound.title') }}</h1>
<p class="mt-4 text-sm md:text-base text-muted-foreground">{{ t('notFound.desc') }}</p>
<RouterLink
to="/"
class="mt-10 inline-flex h-11 items-center justify-center gap-2 rounded-full px-6 text-sm font-medium bg-foreground text-background transition-transform hover:scale-[1.02] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<ArrowLeft :size="16" />
{{ t('notFound.back') }}
</RouterLink>
</main>
</template>
3 changes: 3 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const HomePage = () => import('./pages/HomePage.vue')
const DownloadPage = () => import('./pages/DownloadPage.vue')
const BlogsPage = () => import('./pages/BlogsPage.vue')
const WaitlistPage = () => import('./pages/WaitlistPage.vue')
const NotFoundPage = () => import('./pages/NotFoundPage.vue')

export const router = createRouter({
history: createWebHistory(),
Expand All @@ -15,6 +16,8 @@ export const router = createRouter({
{ path: '/blogs/:slug', name: 'blog-post', component: BlogsPage },
// /desktop has been merged into /download; keep a redirect for old links.
{ path: '/desktop', redirect: '/download' },
// Catch-all: without it, unknown URLs render an empty body between TopBar and footer.
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundPage },
],
scrollBehavior(_to, _from, savedPosition) {
if (savedPosition) return savedPosition
Expand Down
Loading
Loading