Skip to content

feat(website): make prompt pages API-first with static fallback - #1079

Draft
ppapsmoken wants to merge 3 commits into
mainfrom
codex/prompt-page-first
Draft

ppapsmoken wants to merge 3 commits into
mainfrom
codex/prompt-page-first

Conversation

@ppapsmoken

Copy link
Copy Markdown
Collaborator

Background

The prompt page currently renders from a compile-time registry even though the Go application already exposes the prompt-library public API. We want to finish the Prompt page first, then import and curate the two external prompt catalogs as a separate step.

Scope

  • Make the website Prompt image/video list and detail routes API-first.
  • Normalize API prompt items into the existing page view model.
  • Keep the current static registry as a safe fallback when the API is unavailable or empty.
  • Preserve the existing Console handoff, including prompt, model, size/aspect, and quality parameters.
  • Update localized detail routes and metadata to support async API reads.

Out of scope / production safety

  • No catalog import or data migration.
  • No Go API/schema changes.
  • This PR is for review only and must not be merged until the staging-only rollout is explicitly approved. It does not publish the Prompt page by itself.

Evidence / design

The Go API already provides GET /api/prompt-library and GET /api/prompt-library/:slug; the website uses APP_CONSOLE_ORIGIN for server-side API access. API failures fall back to the existing static registry so the page remains available during the transition.

Validation

  • bun run typecheck
  • bun run lint (existing <img> warnings only)
  • bun run build
  • bun test src/components/model-landing-page.test.tsx

Deployment recommendation

Router deploy: not required. This is website-only code and has no effect on /v1 or relay paths. Intended next target is newapi-web staging only after review; do not merge to main for production yet.

@KingCesc

KingCesc commented Sep 3, 2026

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 commit 8bf48a06 · 共 2 条

website/src/lib/prompt-library.ts

  • L1273-1277: [严重] 这里把 API 返回的 ratio 限定成少量白名单后,像 4:521:93:4 这类后端可能已经支持的合法比例会被静默回退成 1:1/16:9,导致列表卡片、详情页和生成入口展示的规格与真实数据不一致。建议不要在归一化阶段覆盖有效值,至少应同步扩充前端可接受的比例集合,或在渲染层做兼容兜底。
function outputRatio(value: unknown, artifact: PromptArtifact): PromptItem["output"]["ratio"] {
  const ratio = isRecord(value) ? String(value.ratio || "") : "";
  if (ratio) return ratio as PromptItem["output"]["ratio"];
  return artifact.kind === "video" ? "16:9" : "1:1";
}

website/src/components/cli-media-library-page.tsx

  • L422-423: [严重] 这里把 API 返回的 source.url 原样带入页面,而下游 PromptCard / SourceInfo 会直接把它放到 <a href> 中。若上游数据被污染,javascript:data: 等协议会形成可点击的 XSS/钓鱼入口。建议在这里统一做协议白名单校验,只保留 http/https,其余置空或降级为纯文本展示。
const rawUrl = String(record.url || sourceUrl || "").trim();
    let url = "";
    try {
      const parsed = new URL(rawUrl);
      if (parsed.protocol === "http:" || parsed.protocol === "https:") {
        url = parsed.toString();
      }
    } catch {}
    url,

@ppapsmoken
ppapsmoken force-pushed the codex/prompt-page-first branch from 8bf48a0 to 7d1f572 Compare September 7, 2026 06:54
@KingCesc

KingCesc commented Sep 7, 2026

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 commit 7d1f5728 · 共 6 条

website/src/app/(en)/cli/image/[slug]/page.tsx

  • L6-9: [严重] 这里把 !isLocale(params.locale) 也一起重定向了,导致诸如 /xx/cli/image/foo 这类非法 locale URL 不再返回 404,而是被永久 308 到英文 prompt 页。这样会掩盖坏链路并把错误路径缓存为永久跳转,影响 SEO 和监控定位。建议对非法 locale 继续 notFound(),只对已支持的 locale 做重定向。
const target = `${PROMPT_IMAGE_PATH}/${params.slug}`;
  if (!isLocale(params.locale)) notFound();
  if (params.locale === "en") permanentRedirect(target);
  permanentRedirect(localizePath(target, params.locale));

website/src/app/sitemap.ts

  • L173-178: [严重] 这里仍然从静态 fallback 读取 prompt 列表,导致通过 API 新增但尚未进入静态数据的 prompt 详情页不会出现在动态 sitemap 中。页面本身已经 API-first,可访问的 API-only slug 会缺少 sitemap 发现入口,线上搜索收录会与实际页面数据不一致。建议在 sitemap 中改用 fetchCliMediaPromptItems,API 异常时再由该函数内部 fallback 到静态数据。
