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
484 changes: 484 additions & 0 deletions .agents/skills/humanizer-zh/SKILL.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions skills-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"humanizer-zh": {
"source": "op7418/humanizer-zh",
"sourceType": "github",
"skillPath": "SKILL.md",
"computedHash": "dede9681d9fea5caed2fb9d88656af3024c9ec0136fc833415ffc6e16d762429"
}
}
}
16 changes: 13 additions & 3 deletions src/app/[lang]/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ import { isLocale } from "@/i18n/config";
import { auth } from "@/auth";
import CodeWorkspace from "@/components/ui/CodeWorkspace";

export const metadata: Metadata = {
title: "Weftmap — Editor",
};
import { getAlternates } from "@/lib/seo";

export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
return {
title: "Weftmap — Editor",
alternates: getAlternates("app", lang),
};
}

export default async function AppPage({
params,
Expand Down
7 changes: 6 additions & 1 deletion src/app/[lang]/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export function generateStaticParams() {
return locales.flatMap((lang) => DOC_SLUGS.map((slug) => ({ lang, slug })));
}

import { getAlternates } from "@/lib/seo";

export async function generateMetadata({
params,
}: {
Expand All @@ -19,7 +21,10 @@ export async function generateMetadata({
const item = DOC_NAV.find((d) => d.slug === slug);
const locale: Locale = isLocale(lang) ? lang : "en";
const title = item ? (item.title[locale] ?? item.title["en"]) : "Docs";
return { title: `${title} — Weftmap` };
return {
title: `${title} — Weftmap`,
alternates: getAlternates(`docs/${slug}`, lang),
};
}

export default async function DocPage({
Expand Down
16 changes: 13 additions & 3 deletions src/app/[lang]/graphs/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ import { getGraph } from "@/lib/graphs";
import Diagram from "@/components/ui/Diagram";
import type { Graph } from "@/lib/analysis/types";

export const metadata: Metadata = {
title: "Weftmap — Saved graph",
};
import { getAlternates } from "@/lib/seo";

export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string; id: string }>;
}): Promise<Metadata> {
const { lang, id } = await params;
return {
title: "Weftmap — Saved graph",
alternates: getAlternates(`graphs/${id}`, lang),
};
}
Comment on lines +13 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Emitting hreflang alternates for an auth-gated, per-user saved graph is questionable.

Unlike the homepage/app/docs/graphs index routes, graphs/${id} is a private, user-specific resource gated by auth()/notFound() and is not translated content. Advertising canonical + per-locale languages here tells crawlers each locale serves an equivalent translated page, which isn't true and exposes the existence of specific graph IDs in head tags. Consider omitting alternates (or setting robots: { index: false }) for saved-graph pages rather than generating locale alternates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[lang]/graphs/[id]/page.tsx around lines 13 - 23, The saved graph
metadata in generateMetadata is incorrectly advertising hreflang alternates for
a private, user-specific page. Update the metadata for this route to avoid
calling getAlternates for graphs/${id}, and instead omit alternates entirely or
add a robots noindex directive for the auth-gated page. Keep the change
localized to generateMetadata in the saved-graph page so public routes that use
getAlternates remain unaffected.


export default async function SavedGraphPage({
params,
Expand Down
16 changes: 13 additions & 3 deletions src/app/[lang]/graphs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import { auth } from "@/auth";
import { listGraphs } from "@/lib/graphs";
import GraphListItem from "@/components/ui/GraphListItem";

export const metadata: Metadata = {
title: "Weftmap — My graphs",
};
import { getAlternates } from "@/lib/seo";

export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
return {
title: "Weftmap — My graphs",
alternates: getAlternates("graphs", lang),
};
}

export default async function GraphsPage({
params,
Expand Down
18 changes: 14 additions & 4 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ import Footer from "@/components/layout/Footer";
import "@fontsource-variable/lexend";
import "../globals.css";

export const metadata: Metadata = {
title: "Weftmap",
description: "Paste code and get an interactive call graph.",
};
import { getAlternates } from "@/lib/seo";

export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
return {
title: "Weftmap",
description: "Paste code and get an interactive call graph.",
alternates: getAlternates("", lang),
};
}

export function generateStaticParams() {
return locales.map((lang) => ({ lang }));
Expand Down
21 changes: 21 additions & 0 deletions src/lib/seo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { locales } from "@/i18n/config";

export function getAlternates(path: string, currentLang: string) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://weftmap.com";

// cleanPath should start with slash if path is not empty, otherwise empty string
const cleanPath = path ? `/${path}` : "";

const languages = locales.reduce((acc, locale) => {
acc[locale] = `${baseUrl}/${locale}${cleanPath}`;
return acc;
}, {} as Record<string, string>);

// Set default fallback language version to English
languages["x-default"] = `${baseUrl}/en${cleanPath}`;

return {
canonical: `${baseUrl}/${currentLang}${cleanPath}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

currentLang flows into the canonical URL without validation.

generateMetadata runs before the page component's isLocale(lang)/notFound() check, so a request to an unsupported locale (e.g. /zz/app) still emits canonical: ${baseUrl}/zz/app, advertising a self-referential canonical for a 404. Consider normalizing currentLang to a valid Locale (falling back to defaultLocale) before building the canonical, mirroring the isLocale guard the routes already use.

🛡️ Proposed guard
-import { locales } from "`@/i18n/config`";
+import { locales, isLocale, defaultLocale } from "`@/i18n/config`";

 export function getAlternates(path: string, currentLang: string) {
   const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://weftmap.com";
+  const lang = isLocale(currentLang) ? currentLang : defaultLocale;

   // cleanPath should start with slash if path is not empty, otherwise empty string
   const cleanPath = path ? `/${path}` : "";
@@
   return {
-    canonical: `${baseUrl}/${currentLang}${cleanPath}`,
+    canonical: `${baseUrl}/${lang}${cleanPath}`,
     languages,
   };
 }

This is the root cause for the unvalidated lang passed by the downstream generateMetadata callers in the metadata-wiring files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
canonical: `${baseUrl}/${currentLang}${cleanPath}`,
canonical: `${baseUrl}/${lang}${cleanPath}`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/seo.ts` at line 18, Normalize currentLang before constructing the
canonical in generateMetadata so unsupported locales do not produce
self-referential canonicals. Use the existing Locale/defaultLocale logic
(matching the isLocale guard used by the route handlers) to validate or fallback
currentLang, then build canonical from the sanitized locale in seo.ts; this also
covers the downstream generateMetadata callers that pass lang through unchanged.

languages,
};
}
Loading