Files
multica/apps/web/proxy.ts
Naiyuan Qing 5f1f08e466 feat(web): add use-cases content pipeline with welcome page (MUL-2349) (#2795)
* feat(web): add use-cases content pipeline with welcome page (MUL-2349)

Wire fumadocs-mdx into apps/web with an independent collection rooted at
content/use-cases/. Add the first page at /use-cases/welcome (header + H1 +
prose + screenshot + footer) using the about-page visual shell.

- source.config.ts + lib/use-cases-source.ts (separate from apps/docs)
- features/landing/components/mdx/screenshot.tsx wraps next/image
- public/use-cases/welcome/screenshot-1.png placeholder (55KB)
- next.config.ts wraps NextConfig with createMDX()
- .gitignore + eslint ignore .source/

Co-authored-by: multica-agent <github@multica.ai>

* feat(web): bilingual db-boy use case with cookie locale (MUL-2349)

Extends the use-cases pipeline into the first real article.

- ZH + EN MDX (auto-data-analysis.{zh,en}.mdx) sharing three real
  screenshots; sensitive fields on db-boy-profile.png (RDS host, DB
  name, password) are blurred in-place.
- Cookie-based locale: /use-cases/<slug> reads multica-locale
  server-side via lib/use-cases-i18n.ts (mirrors LandingLayout's
  cookie + Accept-Language fallback). Same URL serves either language;
  no [lang] segment so all other landing routes stay unchanged.
- Frontmatter schema (source.config.ts): z.looseObject with declared
  hero_image / updated_at (required) / category (optional); a
  preprocess converts YAML-auto-parsed Date back to a YYYY-MM-DD string.
- MDX components factory createMdxComponents(locale) routes the
  secondary CTA to /docs/zh (ZH) or /docs (EN); internal MDX links
  use <Link> for SPA nav; full-width and half-width colons both
  trigger [CTA: ...] / [占位图: ...] markers; 副 and Secondary
  both work as the secondary CTA prefix.
- Index page localizes hero / subtitle / card CTA / metadata; sort
  fallback uses an epoch placeholder so undefined-order disappears.
- Landing header + footer surface use-cases entry in both locales.
- Detail route: sticky header, right-rail TOC with anchor jumps,
  scroll-mt-[100px] on H2/H3 so anchor jumps don't slip under the
  sticky header.
- Drop welcome demo page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): resolve code review blockers on use-cases PR

- Add `use-cases` to reserved_slugs.json + regenerate TS (P1: prevent
  future workspace slug collision)
- Fix dead links in both MDX files: /features/* → /docs/* (P2)
- Remove duplicate brand suffix in page title metadata (nit)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(web): align usecases locale routing

* chore: refresh web mdx lockfile

* fix(web): type mdx next config adapter

* fix(web): wrap settings route page

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:05:17 +08:00

94 lines
3.4 KiB
TypeScript

import { NextResponse, type NextRequest } from "next/server";
import { LOCALE_COOKIE } from "@multica/core/i18n";
import {
MULTICA_LOCALE_HEADER,
resolveLocaleFromSignals,
} from "./lib/locale-routing";
// Old workspace-scoped route segments that existed before the URL refactor
// (pre-#1131). Any URL with these as the FIRST segment is a legacy URL that
// needs to be rewritten to /{slug}/{route}/... so old bookmarks, deep links,
// and post-revert-and-reapply users don't hit 404.
const LEGACY_ROUTE_SEGMENTS = new Set([
"issues",
"projects",
"agents",
"inbox",
"my-issues",
"autopilots",
"runtimes",
"skills",
"settings",
]);
function resolveLocale(req: NextRequest): string {
return resolveLocaleFromSignals({
cookieLocale: req.cookies.get(LOCALE_COOKIE)?.value,
acceptLanguage: req.headers.get("accept-language"),
});
}
// Forward the resolved locale to RSC layouts via the `x-multica-locale`
// request header. layout.tsx reads it through `await headers()`. The
// `request: { headers }` form is what makes the header land on the upstream
// request — without it the value would only sit on the response.
function nextWithLocale(req: NextRequest): NextResponse {
const headers = new Headers(req.headers);
headers.set(MULTICA_LOCALE_HEADER, resolveLocale(req));
return NextResponse.next({ request: { headers } });
}
// Next.js 16 renamed `middleware` → `proxy`. API surface (NextRequest /
// NextResponse / cookies / matcher) is identical; the only behavioral
// change is the runtime — proxy is forced to nodejs and cannot opt into
// edge.
export function proxy(req: NextRequest) {
const { pathname } = req.nextUrl;
const hasSession = req.cookies.has("multica_logged_in");
const lastSlug = req.cookies.get("last_workspace_slug")?.value;
// --- Legacy URL redirect: /issues/... → /{slug}/issues/... ---
// Old bookmarks and clients that hit us before the slug migration would
// otherwise 404 since the route moved under [workspaceSlug].
const firstSegment = pathname.split("/")[1] ?? "";
if (LEGACY_ROUTE_SEGMENTS.has(firstSegment)) {
const url = req.nextUrl.clone();
if (!hasSession) {
url.pathname = "/login";
return NextResponse.redirect(url);
}
if (lastSlug) {
// Preserve deep-link path + query: /issues/abc → /{lastSlug}/issues/abc
url.pathname = `/${lastSlug}${pathname}`;
return NextResponse.redirect(url);
}
// Logged-in but no cookie yet (first login since slug migration, or
// cookie cleared). Bounce to root; the root-path logic below picks a
// workspace and writes the cookie, then future hits short-circuit here.
url.pathname = "/";
return NextResponse.redirect(url);
}
// --- Root path: redirect logged-in users to their last workspace ---
if (pathname === "/" && hasSession && lastSlug) {
const url = req.nextUrl.clone();
url.pathname = `/${lastSlug}/issues`;
return NextResponse.redirect(url);
}
// --- Default: forward locale header to RSC, no redirect/rewrite ---
// Covers logged-out root path, /login, /:slug/*, and everything else.
return nextWithLocale(req);
}
export const config = {
// i18n header must land on every page request, so we use the standard
// negative-lookahead pattern from Next's i18n guide: skip API routes
// (Go backend), Next internals, and any path with a file extension
// (favicons, sw.js, public/* assets).
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)"],
};