Skip to content

Commit 2523669

Browse files
feat(landing): unify multilingual download conversion (#6369)
* feat(landing): unify multilingual download conversion * fix(landing): refresh homepage proof points * fix(landing): harden download prompt triggers * fix(landing): keep iPadOS download fallback * fix(landing): localize template share URLs * fix(landing): preserve iPadOS prompt fallback --------- Co-authored-by: Joey-nexu <joeylee12629-star@users.noreply.github.com>
1 parent ba7f340 commit 2523669

48 files changed

Lines changed: 2509 additions & 448 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/landing-page/app/_components/download-engagement-prompt.astro

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.

apps/landing-page/app/_components/home-enhancer.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@
205205
const btn = document.querySelector('[data-download-cta]:not([data-download-page])');
206206
if (!btn) return;
207207
const ua = navigator.userAgent || '';
208+
const platform = (
209+
navigator.userAgentData?.platform ||
210+
navigator.platform ||
211+
''
212+
).toLowerCase();
213+
const isIpadOS = navigator.maxTouchPoints > 1 && /mac/.test(platform);
214+
if (isIpadOS || /iPhone|iPad|iPod/i.test(ua)) return;
208215
const isWin = /Windows|Win32|Win64|WOW64/i.test(ua);
209216
const isMac = /Macintosh|Mac OS X/i.test(ua) && !/iPhone|iPad|iPod/i.test(ua);
210217

apps/landing-page/app/_components/site-footer.astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../i18n';
1111
import { getFooterLegalCopy } from '../footer-legal-i18n';
1212
import { getSolutionPageCopy, type SolutionPageKey } from '../solution-pages-i18n';
13+
import DownloadEngagementPrompt from './download-engagement-prompt.astro';
1314
1415
interface Props {
1516
counts: HeaderProps['counts'];
@@ -261,3 +262,5 @@ const l = getFooterLegalCopy(locale);
261262
}
262263
</style>
263264
</footer>
265+
266+
<DownloadEngagementPrompt locale={locale} />
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
/*
3+
* Shared hero proof for high-intent solution pages. Unlike the editorial
4+
* illustrations these pages used before, every image here is a real Open
5+
* Design output or a real in-product generation screen. The image links to
6+
* the matching live template catalogue so visitors can inspect the artifact.
7+
*/
8+
import LazyImg from './lazy-img.astro';
9+
10+
interface Props {
11+
src: string;
12+
alt: string;
13+
href: string;
14+
linkLabel: string;
15+
width: number;
16+
height: number;
17+
}
18+
19+
const { src, alt, href, linkLabel, width, height } = Astro.props;
20+
---
21+
22+
<figure class="solution-hero solution-proof-media">
23+
<a href={href} aria-label={linkLabel}>
24+
<LazyImg
25+
src={src}
26+
alt={alt}
27+
loading="priority"
28+
sizes="(max-width: 880px) 100vw, 540px"
29+
width={width}
30+
height={height}
31+
/>
32+
<span class="solution-proof-open" aria-hidden="true">↗</span>
33+
</a>
34+
</figure>

apps/landing-page/app/_lib/catalog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,8 @@ export interface CatalogCounts {
873873
systems: number;
874874
templates: number;
875875
craft: number;
876+
/** User-facing bundled plugins shown in the public plugin library. */
877+
plugins: number;
876878
/** SKILL.md `od.mode` → count. Lowercase keys (e.g. `deck`, `prototype`). */
877879
byMode: Readonly<Record<string, number>>;
878880
/** SKILL.md `od.platform` → count. Lowercase keys (e.g. `mobile`, `desktop`). */
@@ -941,6 +943,7 @@ export async function getCatalogCounts(
941943
systems: systems.length,
942944
templates: templates.length,
943945
craft: craft.length,
946+
plugins: getBundledPlugins().length,
944947
byMode: tallyKey(skills.map((s) => s.mode)),
945948
byPlatform: tallyKey(skills.map((s) => s.platform)),
946949
templateCategories: computeTemplateCategories(),
@@ -964,6 +967,7 @@ export async function getCatalogCounts(
964967
systems: systems.length,
965968
templates: templates.length,
966969
craft: craft.length,
970+
plugins: getBundledPlugins().length,
967971
byMode: tallyKey(skills.map((s) => s.mode)),
968972
byPlatform: tallyKey(skills.map((s) => s.platform)),
969973
templateCategories: computeTemplateCategories(),

apps/landing-page/app/_lib/github.ts

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { RELEASE_METADATA_UPSTREAM_URL, formatStableReleaseVersion } from './rel
22

33
export interface GithubRepoMeta {
44
starsLabel: string;
5+
contributorsCount: number;
56
versionLabel: string;
67
}
78

89
const REPO_API = 'https://api.github.com/repos/nexu-io/open-design';
910
const FALLBACK_META: GithubRepoMeta = {
10-
starsLabel: '40K+',
11+
starsLabel: '83.3K+',
12+
contributorsCount: 387,
1113
// Build-time fallback when the GitHub releases API is unavailable / rate
1214
// limited. Keep in step with the latest published release.
1315
versionLabel: 'v0.9.0',
@@ -18,7 +20,23 @@ let repoMetaPromise: Promise<GithubRepoMeta> | null = null;
1820
function formatStars(count: unknown): string | null {
1921
if (typeof count !== 'number' || !Number.isFinite(count) || count <= 0) return null;
2022
if (count < 1000) return String(count);
21-
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`;
23+
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K+`;
24+
}
25+
26+
async function fetchContributorCount(): Promise<number> {
27+
const response = await fetch(`${REPO_API}/contributors?per_page=1`, {
28+
headers: { Accept: 'application/vnd.github+json' },
29+
});
30+
if (!response.ok) {
31+
throw new Error(`Request returned ${response.status}: ${response.url}`);
32+
}
33+
34+
const link = response.headers.get('link') ?? '';
35+
const lastPage = link.match(/[?&]page=(\d+)>;\s*rel="last"/);
36+
if (lastPage?.[1]) return Number.parseInt(lastPage[1], 10);
37+
38+
const contributors = (await response.json()) as unknown;
39+
return Array.isArray(contributors) ? contributors.length : 0;
2240
}
2341

2442
// Parse a display version (e.g. "v0.9.0") from a GitHub /releases/latest object.
@@ -51,18 +69,24 @@ async function fetchJson(url: string, headers?: Record<string, string>): Promise
5169

5270
export function getGithubRepoMeta(): Promise<GithubRepoMeta> {
5371
repoMetaPromise ??= (async () => {
54-
const [repoResult, releaseMetadataResult] = await Promise.allSettled([
72+
const [repoResult, contributorsResult, releaseMetadataResult] = await Promise.allSettled([
5573
fetchJson(REPO_API, { Accept: 'application/vnd.github+json' }),
74+
fetchContributorCount(),
5675
fetchJson(RELEASE_METADATA_UPSTREAM_URL, { Accept: 'application/json' }),
5776
]);
5877

5978
const repo = repoResult.status === 'fulfilled' ? repoResult.value : null;
79+
const contributorsCount =
80+
contributorsResult.status === 'fulfilled' && contributorsResult.value > 0
81+
? contributorsResult.value
82+
: null;
6083
const releaseMetadata = releaseMetadataResult.status === 'fulfilled' ? releaseMetadataResult.value : null;
6184
const starsLabel = formatStars((repo as { stargazers_count?: unknown } | null)?.stargazers_count);
6285
const versionLabel = formatStableReleaseVersion(releaseMetadata);
6386

6487
return {
6588
starsLabel: starsLabel ?? FALLBACK_META.starsLabel,
89+
contributorsCount: contributorsCount ?? FALLBACK_META.contributorsCount,
6690
versionLabel: versionLabel ?? FALLBACK_META.versionLabel,
6791
};
6892
})();
@@ -131,6 +155,66 @@ const EMPTY_MATRIX: ReleaseMatrix = {
131155
linux: null,
132156
};
133157

158+
function isRecord(value: unknown): value is Record<string, unknown> {
159+
return Boolean(value) && typeof value === 'object';
160+
}
161+
162+
function stableArtifact(value: unknown): ReleaseAsset | null {
163+
if (!isRecord(value) || typeof value.name !== 'string' || typeof value.url !== 'string') {
164+
return null;
165+
}
166+
167+
return {
168+
name: value.name,
169+
url: value.url,
170+
size: typeof value.size === 'number' && Number.isFinite(value.size) ? value.size : 0,
171+
sha256Url: typeof value.sha256Url === 'string' ? value.sha256Url : null,
172+
};
173+
}
174+
175+
function stablePlatformArtifact(
176+
metadata: unknown,
177+
platformKey: string,
178+
artifactKey: string,
179+
): ReleaseAsset | null {
180+
if (!isRecord(metadata) || !isRecord(metadata.platforms)) return null;
181+
const platform = metadata.platforms[platformKey];
182+
if (!isRecord(platform) || !isRecord(platform.artifacts)) return null;
183+
return stableArtifact(platform.artifacts[artifactKey]);
184+
}
185+
186+
/**
187+
* Parse the canonical stable-release manifest served from releases.open-design.ai.
188+
* Unlike the unauthenticated GitHub API, this endpoint is not constrained by the
189+
* shared 60-request rate limit, so high-intent download links stay direct even
190+
* when GitHub metadata cannot be resolved during a build.
191+
*/
192+
export function buildMatrixFromStableMetadata(metadata: unknown): ReleaseMatrix {
193+
return {
194+
macArm64Dmg: stablePlatformArtifact(metadata, 'mac', 'dmg'),
195+
macArm64Zip: stablePlatformArtifact(metadata, 'mac', 'zip'),
196+
macX64Dmg: stablePlatformArtifact(metadata, 'macIntel', 'dmg'),
197+
macX64Zip: stablePlatformArtifact(metadata, 'macIntel', 'zip'),
198+
winSetup: stablePlatformArtifact(metadata, 'win', 'installer'),
199+
winPortable: stablePlatformArtifact(metadata, 'win', 'portableZip'),
200+
linux:
201+
stablePlatformArtifact(metadata, 'linux', 'appImage') ??
202+
stablePlatformArtifact(metadata, 'linux', 'appimage'),
203+
};
204+
}
205+
206+
function mergeMatrices(preferred: ReleaseMatrix, fallback: ReleaseMatrix): ReleaseMatrix {
207+
return {
208+
macArm64Dmg: preferred.macArm64Dmg ?? fallback.macArm64Dmg,
209+
macArm64Zip: preferred.macArm64Zip ?? fallback.macArm64Zip,
210+
macX64Dmg: preferred.macX64Dmg ?? fallback.macX64Dmg,
211+
macX64Zip: preferred.macX64Zip ?? fallback.macX64Zip,
212+
winSetup: preferred.winSetup ?? fallback.winSetup,
213+
winPortable: preferred.winPortable ?? fallback.winPortable,
214+
linux: preferred.linux ?? fallback.linux,
215+
};
216+
}
217+
134218
function cleanVersion(versionLabel: string): string {
135219
return versionLabel.replace(/^v/, '');
136220
}
@@ -172,12 +256,13 @@ let latestReleasePromise: Promise<LatestRelease> | null = null;
172256

173257
export function getLatestRelease(): Promise<LatestRelease> {
174258
latestReleasePromise ??= (async () => {
175-
let release: unknown = null;
176-
try {
177-
release = await fetchJson(`${REPO_API}/releases/latest`);
178-
} catch {
179-
release = null;
180-
}
259+
const [releaseResult, stableMetadataResult] = await Promise.allSettled([
260+
fetchJson(`${REPO_API}/releases/latest`, { Accept: 'application/vnd.github+json' }),
261+
fetchJson(RELEASE_METADATA_UPSTREAM_URL, { Accept: 'application/json' }),
262+
]);
263+
const release = releaseResult.status === 'fulfilled' ? releaseResult.value : null;
264+
const stableMetadata =
265+
stableMetadataResult.status === 'fulfilled' ? stableMetadataResult.value : null;
181266

182267
const rec = (release && typeof release === 'object' ? release : {}) as {
183268
tag_name?: unknown;
@@ -186,17 +271,39 @@ export function getLatestRelease(): Promise<LatestRelease> {
186271
assets?: unknown;
187272
};
188273

189-
const versionLabel = formatVersion(release) ?? FALLBACK_META.versionLabel;
274+
const stableRec = isRecord(stableMetadata) ? stableMetadata : {};
275+
const versionLabel =
276+
formatVersion(release) ??
277+
formatStableReleaseVersion(stableMetadata) ??
278+
FALLBACK_META.versionLabel;
190279
const rawAssets = Array.isArray(rec.assets) ? (rec.assets as RawAsset[]) : [];
191-
const matrix = release ? buildMatrix(rawAssets) : EMPTY_MATRIX;
192-
const resolved = Boolean(release) && Object.values(matrix).some((a) => a !== null);
280+
const githubMatrix = release ? buildMatrix(rawAssets) : EMPTY_MATRIX;
281+
const stableMatrix = buildMatrixFromStableMetadata(stableMetadata);
282+
const matrix = mergeMatrices(stableMatrix, githubMatrix);
283+
const resolved = Object.values(matrix).some((asset) => asset !== null);
284+
const tagName =
285+
typeof rec.tag_name === 'string'
286+
? rec.tag_name
287+
: typeof stableRec.versionTag === 'string'
288+
? stableRec.versionTag
289+
: null;
193290

194291
return {
195292
version: cleanVersion(versionLabel),
196293
versionLabel,
197-
tagName: typeof rec.tag_name === 'string' ? rec.tag_name : null,
198-
publishedAt: typeof rec.published_at === 'string' ? rec.published_at : null,
199-
releaseUrl: typeof rec.html_url === 'string' ? rec.html_url : REPO_RELEASES,
294+
tagName,
295+
publishedAt:
296+
typeof rec.published_at === 'string'
297+
? rec.published_at
298+
: typeof stableRec.generatedAt === 'string'
299+
? stableRec.generatedAt
300+
: null,
301+
releaseUrl:
302+
typeof rec.html_url === 'string'
303+
? rec.html_url
304+
: tagName
305+
? `${REPO_RELEASES}/tag/${encodeURIComponent(tagName)}`
306+
: REPO_RELEASES,
200307
matrix,
201308
resolved,
202309
};

apps/landing-page/app/_lib/plugins-i18n.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,16 +663,16 @@ const overrides: Partial<Record<LandingLocaleCode, Partial<PluginsCopy>>> = {
663663
sceneLabel: '场景',
664664
allChip: '全部',
665665
category: {
666-
prototype: { label: '原型', description: '交互式产品稿——仪表盘、应用、落地页、内部工具。任何能交给 stakeholder 点击的东西。' },
667-
'live-artifact': { label: '实时产物', description: '可刷新、感知数据的产物,底层数据变化时自动重新渲染。实时仪表盘、监控板、周期跟踪。' },
666+
prototype: { label: '原型', description: '交互式产品稿——数据看板、应用、落地页、内部工具。任何能交给 stakeholder 点击的东西。' },
667+
'live-artifact': { label: '实时产物', description: '可刷新、感知数据的产物,底层数据变化时自动重新渲染。实时数据看板、监控板、周期跟踪。' },
668668
deck: { label: '幻灯片', description: '从叙事简报生成的精致 deck——融资 deck、课程模块、周报、产品发布。' },
669669
image: { label: '图像', description: '从结构化创意指令生成的图像——UI 稿、品牌视觉、分镜、社媒、插画。' },
670670
video: { label: '视频', description: '视频提示词、分镜与可渲染的动态产物——短视频、营销片段、动效图形、电影感故事。' },
671671
hyperframes: { label: 'HyperFrames', description: 'HyperFrames 就绪的动效合成——agent 构建的视频,融合模板 HTML 与帧级关键帧。' },
672672
audio: { label: '音频', description: '从简报生成的音频、人声与声音设计——播客片头、音乐衬底、环境音。' },
673673
},
674674
subcategory: {
675-
'business-dashboards': '仪表盘', 'app-prototypes': '应用', 'landing-marketing': '落地页 / 营销',
675+
'business-dashboards': '数据看板', 'app-prototypes': '应用', 'landing-marketing': '落地页 / 营销',
676676
'developer-tools': '开发者工具', 'docs-reports': '文档 / 报告', 'brand-design': '品牌 / 设计',
677677
'pitch-business': '路演 / 商业', 'course-training': '课程 / 培训', 'reports-briefings': '报告 / 简报',
678678
'product-sales': '产品 / 销售', 'engineering-talks': '工程演讲', 'creative-decks': '创意 deck',

0 commit comments

Comments
 (0)