const promptEntries = (await Promise.all([
    fetchCliMediaPromptItems("image").then((items) => ({ pathname: PROMPT_IMAGE_PATH, items })),
    fetchCliMediaPromptItems("video").then((items) => ({ pathname: PROMPT_VIDEO_PATH, items })),
  ])).flatMap(({ pathname, items }) =>
    items.flatMap((item) => entry(`${pathname}/${item.slug}`, 0.62, "weekly"))
  );

website/src/lib/prompt-library.ts

  • L1240-1243: [严重] 这里把 title/summary 当成“多语言对象”处理;如果 API 实际返回的是普通字符串,会直接回退到 slug 文本,导致列表/详情页标题和摘要被错误替换。建议兼容字符串输入,至少在收到字符串时把原文映射到各语言回退值。
function localizedText(value: unknown, fallback: string): Record<Locale, string> {
  if (typeof value === "string" && value.trim()) {
    return withIdFallback({ en: value, zh: value, es: value, fr: value, pt: value, ru: value, ja: value, vi: value, de: value });
  }
  const record = isRecord(value) ? value : {};
  const en = typeof record.en === "string" && record.en.trim() ? record.en : fallback;
  const zh = typeof record.zh === "string" && record.zh.trim() ? record.zh : en;
  • L1318-1320: [严重] 这里把 source.url 作为硬性门槛,API 只要漏传外链就会把整条 prompt 静默丢弃。这样 API-first 模式下会直接少内容,而页面渲染层本身已经对空链接做了兜底。建议不要因为缺少 URL 就过滤掉记录,保留条目并在展示外链时按空值处理。
const source = promptSource(value.source, value.source_platform, value.source_url);
  const title = localizedText(value.title, slug.replace(/-/g, " "));

website/src/components/prompt-directory.tsx

  • L79-88: [严重] 这里把筛选状态单向写回地址栏,但没有监听 popstate 或路由变化把 location.search 回填到组件 state。用户使用浏览器前进/后退,或者打开带查询参数的目录链接后,URL 和当前筛选结果会不同步,页面可能继续展示旧筛选条件。建议增加从 URL 反向同步的逻辑,或者直接以 useSearchParams 作为状态源。
useEffect(() => {
    const syncFromLocation = () => {
      const params = new URLSearchParams(window.location.search);
      setQuery(params.get("q") ?? "");
      setType(params.get("type") ?? "");
      setModel(params.get("model") ?? "");
      setUseCase(params.get("useCase") ?? "");
      setSource(params.get("source") ?? "");
      setSort((params.get("sort") as Sort) || "updated");
    };

    window.addEventListener("popstate", syncFromLocation);
    return () => window.removeEventListener("popstate", syncFromLocation);
  }, []);

  useEffect(() => {
    const params = new URLSearchParams();
    if (query) params.set("q", query);
    if (type) params.set("type", type);
    if (model) params.set("model", model);
    if (useCase) params.set("useCase", useCase);
    if (source) params.set("source", source);
    if (sort !== "updated") params.set("sort", sort);
    window.history.replaceState(null, "", `${localizePath("/prompts", locale)}${params.toString() ? `?${params}` : ""}`);
  }, [locale, model, query, sort, source, type, useCase]);
  • L59-77: [严重] 该组件在客户端一次性接收并内存中过滤/排序全部 prompt 数据,而且后续每次输入都会重新在浏览器端遍历整份数据。随着提示词库增长,这会明显放大首屏 JS 体积和交互时的 CPU 开销,低端设备上可能出现搜索卡顿。建议改为服务端分页/按条件拉取,或至少在客户端做分页与更轻量的搜索索引。
const matched = useMemo(() => {
    const needle = query.trim().toLowerCase();
    return items
      .filter((item) => !type || item.category === type)
      .filter((item) => !model || item.model === model)
      .filter((item) => !useCase || item.tags.includes(useCase))
      .filter((item) => !source || item.source.platform === source)
      .filter((item) => {
        if (!needle) return true;
        const title = item.title[locale] ?? item.title.en;
        const summary = item.summary[locale] ?? item.summary.en;
        return [title, summary, item.prompt, item.model, ...item.tags].join(" ").toLowerCase().includes(needle);
      })
      .sort((a, b) => {
        if (sort === "oldest") return Date.parse(a.updatedAt) - Date.parse(b.updatedAt);
        if (sort === "complete") return artifactScore(b.artifact) - artifactScore(a.artifact);
        return Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
      });
  }, [items, locale, model, query, sort, source, type, useCase]);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants