diff --git a/e2e/seo.spec.ts b/e2e/seo.spec.ts
new file mode 100644
index 0000000..4556c9c
--- /dev/null
+++ b/e2e/seo.spec.ts
@@ -0,0 +1,140 @@
+import { test, expect, type Page } from "@playwright/test";
+
+const SITE_URL = "https://weftmap.vercel.app";
+
+declare global {
+ interface Window {
+ __cspViolations: string[];
+ }
+}
+
+// The CSP is built per request in src/proxy.ts and has no 'unsafe-inline' in
+// either dev or production — only 'unsafe-eval' differs — so the nonce path
+// this test exercises is the same one that runs in production.
+async function collectCspViolations(page: Page, path: string) {
+ await page.addInitScript(() => {
+ window.__cspViolations = [];
+ document.addEventListener("securitypolicyviolation", (event) => {
+ window.__cspViolations.push(
+ `${event.violatedDirective} blocked ${event.blockedURI}`,
+ );
+ });
+ });
+ await page.goto(path);
+ return page.evaluate(() => window.__cspViolations);
+}
+
+test("homepage emits JSON-LD that the CSP does not block", async ({ page }) => {
+ const violations = await collectCspViolations(page, "/en");
+ expect(violations).toEqual([]);
+
+ // The nonce has to be checked in the server HTML, not the DOM: browsers blank
+ // the attribute once the CSP is applied, so page.locator() always sees "".
+ const response = await page.request.get("/en");
+ const html = await response.text();
+ const csp = response.headers()["content-security-policy"];
+ const scriptNonce = html.match(
+ /" sequence inside any string from
+ // closing the tag early. < is valid JSON, so parsers are unaffected.
+ const json = JSON.stringify(data).replace(/
+ );
+}
diff --git a/src/i18n/dictionaries/ar.json b/src/i18n/dictionaries/ar.json
index 585cfb5..49be872 100644
--- a/src/i18n/dictionaries/ar.json
+++ b/src/i18n/dictionaries/ar.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "مخطط قاعدة البيانات SQL",
"showcaseSchemaDesc": "تتحول الجداول إلى كيانات والروابط الخارجية إلى علاقات مع تحديد نوع العلاقة.",
"languagesTitle": "اللغات المدعومة",
- "languagesSubtitle": "ست لغات حالياً — خمسة منها ترسم مخطط استدعاءات، ولغة SQL ترسم مخطط كيانات وقواعد بيانات.",
+ "languagesSubtitle": "سبع لغات حالياً — ست منها ترسم مخطط استدعاءات، ولغة SQL ترسم مخطط كيانات وعلاقات.",
"languageRows": [
{
"name": "بايثون (Python)",
@@ -103,6 +103,11 @@
"kind": "مخطط استدعاءات",
"detail": "الدوال والاستدعاءات"
},
+ {
+ "name": "جافا (Java)",
+ "kind": "مخطط استدعاءات",
+ "detail": "التوابع والأصناف"
+ },
{
"name": "إس كيو إل (SQL)",
"kind": "مخطط قواعد البيانات",
@@ -194,5 +199,125 @@
"testimonialAllRatings": "الكل",
"testimonialLoadMore": "تحميل المزيد",
"testimonialNoResults": "لا توجد تجارب تطابق عوامل التصفية."
+ },
+ "seo": {
+ "home": {
+ "title": "مولّد مخطط الاستدعاءات لـ Python وGo وRust وSQL",
+ "description": "ألصق الكود واحصل على مخطط استدعاءات تفاعلي خلال ثوانٍ. يدعم Python وJavaScript وTypeScript وGo وRust وJava وSQL. مجاني ومفتوح المصدر بالكامل."
+ },
+ "app": {
+ "title": "ألصق الكود واحصل على المخطط",
+ "description": "ألصق مقطعاً برمجياً أو ارفع مجلداً، اختر اللغة، واحصل على مخطط استدعاءات تفاعلي قابل للتكبير والتصدير. بدون تسجيل، ولا نحفظ أي شيء من كودك."
+ },
+ "graphs": {
+ "title": "مخططاتي المحفوظة",
+ "description": "مخططات الاستدعاءات ومخططات قواعد البيانات المحفوظة لديك، جاهزة لإعادة الفتح أو المشاركة أو التصدير وقت ما تشاء. سجّل الدخول عبر GitHub للاحتفاظ بها."
+ },
+ "docs.introduction": {
+ "title": "مقدمة",
+ "description": "ما هو Weftmap، وما المشكلة التي يحلها، وكيف تجعل بنية Tree-sitter القابلة للتوسيع تحليل مخططات الاستدعاءات عبر عدة لغات أمراً عملياً وسهل التطبيق."
+ },
+ "docs.getting-started": {
+ "title": "البدء السريع",
+ "description": "استخدم Weftmap في المتصفح أو محلياً: ألصق مقطعاً برمجياً، اختر اللغة، واقرأ أول مخطط استدعاءات لك في أقل من دقيقة واحدة. شرح خطوة بخطوة بالتفصيل."
+ },
+ "docs.reading-the-diagram": {
+ "title": "قراءة المخطط",
+ "description": "كيف تقرأ مخطط Weftmap: ماذا تعني العقد والأسهم، ولماذا تُستبعد استدعاءات المكتبات الخارجية، وكيف يرتّب تخطيط dagre الدوال داخل المخطط النهائي."
+ },
+ "docs.languages": {
+ "title": "اللغات المدعومة",
+ "description": "كل اللغات التي يحللها Weftmap حالياً — Python وJavaScript وTypeScript وGo وRust وJava وSQL — وما الذي يستخرجه كل محلّل منها من داخل الكود لديك."
+ },
+ "docs.how-it-works": {
+ "title": "كيف يعمل",
+ "description": "داخل خط المعالجة: التحقق من المدخلات، التحليل عبر Tree-sitter WASM، اجتياز الشجرة النحوية، ثم المخطط الموحّد الذي يُرسل إلى React Flow لعرضه."
+ },
+ "docs.api": {
+ "title": "مرجع الواجهة البرمجية",
+ "description": "نقطة النهاية POST /api/analyze: شكل الطلب، واللغات المقبولة، وصيغة JSON للمخطط المُعاد، وحدود الحجم المسموح بها، والأخطاء التي قد تصادفك."
+ },
+ "docs.adding-a-language": {
+ "title": "إضافة لغة جديدة",
+ "description": "أضف لغة إلى Weftmap عبر LangSpec واحد واستعلامَي Tree-sitter. الدليل الكامل، من ملف WASM الخاص بالقواعد النحوية إلى مخطط استدعاءات يعمل فعلياً."
+ },
+ "docs.faq": {
+ "title": "الأسئلة الشائعة",
+ "description": "هل يغادر كودك جهازك، وما هي حدود الحجم المسموح بها، وأي اللغات مدعومة، وتحت أي رخصة يُنشر Weftmap. إجابات مختصرة ومباشرة عن أكثر الأسئلة تكراراً."
+ },
+ "landing.python": {
+ "title": "مولّد مخطط استدعاءات Python",
+ "description": "ألصق كود Python وشاهد أي دالة تستدعي الأخرى في مخطط تفاعلي. يفهم الأصناف والاستيرادات والوراثة. مجاني تماماً وبدون تثبيت أي شيء على جهازك."
+ },
+ "landing.javascript": {
+ "title": "مولّد مخطط استدعاءات JavaScript",
+ "description": "حوّل كود JavaScript إلى مخطط استدعاءات تفاعلي. الدوال العادية ودوال السهم والأصناف واستيرادات ES، كلها تُحلَّل مباشرة في المتصفح ومجاناً للاستخدام."
+ },
+ "landing.typescript": {
+ "title": "مولّد مخطط استدعاءات TypeScript",
+ "description": "ارسم مسار الاستدعاءات لأي ملف TypeScript في مخطط تفاعلي. يعتمد على القواعد النحوية المُنمّطة، لذا تُقرأ الأنواع العامة والواجهات بشكل صحيح تماماً."
+ },
+ "landing.go": {
+ "title": "مولّد مخطط استدعاءات Go",
+ "description": "ألصق كود Go واحصل على مخطط يوضّح أي الدوال والتوابع تستدعي الأخرى. تُستبعد استدعاءات المكتبة القياسية تلقائياً كي يبقى المخطط واضحاً وسهل القراءة."
+ },
+ "landing.rust": {
+ "title": "مولّد مخطط استدعاءات Rust",
+ "description": "شاهد كيف تستدعي دوال Rust بعضها البعض في مخطط تفاعلي. ألصق وحدة برمجية أو ملفاً كاملاً وتتبّع مسار التنفيذ دون أن تغادر متصفحك أو تثبّت أي أداة."
+ },
+ "landing.java": {
+ "title": "مولّد مخطط استدعاءات Java",
+ "description": "ألصق كود Java واحصل على مخطط استدعاءات تفاعلي لتوابعك وأصنافك. مفيد لمراجعة خدمة غير مألوفة والاطلاع على بنيتها قبل قراءتها سطراً بسطر بالكامل."
+ },
+ "landing.sql": {
+ "title": "مولّد مخطط الكيانات من SQL",
+ "description": "ألصق تعليمات SQL واحصل على مخطط كيانات وعلاقات: الجداول كعقد، والمفاتيح الأجنبية كأسهم. اقرأ أي مخطط قاعدة بيانات دون الحاجة إلى فتح القاعدة نفسها."
+ }
+ },
+ "landing": {
+ "exampleHeading": "مثال",
+ "showsHeading": "ماذا يعرض المخطط",
+ "ctaTitle": "جرّبه الآن على الكود الخاص بك",
+ "ctaDesc": "ألصق ملفاً، اختر اللغة، واضغط تحليل. لا يتم حفظ أي شيء.",
+ "ctaButton": "افتح المحرر",
+ "docsReadDiagram": "كيف تقرأ المخطط",
+ "docsLanguages": "كل اللغات المدعومة",
+ "pages": {
+ "python": {
+ "h1": "مولّد مخطط استدعاءات Python",
+ "intro": "ألصق ملف Python وسيرسم Weftmap أي دالة تستدعي الأخرى. مفيد حين ترث وحدة برمجية بلا توثيق وتحتاج أن ترى شكلها العام قبل قراءتها سطراً بسطر.",
+ "shows": "محلّل Python يتعرّف على الدوال والأصناف والاستيرادات والوراثة، فيظهر الصنف الفرعي متصلاً بالصنف الذي يرث منه. أما الاستدعاءات إلى المكتبة القياسية وحزم الطرف الثالث فتُستبعد، لأنها ستغرق المخطط دون أن تخبرك بشيء عن كودك أنت."
+ },
+ "javascript": {
+ "h1": "مولّد مخطط استدعاءات JavaScript",
+ "intro": "ضع ملف JavaScript واحصل على مخطط لمسار الاستدعاءات فيه. عملي مع ملف الأدوات المساعدة الذي تضخّم عبر ثلاث سنوات ولم يعد أحد يعرف أي نصف منه ما زال مستخدماً.",
+ "shows": "تصريحات الدوال ودوال السهم وتوابع الأصناف تصبح كلها عقداً، واستيرادات ES تربط الملفات التي أتت منها. أما دوال رد النداء المُمرّرة مباشرة فتُربط بالدالة التي تسمّيها، فيبقى المعالج الموصول عبر مُطلِق أحداث ظاهراً متصلاً في المخطط."
+ },
+ "typescript": {
+ "h1": "مولّد مخطط استدعاءات TypeScript",
+ "intro": "اطّلع على بنية الاستدعاءات في أي ملف TypeScript دون إعداد خادم لغة. تُلصق الكود، وتختار TypeScript، وتقرأ المخطط.",
+ "shows": "يحلّل Weftmap لغة TypeScript بقواعدها النحوية الخاصة بدلاً من إزالة الأنواع أولاً، فلا تُعطِّل الأنواع العامة ولا الواجهات ولا التعليقات النوعية عملية التحليل. النتيجة هي نفس مخطط محلّل JavaScript، لكن على كود يعجز عنه أي محلّل JavaScript عادي."
+ },
+ "go": {
+ "h1": "مولّد مخطط استدعاءات Go",
+ "intro": "ألصق كود Go وشاهد أي الدوال والتوابع تستدعي غيرها. طريقة سريعة لتقدير حجم حزمة برمجية أنت على وشك تعديلها.",
+ "shows": "لا تحتوي Go على أصناف، لذا يخرج المخطط أبسط من نظيره في Python أو Java: الدوال والتوابع عقد، والاستدعاءات أسهم. تبقى المُستقبِلات ملتصقة بتوابعها فيتضح إلى أي نوع ينتمي كل تابع. أما استدعاءات المكتبة القياسية فتُستبعد تلقائياً."
+ },
+ "rust": {
+ "h1": "مولّد مخطط استدعاءات Rust",
+ "intro": "تتبّع كيف تصل دوال وحدة Rust إلى بعضها، دون انتظار تشغيل cargo check كاملاً. تُلصق الملف فيظهر المخطط مباشرة.",
+ "shows": "الدوال الحرة واستدعاءاتها هي ما يشكّل المخطط. ولأن التحليل نحوي بحت، لا يشترط أن يُصرَّف الملف: يمكنك توجيه Weftmap إلى وحدة في منتصف إعادة هيكلة، وبينما مدقق الاستعارة ما زال معترضاً، وتحصل رغم ذلك على صورة واضحة."
+ },
+ "java": {
+ "h1": "مولّد مخطط استدعاءات Java",
+ "intro": "ألصق صنف Java واحصل على مخطط لكيفية استدعاء توابعه لبعضها. مصمَّم للحظة التي تفتح فيها خدمة لا تعرفها وتريد الصورة العامة قبل التفاصيل.",
+ "shows": "تصبح التوابع عقداً وتجمّعها الأصناف، فيعكس المخطط البنية التي تراها أصلاً في الملف. أما الاستدعاءات الموجّهة إلى JDK وإلى المكتبات الخارجية فتُحذف، ولا يبقى سوى المسارات التي تخص الكود الذي ألصقته أنت."
+ },
+ "sql": {
+ "h1": "مولّد مخطط الكيانات من SQL",
+ "intro": "ألصق تعليمات SQL واحصل على مخطط كيانات وعلاقات. بلا سلسلة اتصال، وبلا عميل، وبلا صلاحية قراءة على أي شيء: تكفي عبارات CREATE TABLE وحدها.",
+ "shows": "هذه هي اللغة الوحيدة التي لا يرسم فيها Weftmap مخطط استدعاءات. تصبح الجداول عقداً تحمل أعمدتها، وتُعلَّم المفاتيح الأساسية والأجنبية، ويتحول كل مفتاح أجنبي إلى سهم نحو الجدول الذي يشير إليه، موسوماً بنوع العلاقة بينهما."
+ }
+ }
}
}
diff --git a/src/i18n/dictionaries/en.json b/src/i18n/dictionaries/en.json
index 134e18e..5113561 100644
--- a/src/i18n/dictionaries/en.json
+++ b/src/i18n/dictionaries/en.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "SQL schema",
"showcaseSchemaDesc": "Tables become entities and foreign keys become relationships with cardinality.",
"languagesTitle": "Languages",
- "languagesSubtitle": "Six today — five produce a call graph, SQL produces an ER diagram.",
+ "languagesSubtitle": "Seven today — six produce a call graph, SQL produces an ER diagram.",
"languageRows": [
{
"name": "Python",
@@ -98,7 +98,16 @@
"kind": "Call graph",
"detail": "Functions and methods (no classes)"
},
- { "name": "Rust", "kind": "Call graph", "detail": "Functions and calls" },
+ {
+ "name": "Rust",
+ "kind": "Call graph",
+ "detail": "Functions and calls"
+ },
+ {
+ "name": "Java",
+ "kind": "Call graph",
+ "detail": "Methods and classes"
+ },
{
"name": "SQL",
"kind": "ER diagram",
@@ -190,5 +199,125 @@
"testimonialAllRatings": "All",
"testimonialLoadMore": "Load more",
"testimonialNoResults": "No experiences match your filters."
+ },
+ "seo": {
+ "home": {
+ "title": "Call graph generator for Python, Go, Rust & SQL",
+ "description": "Paste source code and get an interactive call graph in seconds. Python, JavaScript, TypeScript, Go, Rust, Java and SQL. Free and open source."
+ },
+ "app": {
+ "title": "Paste code, get a call graph",
+ "description": "Paste a snippet or upload a folder, pick a language, and get an interactive call graph you can pan, zoom and export. No signup, nothing stored."
+ },
+ "graphs": {
+ "title": "My saved graphs",
+ "description": "Your saved call graphs and schema diagrams, ready to reopen, share or export whenever you need them. Sign in with GitHub to keep your analyses."
+ },
+ "docs.introduction": {
+ "title": "Introduction",
+ "description": "What Weftmap is, which problem it solves, and how a pluggable Tree-sitter backend makes call graph analysis work across many languages at once."
+ },
+ "docs.getting-started": {
+ "title": "Getting started",
+ "description": "Run Weftmap locally or use it in the browser: paste a snippet, choose a language, and read your first call graph in under a minute. Step by step."
+ },
+ "docs.reading-the-diagram": {
+ "title": "Reading the diagram",
+ "description": "How to read a Weftmap call graph: what nodes and arrows mean, why library calls are filtered out, and how the dagre layout orders the functions."
+ },
+ "docs.languages": {
+ "title": "Supported languages",
+ "description": "Every language Weftmap analyses today — Python, JavaScript, TypeScript, Go, Rust, Java and SQL — with what each analyzer extracts from your code."
+ },
+ "docs.how-it-works": {
+ "title": "How it works",
+ "description": "Inside the pipeline: input validation, Tree-sitter WASM parsing, syntax tree traversal, and the normalized nodes-and-edges graph sent to React Flow."
+ },
+ "docs.api": {
+ "title": "API reference",
+ "description": "The POST /api/analyze endpoint: request shape, supported language values, the graph JSON it returns, size limits, and the errors you can get back."
+ },
+ "docs.adding-a-language": {
+ "title": "Adding a language",
+ "description": "Add a new language to Weftmap with one LangSpec and two Tree-sitter queries. The full walkthrough, from grammar WASM to a working call graph."
+ },
+ "docs.faq": {
+ "title": "Frequently asked questions",
+ "description": "Does your code leave your machine, what are the size limits, which languages are supported, and how Weftmap is licensed. The short answers."
+ },
+ "landing.python": {
+ "title": "Python call graph generator",
+ "description": "Paste Python code and see which function calls which, as an interactive diagram. Handles classes, imports and inheritance. Free, no install needed."
+ },
+ "landing.javascript": {
+ "title": "JavaScript call graph generator",
+ "description": "Turn JavaScript into an interactive call graph. Function declarations, arrow functions, classes and ES imports, parsed in the browser. Free to use."
+ },
+ "landing.typescript": {
+ "title": "TypeScript call graph generator",
+ "description": "Map the call flow of any TypeScript file into an interactive diagram. Uses the typed grammar, so generics and interfaces parse cleanly. Free to use."
+ },
+ "landing.go": {
+ "title": "Go call graph generator",
+ "description": "Paste Go source and get a diagram of which functions and methods call which. Standard library calls are filtered out to keep the graph readable."
+ },
+ "landing.rust": {
+ "title": "Rust call graph generator",
+ "description": "See how your Rust functions call each other as an interactive diagram. Paste a module or a whole file and trace the flow without leaving the browser."
+ },
+ "landing.java": {
+ "title": "Java call graph generator",
+ "description": "Paste Java code and get an interactive call graph of your methods and classes. Useful for reviewing unfamiliar services before you read them fully."
+ },
+ "landing.sql": {
+ "title": "SQL schema diagram generator",
+ "description": "Paste SQL DDL and get an entity-relationship diagram: tables as nodes, foreign keys as edges. Read an unfamiliar schema without opening a database."
+ }
+ },
+ "landing": {
+ "exampleHeading": "Example",
+ "showsHeading": "What the graph shows",
+ "ctaTitle": "Now try it with your own code",
+ "ctaDesc": "Paste a file, pick the language, hit Analyze. Nothing is stored.",
+ "ctaButton": "Open the editor",
+ "docsReadDiagram": "How to read the diagram",
+ "docsLanguages": "All supported languages",
+ "pages": {
+ "python": {
+ "h1": "Python call graph generator",
+ "intro": "Paste a Python file and Weftmap draws which function calls which. Useful when you inherit a module with no docstrings and need the shape of it before reading top to bottom.",
+ "shows": "The Python analyzer picks up functions, classes, imports and inheritance, so a subclass shows up connected to the base it extends. Calls into the standard library and third-party packages are left out — they would swamp the diagram without telling you anything about your own code."
+ },
+ "javascript": {
+ "h1": "JavaScript call graph generator",
+ "intro": "Drop in a JavaScript file and get a diagram of its call flow. Handy for the utility module that grew for three years and now nobody is sure which half is still reachable.",
+ "shows": "Function declarations, arrow functions and class methods all become nodes, and ES imports link the files they come from. Callbacks passed inline resolve to the function they name, so a handler wired through an event emitter still appears connected."
+ },
+ "typescript": {
+ "h1": "TypeScript call graph generator",
+ "intro": "See the call structure of any TypeScript file without setting up a language server. Paste it, pick TypeScript, and read the diagram.",
+ "shows": "Weftmap parses TypeScript with its own grammar rather than stripping types first, so generics, interfaces and type annotations never derail the parse. What you get is the same call graph as the JavaScript analyzer, on code that plain JS parsers choke on."
+ },
+ "go": {
+ "h1": "Go call graph generator",
+ "intro": "Paste Go source and see which functions and methods call which. A quick way to size up a package you are about to change.",
+ "shows": "Go has no classes, so the graph is flatter than the Python or Java equivalent: functions and methods are nodes, calls are arrows. Receivers are kept with their methods, so it stays clear which type a method hangs off. Standard library calls are filtered out."
+ },
+ "rust": {
+ "h1": "Rust call graph generator",
+ "intro": "Trace how the functions in a Rust module reach each other, without waiting on a full cargo check. Paste a file and the diagram appears.",
+ "shows": "Free functions and their calls become the graph. Because the parse is purely syntactic, the file does not need to compile — you can point Weftmap at a module mid-refactor, while the borrow checker is still unhappy with it, and still get a readable picture."
+ },
+ "java": {
+ "h1": "Java call graph generator",
+ "intro": "Paste a Java class and get a diagram of how its methods call one another. Built for the moment you open an unfamiliar service and want the outline before the detail.",
+ "shows": "Methods become nodes and classes group them, so the diagram reflects the structure you already see in the file. Calls to the JDK and to libraries are dropped, leaving only the paths that belong to the code you pasted."
+ },
+ "sql": {
+ "h1": "SQL schema diagram generator",
+ "intro": "Paste SQL DDL and get an entity-relationship diagram. No connection string, no client, no read access to anything — just the CREATE TABLE statements.",
+ "shows": "This is the one language where Weftmap does not draw a call graph. Tables become nodes carrying their columns, primary and foreign keys are marked, and each foreign key becomes an arrow to the table it references, labelled with its cardinality."
+ }
+ }
}
}
diff --git a/src/i18n/dictionaries/es.json b/src/i18n/dictionaries/es.json
index 8b16a19..9d49619 100644
--- a/src/i18n/dictionaries/es.json
+++ b/src/i18n/dictionaries/es.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "Esquema SQL",
"showcaseSchemaDesc": "Las tablas son entidades y las foreign keys, relaciones con cardinalidad.",
"languagesTitle": "Lenguajes",
- "languagesSubtitle": "Seis hoy — cinco generan un call graph y SQL un diagrama ER.",
+ "languagesSubtitle": "Siete hoy — seis generan un call graph y SQL un diagrama ER.",
"languageRows": [
{
"name": "Python",
@@ -98,7 +98,16 @@
"kind": "Call graph",
"detail": "Funciones y métodos (sin clases)"
},
- { "name": "Rust", "kind": "Call graph", "detail": "Funciones y llamadas" },
+ {
+ "name": "Rust",
+ "kind": "Call graph",
+ "detail": "Funciones y llamadas"
+ },
+ {
+ "name": "Java",
+ "kind": "Call graph",
+ "detail": "Métodos y clases"
+ },
{
"name": "SQL",
"kind": "Diagrama ER",
@@ -190,5 +199,125 @@
"testimonialAllRatings": "Todas",
"testimonialLoadMore": "Cargar más",
"testimonialNoResults": "Ninguna experiencia coincide con los filtros."
+ },
+ "seo": {
+ "home": {
+ "title": "Generador de call graph: Python, Go, Rust y SQL",
+ "description": "Pega código y obtén un grafo de llamadas interactivo en segundos. Python, JavaScript, TypeScript, Go, Rust, Java y SQL. Gratis y open source."
+ },
+ "app": {
+ "title": "Pega código, obtén un call graph",
+ "description": "Pega un fragmento o sube una carpeta, elige el lenguaje y obtén un grafo de llamadas interactivo con zoom y exportación. Sin registro ni guardado."
+ },
+ "graphs": {
+ "title": "Mis grafos guardados",
+ "description": "Tus call graphs y diagramas de esquema guardados, listos para reabrir, compartir o exportar cuando quieras. Inicia sesión con GitHub para conservarlos."
+ },
+ "docs.introduction": {
+ "title": "Introducción",
+ "description": "Qué es Weftmap, qué problema resuelve y cómo un backend enchufable sobre Tree-sitter hace viable el análisis de call graphs en varios lenguajes."
+ },
+ "docs.getting-started": {
+ "title": "Empezar",
+ "description": "Usa Weftmap en el navegador o localmente: pega un fragmento, elige el lenguaje y lee tu primer grafo de llamadas en menos de un minuto. Paso a paso."
+ },
+ "docs.reading-the-diagram": {
+ "title": "Leer el diagrama",
+ "description": "Cómo leer un call graph de Weftmap: qué significan nodos y flechas, por qué se filtran las llamadas a librerías y cómo ordena el layout de dagre."
+ },
+ "docs.languages": {
+ "title": "Lenguajes soportados",
+ "description": "Todos los lenguajes que Weftmap analiza hoy — Python, JavaScript, TypeScript, Go, Rust, Java y SQL — y qué extrae de tu código cada analizador."
+ },
+ "docs.how-it-works": {
+ "title": "Cómo funciona",
+ "description": "Dentro del pipeline: validación de entrada, parseo con Tree-sitter WASM, recorrido del árbol sintáctico y el grafo normalizado que llega a React Flow."
+ },
+ "docs.api": {
+ "title": "Referencia de la API",
+ "description": "El endpoint POST /api/analyze: forma de la petición, lenguajes admitidos, el JSON del grafo que devuelve, límites de tamaño y errores posibles."
+ },
+ "docs.adding-a-language": {
+ "title": "Agregar un lenguaje",
+ "description": "Agrega un lenguaje a Weftmap con un LangSpec y dos consultas de Tree-sitter. La guía completa, del WASM de la gramática al call graph funcionando."
+ },
+ "docs.faq": {
+ "title": "Preguntas frecuentes",
+ "description": "Si tu código sale de tu máquina, cuáles son los límites de tamaño, qué lenguajes están soportados y bajo qué licencia está Weftmap. Respuestas cortas."
+ },
+ "landing.python": {
+ "title": "Generador de call graph de Python",
+ "description": "Pega código Python y mira qué función llama a cuál, como diagrama interactivo. Entiende clases, imports y herencia. Gratis y sin instalar nada."
+ },
+ "landing.javascript": {
+ "title": "Generador de call graph de JavaScript",
+ "description": "Convierte JavaScript en un grafo de llamadas interactivo. Funciones, arrow functions, clases e imports ES, parseados al vuelo. Gratis de usar."
+ },
+ "landing.typescript": {
+ "title": "Generador de call graph de TypeScript",
+ "description": "Mapea el flujo de llamadas de cualquier archivo TypeScript en un diagrama interactivo. Usa la gramática tipada, así que genéricos e interfaces parsean bien."
+ },
+ "landing.go": {
+ "title": "Generador de call graph de Go",
+ "description": "Pega código Go y obtén un diagrama de qué funciones y métodos llaman a cuáles. Las llamadas a la librería estándar se filtran para que se lea claro."
+ },
+ "landing.rust": {
+ "title": "Generador de call graph de Rust",
+ "description": "Mira cómo se llaman entre sí tus funciones de Rust en un diagrama interactivo. Pega un módulo o un archivo entero y sigue el flujo sin salir del navegador."
+ },
+ "landing.java": {
+ "title": "Generador de call graph de Java",
+ "description": "Pega código Java y obtén un grafo de llamadas interactivo de tus métodos y clases. Útil para revisar servicios ajenos antes de leerlos por completo."
+ },
+ "landing.sql": {
+ "title": "Generador de diagramas ER para SQL",
+ "description": "Pega DDL de SQL y obtén un diagrama entidad-relación: tablas como nodos, claves foráneas como aristas. Lee un esquema ajeno sin abrir la base de datos."
+ }
+ },
+ "landing": {
+ "exampleHeading": "Ejemplo",
+ "showsHeading": "Qué muestra el diagrama",
+ "ctaTitle": "Ahora pruébalo con tu propio código",
+ "ctaDesc": "Pega un archivo, elige el lenguaje y dale a Analizar. No se guarda nada.",
+ "ctaButton": "Abrir el editor",
+ "docsReadDiagram": "Cómo leer el diagrama",
+ "docsLanguages": "Todos los lenguajes soportados",
+ "pages": {
+ "python": {
+ "h1": "Generador de call graph de Python",
+ "intro": "Pega un archivo de Python y Weftmap dibuja qué función llama a cuál. Sirve cuando heredas un módulo sin docstrings y necesitas verle la forma antes de leerlo de arriba a abajo.",
+ "shows": "El analizador de Python detecta funciones, clases, imports y herencia, así que una subclase aparece conectada con la base que extiende. Las llamadas a la librería estándar y a paquetes de terceros quedan fuera: llenarían el diagrama sin decirte nada de tu propio código."
+ },
+ "javascript": {
+ "h1": "Generador de call graph de JavaScript",
+ "intro": "Suelta un archivo de JavaScript y obtén un diagrama de su flujo de llamadas. Útil para ese módulo de utilidades que creció durante tres años y ya nadie sabe qué mitad sigue viva.",
+ "shows": "Las declaraciones de función, las arrow functions y los métodos de clase se vuelven nodos, y los imports ES enlazan los archivos de donde vienen. Los callbacks pasados en línea se resuelven a la función que nombran, así que un handler conectado por un emisor de eventos también sale enlazado."
+ },
+ "typescript": {
+ "h1": "Generador de call graph de TypeScript",
+ "intro": "Mira la estructura de llamadas de cualquier archivo TypeScript sin montar un language server. Lo pegas, eliges TypeScript y lees el diagrama.",
+ "shows": "Weftmap parsea TypeScript con su propia gramática en lugar de quitar los tipos primero, así que genéricos, interfaces y anotaciones nunca rompen el parseo. Obtienes el mismo call graph que con el analizador de JavaScript, sobre código donde un parser de JS puro se atraganta."
+ },
+ "go": {
+ "h1": "Generador de call graph de Go",
+ "intro": "Pega código Go y mira qué funciones y métodos llaman a cuáles. Una forma rápida de medir un paquete que estás por tocar.",
+ "shows": "Go no tiene clases, así que el grafo sale más plano que el de Python o Java: funciones y métodos son nodos, las llamadas son flechas. Los receivers se mantienen junto a sus métodos, de modo que se ve de qué tipo cuelga cada uno. Las llamadas a la librería estándar se filtran."
+ },
+ "rust": {
+ "h1": "Generador de call graph de Rust",
+ "intro": "Sigue cómo se alcanzan entre sí las funciones de un módulo de Rust, sin esperar a un cargo check completo. Pegas un archivo y aparece el diagrama.",
+ "shows": "Las funciones libres y sus llamadas forman el grafo. Como el parseo es puramente sintáctico, el archivo no necesita compilar: puedes apuntar Weftmap a un módulo a medio refactor, con el borrow checker todavía protestando, y aun así obtener una imagen legible."
+ },
+ "java": {
+ "h1": "Generador de call graph de Java",
+ "intro": "Pega una clase de Java y obtén un diagrama de cómo se llaman sus métodos entre sí. Pensado para cuando abres un servicio ajeno y quieres el esquema antes que el detalle.",
+ "shows": "Los métodos se vuelven nodos y las clases los agrupan, así que el diagrama refleja la estructura que ya ves en el archivo. Las llamadas al JDK y a librerías se descartan, dejando solo los caminos que pertenecen al código que pegaste."
+ },
+ "sql": {
+ "h1": "Generador de diagramas ER para SQL",
+ "intro": "Pega DDL de SQL y obtén un diagrama entidad-relación. Sin cadena de conexión, sin cliente y sin acceso de lectura a nada: solo las sentencias CREATE TABLE.",
+ "shows": "Este es el único lenguaje donde Weftmap no dibuja un call graph. Las tablas se vuelven nodos con sus columnas, las claves primarias y foráneas quedan marcadas, y cada clave foránea se convierte en una flecha hacia la tabla que referencia, etiquetada con su cardinalidad."
+ }
+ }
}
}
diff --git a/src/i18n/dictionaries/fr.json b/src/i18n/dictionaries/fr.json
index 3c3aea3..f00ca66 100644
--- a/src/i18n/dictionaries/fr.json
+++ b/src/i18n/dictionaries/fr.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "Schéma SQL",
"showcaseSchemaDesc": "Les tables deviennent des entités et les clés étrangères des relations avec leur cardinalité.",
"languagesTitle": "Langages",
- "languagesSubtitle": "Six aujourd'hui — cinq produisent un graphe d'appels, SQL produit un schéma relationnel.",
+ "languagesSubtitle": "Sept aujourd'hui — six produisent un graphe d'appels, SQL produit un schéma relationnel.",
"languageRows": [
{
"name": "Python",
@@ -103,6 +103,11 @@
"kind": "Graphe d'appels",
"detail": "Fonctions et appels"
},
+ {
+ "name": "Java",
+ "kind": "Graphe d'appels",
+ "detail": "Méthodes et classes"
+ },
{
"name": "SQL",
"kind": "Schéma relationnel",
@@ -194,5 +199,125 @@
"testimonialAllRatings": "Toutes",
"testimonialLoadMore": "Charger plus",
"testimonialNoResults": "Aucune expérience ne correspond aux filtres."
+ },
+ "seo": {
+ "home": {
+ "title": "Graphe d'appels pour Python, Go, Rust et SQL",
+ "description": "Collez du code et obtenez un graphe d'appels interactif en quelques secondes. Python, JavaScript, TypeScript, Go, Rust, Java et SQL. Libre et gratuit."
+ },
+ "app": {
+ "title": "Collez du code, obtenez le graphe",
+ "description": "Collez un extrait ou envoyez un dossier, choisissez le langage et obtenez un graphe d'appels interactif, zoomable et exportable. Sans compte ni stockage."
+ },
+ "graphs": {
+ "title": "Mes graphes enregistrés",
+ "description": "Vos graphes d'appels et schémas enregistrés, prêts à rouvrir, partager ou exporter quand vous voulez. Connectez-vous avec GitHub pour les conserver."
+ },
+ "docs.introduction": {
+ "title": "Introduction",
+ "description": "Ce qu'est Weftmap, le problème qu'il résout, et comment un backend modulaire fondé sur Tree-sitter rend l'analyse multi-langage réellement praticable."
+ },
+ "docs.getting-started": {
+ "title": "Démarrer",
+ "description": "Utilisez Weftmap dans le navigateur ou en local : collez un extrait, choisissez le langage et lisez votre premier graphe d'appels en moins d'une minute."
+ },
+ "docs.reading-the-diagram": {
+ "title": "Lire le diagramme",
+ "description": "Comment lire un graphe Weftmap : le sens des nœuds et des flèches, pourquoi les appels aux bibliothèques sont filtrés, et comment dagre ordonne le tout."
+ },
+ "docs.languages": {
+ "title": "Langages pris en charge",
+ "description": "Tous les langages analysés par Weftmap aujourd'hui — Python, JavaScript, TypeScript, Go, Rust, Java et SQL — et ce que chaque analyseur extrait du code."
+ },
+ "docs.how-it-works": {
+ "title": "Comment ça marche",
+ "description": "Dans le pipeline : validation de l'entrée, analyse avec Tree-sitter WASM, parcours de l'arbre syntaxique et le graphe normalisé envoyé à React Flow."
+ },
+ "docs.api": {
+ "title": "Référence de l'API",
+ "description": "Le point d'entrée POST /api/analyze : forme de la requête, langages acceptés, le JSON du graphe renvoyé, limites de taille et erreurs possibles."
+ },
+ "docs.adding-a-language": {
+ "title": "Ajouter un langage",
+ "description": "Ajoutez un langage à Weftmap avec un LangSpec et deux requêtes Tree-sitter. Le guide complet, du WASM de la grammaire au graphe d'appels fonctionnel."
+ },
+ "docs.faq": {
+ "title": "Questions fréquentes",
+ "description": "Votre code quitte-t-il votre machine, quelles sont les limites de taille, quels langages sont pris en charge et sous quelle licence. Réponses courtes."
+ },
+ "landing.python": {
+ "title": "Graphe d'appels Python en ligne",
+ "description": "Collez du code Python et voyez quelle fonction appelle laquelle, sous forme de diagramme interactif. Gère classes, imports et héritage. Sans installation."
+ },
+ "landing.javascript": {
+ "title": "Graphe d'appels JavaScript",
+ "description": "Transformez du JavaScript en graphe d'appels interactif. Fonctions, fonctions fléchées, classes et imports ES, analysés à la volée. Gratuit à utiliser."
+ },
+ "landing.typescript": {
+ "title": "Graphe d'appels TypeScript",
+ "description": "Cartographiez le flux d'appels de n'importe quel fichier TypeScript. Utilise la grammaire typée : génériques et interfaces sont analysés correctement."
+ },
+ "landing.go": {
+ "title": "Graphe d'appels Go en ligne",
+ "description": "Collez du code Go et obtenez un diagramme des fonctions et méthodes qui s'appellent. Les appels à la bibliothèque standard sont filtrés pour la lisibilité."
+ },
+ "landing.rust": {
+ "title": "Graphe d'appels Rust en ligne",
+ "description": "Voyez comment vos fonctions Rust s'appellent entre elles dans un diagramme interactif. Collez un module ou un fichier entier et suivez le flux d'exécution."
+ },
+ "landing.java": {
+ "title": "Graphe d'appels Java en ligne",
+ "description": "Collez du code Java et obtenez un graphe d'appels interactif de vos méthodes et classes. Pratique pour parcourir un service inconnu avant de tout lire."
+ },
+ "landing.sql": {
+ "title": "Schéma relationnel depuis du SQL",
+ "description": "Collez du DDL SQL et obtenez un schéma entité-association : tables en nœuds, clés étrangères en arêtes. Lisez un schéma sans ouvrir la base de données."
+ }
+ },
+ "landing": {
+ "exampleHeading": "Exemple",
+ "showsHeading": "Ce que montre le diagramme",
+ "ctaTitle": "Essayez maintenant avec votre propre code",
+ "ctaDesc": "Collez un fichier, choisissez le langage, lancez l'analyse. Rien n'est conservé.",
+ "ctaButton": "Ouvrir l'éditeur",
+ "docsReadDiagram": "Comment lire le diagramme",
+ "docsLanguages": "Tous les langages pris en charge",
+ "pages": {
+ "python": {
+ "h1": "Générateur de graphe d'appels Python",
+ "intro": "Collez un fichier Python et Weftmap dessine quelle fonction appelle laquelle. Utile quand vous héritez d'un module sans docstrings et qu'il vous faut sa forme avant de le lire de bout en bout.",
+ "shows": "L'analyseur Python repère les fonctions, les classes, les imports et l'héritage : une sous-classe apparaît donc reliée à la base qu'elle étend. Les appels à la bibliothèque standard et aux paquets tiers sont écartés, car ils rempliraient le diagramme sans rien dire de votre propre code."
+ },
+ "javascript": {
+ "h1": "Générateur de graphe d'appels JavaScript",
+ "intro": "Déposez un fichier JavaScript et obtenez un diagramme de son flux d'appels. Pratique pour ce module utilitaire qui a grossi pendant trois ans et dont plus personne ne sait quelle moitié sert encore.",
+ "shows": "Déclarations de fonctions, fonctions fléchées et méthodes de classe deviennent des nœuds, et les imports ES relient les fichiers d'origine. Les callbacks passés en ligne sont résolus vers la fonction qu'ils nomment : un handler branché via un émetteur d'événements reste donc connecté."
+ },
+ "typescript": {
+ "h1": "Générateur de graphe d'appels TypeScript",
+ "intro": "Visualisez la structure d'appels de n'importe quel fichier TypeScript sans installer de language server. Vous collez, vous choisissez TypeScript, vous lisez le diagramme.",
+ "shows": "Weftmap analyse TypeScript avec sa grammaire dédiée plutôt qu'en retirant d'abord les types : génériques, interfaces et annotations ne cassent jamais l'analyse. Vous obtenez le même graphe qu'avec l'analyseur JavaScript, sur du code où un parseur JS classique échoue."
+ },
+ "go": {
+ "h1": "Générateur de graphe d'appels Go",
+ "intro": "Collez du code Go et voyez quelles fonctions et méthodes en appellent d'autres. Une manière rapide de jauger un paquet que vous vous apprêtez à modifier.",
+ "shows": "Go n'a pas de classes : le graphe est donc plus plat que son équivalent Python ou Java, avec les fonctions et méthodes en nœuds et les appels en flèches. Les receivers restent attachés à leurs méthodes, ce qui garde visible le type auquel chacune se rattache. Les appels à la bibliothèque standard sont filtrés."
+ },
+ "rust": {
+ "h1": "Générateur de graphe d'appels Rust",
+ "intro": "Suivez comment les fonctions d'un module Rust s'atteignent, sans attendre un cargo check complet. Vous collez un fichier et le diagramme apparaît.",
+ "shows": "Les fonctions libres et leurs appels forment le graphe. Comme l'analyse est purement syntaxique, le fichier n'a pas besoin de compiler : vous pouvez pointer Weftmap sur un module en plein refactoring, alors que le borrow checker râle encore, et obtenir tout de même une image lisible."
+ },
+ "java": {
+ "h1": "Générateur de graphe d'appels Java",
+ "intro": "Collez une classe Java et obtenez un diagramme de la façon dont ses méthodes s'appellent. Conçu pour le moment où vous ouvrez un service inconnu et voulez la trame avant le détail.",
+ "shows": "Les méthodes deviennent des nœuds et les classes les regroupent : le diagramme reflète la structure que vous voyez déjà dans le fichier. Les appels au JDK et aux bibliothèques sont écartés, ne laissant que les chemins appartenant au code collé."
+ },
+ "sql": {
+ "h1": "Générateur de schéma relationnel depuis du SQL",
+ "intro": "Collez du DDL SQL et obtenez un schéma entité-association. Pas de chaîne de connexion, pas de client, aucun accès en lecture : seulement les instructions CREATE TABLE.",
+ "shows": "C'est le seul langage pour lequel Weftmap ne dessine pas de graphe d'appels. Les tables deviennent des nœuds portant leurs colonnes, les clés primaires et étrangères sont signalées, et chaque clé étrangère devient une flèche vers la table référencée, étiquetée avec sa cardinalité."
+ }
+ }
}
}
diff --git a/src/i18n/dictionaries/it.json b/src/i18n/dictionaries/it.json
index 746383f..9817928 100644
--- a/src/i18n/dictionaries/it.json
+++ b/src/i18n/dictionaries/it.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "Schema SQL",
"showcaseSchemaDesc": "Le tabelle diventano entità e le chiavi esterne diventano relazioni con cardinalità.",
"languagesTitle": "Linguaggi",
- "languagesSubtitle": "Sei oggi — cinque producono un grafo delle chiamate, SQL produce un diagramma ER.",
+ "languagesSubtitle": "Sette oggi — sei producono un grafo delle chiamate, SQL produce un diagramma ER.",
"languageRows": [
{
"name": "Python",
@@ -103,6 +103,11 @@
"kind": "Grafo delle chiamate",
"detail": "Funzioni e chiamate"
},
+ {
+ "name": "Java",
+ "kind": "Grafo delle chiamate",
+ "detail": "Metodi e classi"
+ },
{
"name": "SQL",
"kind": "Diagramma ER",
@@ -194,5 +199,125 @@
"testimonialAllRatings": "Tutte",
"testimonialLoadMore": "Carica altro",
"testimonialNoResults": "Nessuna esperienza corrisponde ai filtri."
+ },
+ "seo": {
+ "home": {
+ "title": "Grafo delle chiamate per Python, Go, Rust e SQL",
+ "description": "Incolla il codice e ottieni un grafo delle chiamate interattivo in pochi secondi. Python, JavaScript, TypeScript, Go, Rust, Java e SQL. Gratis e open source."
+ },
+ "app": {
+ "title": "Incolla il codice, ottieni il grafo",
+ "description": "Incolla uno snippet o carica una cartella, scegli il linguaggio e ottieni un grafo interattivo con zoom ed esportazione. Nessuna registrazione richiesta."
+ },
+ "graphs": {
+ "title": "I miei grafi salvati",
+ "description": "I tuoi grafi delle chiamate e diagrammi di schema salvati, pronti da riaprire, condividere o esportare quando vuoi. Accedi con GitHub per conservarli."
+ },
+ "docs.introduction": {
+ "title": "Introduzione",
+ "description": "Cos'è Weftmap, quale problema risolve e come un backend modulare basato su Tree-sitter rende praticabile l'analisi su più linguaggi contemporaneamente."
+ },
+ "docs.getting-started": {
+ "title": "Iniziare",
+ "description": "Usa Weftmap nel browser o in locale: incolla uno snippet, scegli il linguaggio e leggi il tuo primo grafo delle chiamate in meno di un minuto. Passo passo."
+ },
+ "docs.reading-the-diagram": {
+ "title": "Leggere il diagramma",
+ "description": "Come leggere un grafo di Weftmap: cosa significano nodi e frecce, perché le chiamate alle librerie vengono filtrate e come dagre dispone le funzioni."
+ },
+ "docs.languages": {
+ "title": "Linguaggi supportati",
+ "description": "Tutti i linguaggi che Weftmap analizza oggi — Python, JavaScript, TypeScript, Go, Rust, Java e SQL — e cosa estrae dal tuo codice ciascun analizzatore."
+ },
+ "docs.how-it-works": {
+ "title": "Come funziona",
+ "description": "Dentro la pipeline: validazione dell'input, parsing con Tree-sitter WASM, visita dell'albero sintattico e il grafo normalizzato inviato a React Flow."
+ },
+ "docs.api": {
+ "title": "Riferimento API",
+ "description": "L'endpoint POST /api/analyze: forma della richiesta, linguaggi accettati, il JSON del grafo restituito, limiti di dimensione ed errori possibili."
+ },
+ "docs.adding-a-language": {
+ "title": "Aggiungere un linguaggio",
+ "description": "Aggiungi un linguaggio a Weftmap con un LangSpec e due query Tree-sitter. La guida completa, dal WASM della grammatica al grafo delle chiamate finito."
+ },
+ "docs.faq": {
+ "title": "Domande frequenti",
+ "description": "Se il tuo codice lascia la tua macchina, quali sono i limiti di dimensione, quali linguaggi sono supportati e con quale licenza. Risposte brevi e chiare."
+ },
+ "landing.python": {
+ "title": "Grafo delle chiamate per Python",
+ "description": "Incolla codice Python e vedi quale funzione chiama quale, come diagramma interattivo. Gestisce classi, import ed ereditarietà. Gratis, senza installare."
+ },
+ "landing.javascript": {
+ "title": "Grafo delle chiamate JavaScript",
+ "description": "Trasforma JavaScript in un grafo delle chiamate interattivo. Funzioni, arrow function, classi e import ES, analizzati al volo. Gratuito da usare subito."
+ },
+ "landing.typescript": {
+ "title": "Grafo delle chiamate TypeScript",
+ "description": "Mappa il flusso di chiamate di qualsiasi file TypeScript in un diagramma interattivo. Usa la grammatica tipizzata: generici e interfacce si leggono bene."
+ },
+ "landing.go": {
+ "title": "Grafo delle chiamate per Go",
+ "description": "Incolla codice Go e ottieni un diagramma di quali funzioni e metodi chiamano quali. Le chiamate alla libreria standard sono filtrate per leggibilità."
+ },
+ "landing.rust": {
+ "title": "Grafo delle chiamate per Rust",
+ "description": "Guarda come le tue funzioni Rust si chiamano a vicenda in un diagramma interattivo. Incolla un modulo o un file intero e segui il flusso nel browser."
+ },
+ "landing.java": {
+ "title": "Grafo delle chiamate per Java",
+ "description": "Incolla codice Java e ottieni un grafo interattivo dei tuoi metodi e classi. Utile per esaminare un servizio sconosciuto prima di leggerlo per intero."
+ },
+ "landing.sql": {
+ "title": "Diagramma ER da codice SQL",
+ "description": "Incolla DDL SQL e ottieni un diagramma entità-relazione: tabelle come nodi, chiavi esterne come archi. Leggi uno schema senza aprire il database."
+ }
+ },
+ "landing": {
+ "exampleHeading": "Esempio",
+ "showsHeading": "Cosa mostra il diagramma",
+ "ctaTitle": "Ora provalo con il tuo codice",
+ "ctaDesc": "Incolla un file, scegli il linguaggio e avvia l'analisi. Non viene salvato nulla.",
+ "ctaButton": "Apri l'editor",
+ "docsReadDiagram": "Come leggere il diagramma",
+ "docsLanguages": "Tutti i linguaggi supportati",
+ "pages": {
+ "python": {
+ "h1": "Generatore di grafo delle chiamate Python",
+ "intro": "Incolla un file Python e Weftmap disegna quale funzione chiama quale. Serve quando erediti un modulo senza docstring e ti serve vederne la forma prima di leggerlo da cima a fondo.",
+ "shows": "L'analizzatore Python riconosce funzioni, classi, import ed ereditarietà, quindi una sottoclasse appare collegata alla base che estende. Le chiamate alla libreria standard e ai pacchetti di terze parti restano fuori: riempirebbero il diagramma senza dirti nulla sul tuo codice."
+ },
+ "javascript": {
+ "h1": "Generatore di grafo delle chiamate JavaScript",
+ "intro": "Butta dentro un file JavaScript e ottieni un diagramma del suo flusso di chiamate. Comodo per quel modulo di utility cresciuto per tre anni, di cui nessuno sa più quale metà sia ancora viva.",
+ "shows": "Dichiarazioni di funzione, arrow function e metodi di classe diventano nodi, e gli import ES collegano i file da cui provengono. I callback passati inline vengono risolti alla funzione che nominano, così un handler agganciato tramite un event emitter resta comunque collegato."
+ },
+ "typescript": {
+ "h1": "Generatore di grafo delle chiamate TypeScript",
+ "intro": "Guarda la struttura delle chiamate di qualsiasi file TypeScript senza configurare un language server. Incolli, scegli TypeScript e leggi il diagramma.",
+ "shows": "Weftmap analizza TypeScript con la sua grammatica dedicata invece di rimuovere prima i tipi, così generici, interfacce e annotazioni non mandano mai in crisi il parsing. Ottieni lo stesso grafo dell'analizzatore JavaScript, su codice dove un parser JS puro si blocca."
+ },
+ "go": {
+ "h1": "Generatore di grafo delle chiamate Go",
+ "intro": "Incolla codice Go e guarda quali funzioni e metodi ne chiamano altri. Un modo rapido per farsi un'idea di un pacchetto che stai per toccare.",
+ "shows": "Go non ha classi, quindi il grafo è più piatto di quello Python o Java: funzioni e metodi sono nodi, le chiamate sono frecce. I receiver restano accanto ai loro metodi, così si vede a quale tipo appartiene ciascuno. Le chiamate alla libreria standard vengono filtrate."
+ },
+ "rust": {
+ "h1": "Generatore di grafo delle chiamate Rust",
+ "intro": "Segui come si raggiungono tra loro le funzioni di un modulo Rust, senza aspettare un cargo check completo. Incolli un file e il diagramma compare.",
+ "shows": "Le funzioni libere e le loro chiamate formano il grafo. Poiché l'analisi è puramente sintattica, il file non deve compilare: puoi puntare Weftmap su un modulo a metà refactoring, con il borrow checker ancora scontento, e ottenere comunque un quadro leggibile."
+ },
+ "java": {
+ "h1": "Generatore di grafo delle chiamate Java",
+ "intro": "Incolla una classe Java e ottieni un diagramma di come i suoi metodi si chiamano a vicenda. Pensato per quando apri un servizio sconosciuto e vuoi lo schema prima del dettaglio.",
+ "shows": "I metodi diventano nodi e le classi li raggruppano, così il diagramma riflette la struttura che già vedi nel file. Le chiamate al JDK e alle librerie vengono scartate, lasciando solo i percorsi che appartengono al codice incollato."
+ },
+ "sql": {
+ "h1": "Generatore di diagramma ER da SQL",
+ "intro": "Incolla DDL SQL e ottieni un diagramma entità-relazione. Nessuna stringa di connessione, nessun client, nessun accesso in lettura: solo le istruzioni CREATE TABLE.",
+ "shows": "Questo è l'unico linguaggio in cui Weftmap non disegna un grafo delle chiamate. Le tabelle diventano nodi con le loro colonne, chiavi primarie ed esterne vengono marcate, e ogni chiave esterna diventa una freccia verso la tabella referenziata, etichettata con la sua cardinalità."
+ }
+ }
}
}
diff --git a/src/i18n/dictionaries/pt.json b/src/i18n/dictionaries/pt.json
index 7afedfa..8745f87 100644
--- a/src/i18n/dictionaries/pt.json
+++ b/src/i18n/dictionaries/pt.json
@@ -76,7 +76,7 @@
"showcaseSchemaTitle": "Esquema SQL",
"showcaseSchemaDesc": "Tabelas se tornam entidades e chaves estrangeiras se tornam relacionamentos com cardinalidade.",
"languagesTitle": "Idiomas",
- "languagesSubtitle": "Seis hoje — cinco produzem grafo de chamadas, SQL produz diagrama ER.",
+ "languagesSubtitle": "Sete hoje — seis produzem grafo de chamadas, SQL produz diagrama ER.",
"languageRows": [
{
"name": "Python",
@@ -103,6 +103,11 @@
"kind": "Grafo de chamadas",
"detail": "Funções e chamadas"
},
+ {
+ "name": "Java",
+ "kind": "Grafo de chamadas",
+ "detail": "Métodos e classes"
+ },
{
"name": "SQL",
"kind": "Diagrama ER",
@@ -194,5 +199,125 @@
"testimonialAllRatings": "Todas",
"testimonialLoadMore": "Carregar mais",
"testimonialNoResults": "Nenhuma experiência corresponde aos filtros."
+ },
+ "seo": {
+ "home": {
+ "title": "Grafo de chamadas: Python, Go, Rust e SQL",
+ "description": "Cole o código e obtenha um grafo de chamadas interativo em segundos. Python, JavaScript, TypeScript, Go, Rust, Java e SQL. Grátis e open source."
+ },
+ "app": {
+ "title": "Cole o código, obtenha o grafo",
+ "description": "Cole um trecho ou envie uma pasta, escolha a linguagem e obtenha um grafo de chamadas interativo com zoom e exportação. Sem cadastro, nada é salvo."
+ },
+ "graphs": {
+ "title": "Meus grafos salvos",
+ "description": "Seus grafos de chamadas e diagramas de esquema salvos, prontos para reabrir, compartilhar ou exportar quando quiser. Entre com o GitHub para guardá-los."
+ },
+ "docs.introduction": {
+ "title": "Introdução",
+ "description": "O que é o Weftmap, qual problema resolve e como um backend plugável sobre Tree-sitter torna viável a análise de grafos em várias linguagens de uma vez."
+ },
+ "docs.getting-started": {
+ "title": "Começar",
+ "description": "Use o Weftmap no navegador ou localmente: cole um trecho, escolha a linguagem e leia seu primeiro grafo de chamadas em menos de um minuto. Passo a passo."
+ },
+ "docs.reading-the-diagram": {
+ "title": "Ler o diagrama",
+ "description": "Como ler um grafo do Weftmap: o que significam nós e setas, por que chamadas a bibliotecas são filtradas e como o layout do dagre ordena as funções."
+ },
+ "docs.languages": {
+ "title": "Linguagens suportadas",
+ "description": "Todas as linguagens que o Weftmap analisa hoje — Python, JavaScript, TypeScript, Go, Rust, Java e SQL — e o que cada analisador extrai do seu código."
+ },
+ "docs.how-it-works": {
+ "title": "Como funciona",
+ "description": "Dentro do pipeline: validação de entrada, parsing com Tree-sitter WASM, percurso da árvore sintática e o grafo normalizado que chega ao React Flow."
+ },
+ "docs.api": {
+ "title": "Referência da API",
+ "description": "O endpoint POST /api/analyze: formato da requisição, linguagens aceitas, o JSON do grafo devolvido, limites de tamanho e os erros que podem ocorrer."
+ },
+ "docs.adding-a-language": {
+ "title": "Adicionar uma linguagem",
+ "description": "Adicione uma linguagem ao Weftmap com um LangSpec e duas consultas Tree-sitter. O guia completo, do WASM da gramática ao grafo de chamadas funcionando."
+ },
+ "docs.faq": {
+ "title": "Perguntas frequentes",
+ "description": "Se o seu código sai da sua máquina, quais são os limites de tamanho, quais linguagens são suportadas e sob qual licença o Weftmap está. Em resumo."
+ },
+ "landing.python": {
+ "title": "Gerador de grafo de chamadas Python",
+ "description": "Cole código Python e veja qual função chama qual, como diagrama interativo. Entende classes, imports e herança. Grátis e sem instalar nada no seu PC."
+ },
+ "landing.javascript": {
+ "title": "Grafo de chamadas para JavaScript",
+ "description": "Transforme JavaScript em um grafo de chamadas interativo. Funções, arrow functions, classes e imports ES, tudo analisado na hora. Grátis para usar."
+ },
+ "landing.typescript": {
+ "title": "Grafo de chamadas para TypeScript",
+ "description": "Mapeie o fluxo de chamadas de qualquer arquivo TypeScript em um diagrama interativo. Usa a gramática tipada, então genéricos e interfaces são lidos bem."
+ },
+ "landing.go": {
+ "title": "Gerador de grafo de chamadas Go",
+ "description": "Cole código Go e obtenha um diagrama de quais funções e métodos chamam quais. Chamadas à biblioteca padrão são filtradas para o grafo ficar legível."
+ },
+ "landing.rust": {
+ "title": "Gerador de grafo de chamadas Rust",
+ "description": "Veja como suas funções Rust se chamam entre si em um diagrama interativo. Cole um módulo ou um arquivo inteiro e siga o fluxo sem sair do navegador."
+ },
+ "landing.java": {
+ "title": "Gerador de grafo de chamadas Java",
+ "description": "Cole código Java e obtenha um grafo de chamadas interativo dos seus métodos e classes. Útil para revisar serviços alheios antes de lê-los por inteiro."
+ },
+ "landing.sql": {
+ "title": "Gerador de diagrama ER para SQL",
+ "description": "Cole DDL de SQL e obtenha um diagrama entidade-relacionamento: tabelas como nós, chaves estrangeiras como arestas. Leia um esquema sem abrir o banco."
+ }
+ },
+ "landing": {
+ "exampleHeading": "Exemplo",
+ "showsHeading": "O que o diagrama mostra",
+ "ctaTitle": "Agora teste com o seu próprio código",
+ "ctaDesc": "Cole um arquivo, escolha a linguagem e clique em Analisar. Nada é salvo.",
+ "ctaButton": "Abrir o editor",
+ "docsReadDiagram": "Como ler o diagrama",
+ "docsLanguages": "Todas as linguagens suportadas",
+ "pages": {
+ "python": {
+ "h1": "Gerador de grafo de chamadas Python",
+ "intro": "Cole um arquivo Python e o Weftmap desenha qual função chama qual. Serve quando você herda um módulo sem docstrings e precisa ver o formato dele antes de ler de cima a baixo.",
+ "shows": "O analisador de Python reconhece funções, classes, imports e herança, então uma subclasse aparece ligada à base que estende. Chamadas à biblioteca padrão e a pacotes de terceiros ficam de fora: encheriam o diagrama sem dizer nada sobre o seu próprio código."
+ },
+ "javascript": {
+ "h1": "Gerador de grafo de chamadas JavaScript",
+ "intro": "Solte um arquivo JavaScript e receba um diagrama do seu fluxo de chamadas. Útil para aquele módulo de utilidades que cresceu por três anos e ninguém mais sabe qual metade ainda é usada.",
+ "shows": "Declarações de função, arrow functions e métodos de classe viram nós, e os imports ES ligam os arquivos de onde vêm. Callbacks passados em linha são resolvidos para a função que nomeiam, então um handler ligado por um emissor de eventos também aparece conectado."
+ },
+ "typescript": {
+ "h1": "Gerador de grafo de chamadas TypeScript",
+ "intro": "Veja a estrutura de chamadas de qualquer arquivo TypeScript sem montar um language server. Cole, escolha TypeScript e leia o diagrama.",
+ "shows": "O Weftmap analisa TypeScript com a gramática própria da linguagem em vez de remover os tipos antes, então genéricos, interfaces e anotações nunca quebram a análise. O resultado é o mesmo grafo do analisador de JavaScript, sobre código em que um parser de JS puro engasga."
+ },
+ "go": {
+ "h1": "Gerador de grafo de chamadas Go",
+ "intro": "Cole código Go e veja quais funções e métodos chamam quais. Um jeito rápido de medir um pacote que você está prestes a mexer.",
+ "shows": "Go não tem classes, então o grafo sai mais plano que o de Python ou Java: funções e métodos são nós, chamadas são setas. Os receivers ficam junto dos seus métodos, deixando claro de que tipo cada um pende. Chamadas à biblioteca padrão são filtradas."
+ },
+ "rust": {
+ "h1": "Gerador de grafo de chamadas Rust",
+ "intro": "Acompanhe como as funções de um módulo Rust se alcançam, sem esperar por um cargo check completo. Você cola um arquivo e o diagrama aparece.",
+ "shows": "Funções livres e suas chamadas formam o grafo. Como a análise é puramente sintática, o arquivo não precisa compilar: dá para apontar o Weftmap para um módulo no meio de um refactor, com o borrow checker ainda reclamando, e mesmo assim obter uma imagem legível."
+ },
+ "java": {
+ "h1": "Gerador de grafo de chamadas Java",
+ "intro": "Cole uma classe Java e receba um diagrama de como seus métodos chamam uns aos outros. Feito para o momento em que você abre um serviço desconhecido e quer o esboço antes do detalhe.",
+ "shows": "Métodos viram nós e classes os agrupam, então o diagrama reflete a estrutura que você já vê no arquivo. Chamadas ao JDK e a bibliotecas são descartadas, restando apenas os caminhos que pertencem ao código que você colou."
+ },
+ "sql": {
+ "h1": "Gerador de diagrama ER para SQL",
+ "intro": "Cole DDL de SQL e receba um diagrama entidade-relacionamento. Sem string de conexão, sem cliente e sem acesso de leitura a nada: apenas os comandos CREATE TABLE.",
+ "shows": "Esta é a única linguagem em que o Weftmap não desenha um grafo de chamadas. Tabelas viram nós com suas colunas, chaves primárias e estrangeiras são marcadas, e cada chave estrangeira vira uma seta para a tabela que referencia, rotulada com sua cardinalidade."
+ }
+ }
}
}
diff --git a/src/lib/landing-languages.test.ts b/src/lib/landing-languages.test.ts
new file mode 100644
index 0000000..ad10f4f
--- /dev/null
+++ b/src/lib/landing-languages.test.ts
@@ -0,0 +1,104 @@
+import { describe, expect, test } from "vitest";
+import { locales } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { SUPPORTED_LANGUAGES, getAnalyzer } from "./analysis/registry";
+import type { SourceFile } from "./analysis/types";
+import { getSeoEntry } from "./seo";
+import {
+ EXCLUDED_FROM_LANDING,
+ LANDING_IDS,
+ LANDING_LANGUAGES,
+} from "./landing-languages";
+
+describe("landing language registry stays in sync", () => {
+ // The repo's stated differentiator is that adding a language takes a few
+ // lines. Without this, the eighth language ships with no landing page and
+ // nobody notices for months.
+ test.each(SUPPORTED_LANGUAGES)(
+ "supported language '%s' has a landing page or an explicit exclusion",
+ (language) => {
+ const covered =
+ (LANDING_IDS as string[]).includes(language) ||
+ EXCLUDED_FROM_LANDING.includes(language);
+ expect(covered, `add '${language}' to LANDING_LANGUAGES or EXCLUDED_FROM_LANDING`).toBe(
+ true,
+ );
+ },
+ );
+
+ test("every landing id maps to a real analyzer", () => {
+ for (const id of LANDING_IDS) {
+ expect(getAnalyzer(id), id).toBeDefined();
+ }
+ });
+
+ test("ids are unique", () => {
+ expect(new Set(LANDING_IDS).size).toBe(LANDING_IDS.length);
+ });
+});
+
+describe("landing snippets produce a non-trivial graph", () => {
+ // The tripwire against placeholder content: a landing page whose example
+ // doesn't actually analyze is thin programmatic content, which is exactly
+ // what Google's scaled-content policy targets.
+ test.each(LANDING_LANGUAGES)("$name snippet analyzes", async ({ id, extension, snippet }) => {
+ const analyzer = getAnalyzer(id);
+ expect(analyzer, id).toBeDefined();
+
+ const files: SourceFile[] = [{ path: `example.${extension}`, content: snippet }];
+ const graph = await analyzer!.analyzeProject(files);
+
+ expect(graph.nodes.length, `${id} nodes`).toBeGreaterThanOrEqual(2);
+ expect(graph.edges.length, `${id} edges`).toBeGreaterThanOrEqual(1);
+ });
+
+ test("SQL produces table nodes, not functions", async () => {
+ const sql = LANDING_LANGUAGES.find((l) => l.id === "sql")!;
+ const graph = await getAnalyzer("sql")!.analyzeProject([
+ { path: "schema.sql", content: sql.snippet },
+ ]);
+ expect(graph.nodes.some((n) => n.type === "table")).toBe(true);
+ expect(graph.edges.some((e) => e.kind === "references")).toBe(true);
+ });
+});
+
+describe.each(locales)("landing copy for locale '%s' is not boilerplate", (lang) => {
+ const pages = getDictionary(lang).landing.pages;
+ const ids = LANDING_IDS;
+
+ function fieldsOf(field: "h1" | "intro" | "shows") {
+ return ids.map((id) => pages[id][field]);
+ }
+
+ test.each(["h1", "intro", "shows"] as const)(
+ "every %s is unique within the locale",
+ (field) => {
+ const values = fieldsOf(field);
+ expect(new Set(values).size, `duplicate ${field}`).toBe(values.length);
+ },
+ );
+
+ test("titles and descriptions are unique within the locale", () => {
+ const titles = ids.map((id) => getSeoEntry(lang, `landing.${id}`).title);
+ const descriptions = ids.map(
+ (id) => getSeoEntry(lang, `landing.${id}`).description,
+ );
+ expect(new Set(titles).size).toBe(titles.length);
+ expect(new Set(descriptions).size).toBe(descriptions.length);
+ });
+
+ test("copy differs by more than a substituted language name", () => {
+ // Blank out each language's own name, then compare. If two pages are the
+ // same sentence with the name swapped, they collide here.
+ const skeletons = LANDING_LANGUAGES.map(({ id, name }) =>
+ pages[id].shows.replaceAll(name, "@").toLowerCase(),
+ );
+ expect(new Set(skeletons).size).toBe(skeletons.length);
+ });
+
+ test("every h1 names its language", () => {
+ for (const { id, name } of LANDING_LANGUAGES) {
+ expect(pages[id].h1, id).toContain(name);
+ }
+ });
+});
diff --git a/src/lib/landing-languages.ts b/src/lib/landing-languages.ts
new file mode 100644
index 0000000..6db3dba
--- /dev/null
+++ b/src/lib/landing-languages.ts
@@ -0,0 +1,226 @@
+/**
+ * The per-language landing pages at /[lang]/call-graph/[language].
+ *
+ * Intentionally free of any import from `@/lib/analysis` — that module tree
+ * pulls in the Tree-sitter runtime, and these pages only need names and a
+ * snippet. `landing-languages.test.ts` imports the real registry and asserts
+ * this list stays in sync, so a newly supported language cannot quietly ship
+ * without a landing page.
+ */
+
+// A literal union rather than `string`, so `` `landing.${id}` `` narrows to a
+// real dictionary key and a typo becomes a compile error instead of an
+// undefined title at runtime.
+export type LandingId =
+ | "python"
+ | "javascript"
+ | "typescript"
+ | "go"
+ | "rust"
+ | "java"
+ | "sql";
+
+export type LandingLanguage = {
+ /** Must match the analyzer's `language` value. */
+ id: LandingId;
+ /** Display name, used in headings and the graph legend. */
+ name: string;
+ /** Which diagram the analyzer produces for this language. */
+ diagram: "call-graph" | "er";
+ /** File extension shown on the example, purely cosmetic. */
+ extension: string;
+ /** A real snippet this language's analyzer turns into a non-trivial graph. */
+ snippet: string;
+};
+
+// Languages the registry supports that deliberately get no landing page.
+// Empty today; kept so the sync test has somewhere to point.
+export const EXCLUDED_FROM_LANDING: string[] = [];
+
+export const LANDING_LANGUAGES: LandingLanguage[] = [
+ {
+ id: "python",
+ name: "Python",
+ diagram: "call-graph",
+ extension: "py",
+ snippet: `def main():
+ user = fetch_user(1)
+ print(render(user))
+
+
+def fetch_user(user_id):
+ row = query_db(user_id)
+ return to_dict(row)
+
+
+def query_db(user_id):
+ return (user_id, "Ada Lovelace")
+
+
+def to_dict(row):
+ return {"id": row[0], "name": row[1]}
+
+
+def render(user):
+ return f"{user['id']}: {user['name']}"
+`,
+ },
+ {
+ id: "javascript",
+ name: "JavaScript",
+ diagram: "call-graph",
+ extension: "js",
+ snippet: `function loadOrders(userId) {
+ const raw = fetchOrders(userId);
+ return raw.map(normalize);
+}
+
+function fetchOrders(userId) {
+ return [{ id: 1, userId, items: [{ price: 12 }] }];
+}
+
+function normalize(order) {
+ return { id: order.id, total: computeTotal(order) };
+}
+
+function computeTotal(order) {
+ return order.items.reduce((sum, item) => sum + item.price, 0);
+}
+`,
+ },
+ {
+ id: "typescript",
+ name: "TypeScript",
+ diagram: "call-graph",
+ extension: "ts",
+ snippet: `type Order = { id: number; items: { price: number }[] };
+
+function loadOrders(userId: number): Summary[] {
+ return fetchOrders(userId).map(normalize);
+}
+
+function fetchOrders(userId: number): Order[] {
+ return [{ id: userId, items: [{ price: 12 }] }];
+}
+
+type Summary = { id: number; total: number };
+
+function normalize(order: Order): Summary {
+ return { id: order.id, total: computeTotal(order) };
+}
+
+function computeTotal(order: Order): number {
+ return order.items.reduce((sum, item) => sum + item.price, 0);
+}
+`,
+ },
+ {
+ id: "go",
+ name: "Go",
+ diagram: "call-graph",
+ extension: "go",
+ snippet: `package billing
+
+func Run() int {
+ total := SumInvoices(fetchInvoices())
+ report(total)
+ return total
+}
+
+func fetchInvoices() []int {
+ return []int{1000, 2500, 400}
+}
+
+func SumInvoices(amounts []int) int {
+ total := 0
+ for _, amount := range amounts {
+ total += amount
+ }
+ return total
+}
+
+func report(total int) {
+ println(total)
+}
+`,
+ },
+ {
+ id: "rust",
+ name: "Rust",
+ diagram: "call-graph",
+ extension: "rs",
+ snippet: `fn run() -> i32 {
+ let total = sum_invoices(fetch_invoices());
+ report(total);
+ total
+}
+
+fn fetch_invoices() -> Vec {
+ vec![1000, 2500, 400]
+}
+
+fn sum_invoices(amounts: Vec) -> i32 {
+ amounts.iter().sum()
+}
+
+fn report(total: i32) {
+ println!("total: {total}");
+}
+`,
+ },
+ {
+ id: "java",
+ name: "Java",
+ diagram: "call-graph",
+ extension: "java",
+ snippet: `class InvoiceService {
+ int totalFor(String customer) {
+ return sum(load(customer));
+ }
+
+ int[] load(String customer) {
+ return new int[] { 1000, 2500, 400 };
+ }
+
+ int sum(int[] amounts) {
+ int total = 0;
+ for (int amount : amounts) {
+ total += amount;
+ }
+ return total;
+ }
+}
+`,
+ },
+ {
+ id: "sql",
+ name: "SQL",
+ diagram: "er",
+ extension: "sql",
+ snippet: `CREATE TABLE authors (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL
+);
+
+CREATE TABLE books (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ author_id INTEGER NOT NULL REFERENCES authors(id)
+);
+
+CREATE TABLE reviews (
+ id INTEGER PRIMARY KEY,
+ book_id INTEGER NOT NULL REFERENCES books(id),
+ rating INTEGER NOT NULL
+);
+`,
+ },
+];
+
+export const LANDING_IDS = LANDING_LANGUAGES.map((l) => l.id);
+
+export const LANDING_NAMES = LANDING_LANGUAGES.map((l) => l.name);
+
+export function getLandingLanguage(id: string): LandingLanguage | undefined {
+ return LANDING_LANGUAGES.find((l) => l.id === id);
+}
diff --git a/src/lib/seo-lengths.test.ts b/src/lib/seo-lengths.test.ts
new file mode 100644
index 0000000..837f8f4
--- /dev/null
+++ b/src/lib/seo-lengths.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, test } from "vitest";
+import { locales } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { TITLE_SUFFIX, getSeoEntry, type SeoKey } from "./seo";
+
+// Google truncates around these points in a search result. With 6 locales and
+// ~18 routes there are over 200 strings to keep inside them, and translations
+// run 15-20% longer than their English source — far past eyeballing.
+const TITLE_MAX = 60;
+const DESCRIPTION_MIN = 120;
+const DESCRIPTION_MAX = 160;
+
+const seoKeys = Object.keys(getDictionary("en").seo) as SeoKey[];
+
+describe("SEO copy fits search-result limits", () => {
+ test("there is at least one key to check", () => {
+ expect(seoKeys.length).toBeGreaterThan(0);
+ });
+
+ for (const locale of locales) {
+ for (const key of seoKeys) {
+ const label = `${locale}/${key}`;
+
+ test(`${label} title fits within ${TITLE_MAX} chars with the brand suffix`, () => {
+ const composed = getSeoEntry(locale, key).title + TITLE_SUFFIX;
+ expect(composed.length, composed).toBeLessThanOrEqual(TITLE_MAX);
+ });
+
+ test(`${label} description is ${DESCRIPTION_MIN}-${DESCRIPTION_MAX} chars`, () => {
+ const { description } = getSeoEntry(locale, key);
+ expect(description.length, description).toBeGreaterThanOrEqual(
+ DESCRIPTION_MIN,
+ );
+ expect(description.length, description).toBeLessThanOrEqual(
+ DESCRIPTION_MAX,
+ );
+ });
+
+ test(`${label} title and description are non-empty and untrimmed`, () => {
+ const { title, description } = getSeoEntry(locale, key);
+ expect(title).toBe(title.trim());
+ expect(description).toBe(description.trim());
+ expect(title.length).toBeGreaterThan(0);
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/src/lib/seo.test.ts b/src/lib/seo.test.ts
new file mode 100644
index 0000000..ecfb00d
--- /dev/null
+++ b/src/lib/seo.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, test } from "vitest";
+import { locales } from "@/i18n/config";
+import { SITE_URL, getAlternates } from "./seo";
+
+describe("SITE_URL", () => {
+ test("has no trailing slash", () => {
+ expect(SITE_URL).not.toMatch(/\/$/);
+ });
+
+ test("is the origin verified in Search Console", () => {
+ expect(SITE_URL).toBe("https://weftmap.vercel.app");
+ });
+});
+
+describe("getAlternates", () => {
+ const paths = ["", "app", "docs", "docs/languages"];
+
+ test.each(paths)("path '%s' yields an entry for every locale", (path) => {
+ const { languages } = getAlternates(path, "en");
+ for (const locale of locales) {
+ expect(languages[locale]).toBeDefined();
+ }
+ });
+
+ test.each(paths)("path '%s' declares x-default pointing at English", (path) => {
+ const { languages } = getAlternates(path, "es");
+ expect(languages["x-default"]).toBe(languages.en);
+ });
+
+ test.each(paths)("path '%s' produces absolute, well-formed URLs", (path) => {
+ const { canonical, languages } = getAlternates(path, "es");
+ for (const url of [canonical, ...Object.values(languages)]) {
+ expect(url.startsWith(SITE_URL)).toBe(true);
+ // No double slash after the scheme.
+ expect(url.slice("https://".length)).not.toContain("//");
+ expect(() => new URL(url)).not.toThrow();
+ }
+ });
+
+ test("canonical reflects the current locale", () => {
+ expect(getAlternates("docs/faq", "pt").canonical).toBe(
+ `${SITE_URL}/pt/docs/faq`,
+ );
+ });
+
+ test("empty path yields the locale homepage without a trailing slash", () => {
+ expect(getAlternates("", "fr").canonical).toBe(`${SITE_URL}/fr`);
+ });
+});
diff --git a/src/lib/seo.ts b/src/lib/seo.ts
index 14838bb..e335e6b 100644
--- a/src/lib/seo.ts
+++ b/src/lib/seo.ts
@@ -1,21 +1,109 @@
-import { locales } from "@/i18n/config";
+import type { Metadata } from "next";
+import { locales, type Locale } from "@/i18n/config";
+import { getDictionary, type Dictionary } from "@/i18n/dictionaries";
-export function getAlternates(path: string, currentLang: string) {
- const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://weftmap.com";
+export const SITE_URL = "https://weftmap.vercel.app";
+
+export const SITE_NAME = "Weftmap";
+
+// Appended to every child-route title by the layout's title.template, and to
+// the homepage title by title.default. Titles are budgeted around it: the
+// composed result must stay under 60 chars or Google truncates it.
+export const TITLE_SUFFIX = ` — ${SITE_NAME}`;
+
+// Shared with src/app/[lang]/opengraph-image.tsx, which renders the image.
+// English-only on purpose: Satori's default font has no Arabic coverage.
+export const OG_IMAGE_SIZE = { width: 1200, height: 630 };
+export const OG_IMAGE_ALT =
+ "Weftmap — interactive call graphs from source code";
+export type SeoKey = keyof Dictionary["seo"];
+
+type SeoEntry = { title: string; description: string };
+
+// OpenGraph wants a full locale identifier, not the bare language subtag.
+const OG_LOCALES: Record = {
+ en: "en_US",
+ es: "es_ES",
+ pt: "pt_BR",
+ ar: "ar_AR",
+ fr: "fr_FR",
+ it: "it_IT",
+};
+
+export function getAlternates(path: string, currentLang: string) {
// 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);
+ const languages = locales.reduce(
+ (acc, locale) => {
+ acc[locale] = `${SITE_URL}/${locale}${cleanPath}`;
+ return acc;
+ },
+ {} as Record,
+ );
// Set default fallback language version to English
- languages["x-default"] = `${baseUrl}/en${cleanPath}`;
+ languages["x-default"] = `${SITE_URL}/en${cleanPath}`;
return {
- canonical: `${baseUrl}/${currentLang}${cleanPath}`,
+ canonical: `${SITE_URL}/${currentLang}${cleanPath}`,
languages,
};
}
+
+// The i18n key-parity test makes a missing key a build failure, so this
+// fallback should never fire. Kept so a half-translated dictionary degrades to
+// English rather than rendering an empty title.
+export function getSeoEntry(lang: Locale, key: SeoKey): SeoEntry {
+ const localized = getDictionary(lang).seo as Record;
+ const english = getDictionary("en").seo as Record;
+ return localized[key] ?? english[key];
+}
+
+/**
+ * Composes the full metadata for a route: localized title and description,
+ * canonical, hreflang, OpenGraph and Twitter card — all from one computation,
+ * so og:url and rel="canonical" cannot drift apart.
+ *
+ * The image is declared explicitly rather than left to the opengraph-image file
+ * convention: setting `openGraph` in a child segment REPLACES the parent's
+ * whole openGraph object, so the file-based image silently disappears from
+ * every route except the homepage.
+ */
+export function buildMetadata({
+ lang,
+ path,
+ seoKey,
+}: {
+ lang: Locale;
+ path: string;
+ seoKey: SeoKey;
+}): Metadata {
+ const { title, description } = getSeoEntry(lang, seoKey);
+ const { canonical, languages } = getAlternates(path, lang);
+ const images = [
+ { url: `/${lang}/opengraph-image`, ...OG_IMAGE_SIZE, alt: OG_IMAGE_ALT },
+ ];
+
+ return {
+ title,
+ description,
+ alternates: { canonical, languages },
+ openGraph: {
+ type: "website",
+ url: canonical,
+ title: title + TITLE_SUFFIX,
+ description,
+ siteName: SITE_NAME,
+ locale: OG_LOCALES[lang],
+ images,
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: title + TITLE_SUFFIX,
+ description,
+ images,
+ },
+ };
+}
diff --git a/src/lib/structured-data.test.ts b/src/lib/structured-data.test.ts
new file mode 100644
index 0000000..bc505d4
--- /dev/null
+++ b/src/lib/structured-data.test.ts
@@ -0,0 +1,109 @@
+import { describe, expect, test } from "vitest";
+import { locales } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { DOC_SLUGS, type DocSlug } from "./docs";
+import { SITE_URL } from "./seo";
+import {
+ buildBreadcrumbs,
+ buildFaqSchema,
+ buildOrganization,
+ buildSoftwareApplication,
+ buildTechArticle,
+} from "./structured-data";
+
+function collectUndefinedPaths(value: unknown, path = "$"): string[] {
+ if (value === undefined) return [path];
+ if (Array.isArray(value)) {
+ return value.flatMap((item, i) => collectUndefinedPaths(item, `${path}[${i}]`));
+ }
+ if (value !== null && typeof value === "object") {
+ return Object.entries(value).flatMap(([key, val]) =>
+ collectUndefinedPaths(val, `${path}.${key}`),
+ );
+ }
+ return [];
+}
+
+function builders(lang: (typeof locales)[number]) {
+ return {
+ Organization: buildOrganization(),
+ SoftwareApplication: buildSoftwareApplication(lang),
+ FAQPage: buildFaqSchema(lang),
+ BreadcrumbList: buildBreadcrumbs(lang, [
+ { name: "Weftmap", path: "" },
+ { name: "Docs", path: "docs/introduction" },
+ ]),
+ TechArticle: buildTechArticle(lang, "introduction"),
+ };
+}
+
+describe.each(locales)("structured data for locale '%s'", (lang) => {
+ const built = builders(lang);
+
+ test.each(Object.keys(built))("%s declares schema.org context and a type", (name) => {
+ const node = built[name as keyof typeof built] as Record;
+ expect(node["@context"]).toBe("https://schema.org");
+ expect(node["@type"]).toBe(name);
+ });
+
+ test.each(Object.keys(built))("%s contains no undefined values", (name) => {
+ const node = built[name as keyof typeof built];
+ expect(collectUndefinedPaths(node, name)).toEqual([]);
+ });
+
+ test("FAQPage mirrors the visible FAQ content exactly", () => {
+ const { faqs } = getDictionary(lang);
+ const schema = buildFaqSchema(lang) as {
+ mainEntity: { name: string; acceptedAnswer: { text: string } }[];
+ };
+ expect(schema.mainEntity).toHaveLength(faqs.length);
+ schema.mainEntity.forEach((entry, i) => {
+ expect(entry.name).toBe(faqs[i].q);
+ expect(entry.acceptedAnswer.text).toBe(faqs[i].a);
+ });
+ });
+
+ test("SoftwareApplication is free and lists the supported languages", () => {
+ const app = buildSoftwareApplication(lang) as {
+ offers: { price: string };
+ programmingLanguage: string[];
+ description: string;
+ };
+ expect(app.offers.price).toBe("0");
+ expect(app.programmingLanguage).toContain("Python");
+ expect(app.programmingLanguage).toContain("SQL");
+ expect(app.description).toBe(getDictionary(lang).seo.home.description);
+ });
+
+ test("breadcrumb items are absolute and correctly positioned", () => {
+ const crumbs = buildBreadcrumbs(lang, [
+ { name: "Weftmap", path: "" },
+ { name: "Docs", path: "docs/introduction" },
+ { name: "Languages", path: "docs/languages" },
+ ]) as { itemListElement: { position: number; item: string }[] };
+
+ expect(crumbs.itemListElement.map((c) => c.position)).toEqual([1, 2, 3]);
+ expect(crumbs.itemListElement[0].item).toBe(`${SITE_URL}/${lang}`);
+ expect(crumbs.itemListElement[2].item).toBe(
+ `${SITE_URL}/${lang}/docs/languages`,
+ );
+ });
+
+ test.each(DOC_SLUGS)("TechArticle for '%s' is localized and linked", (slug) => {
+ const article = buildTechArticle(lang, slug as DocSlug) as {
+ inLanguage: string;
+ url: string;
+ headline: string;
+ };
+ expect(article.inLanguage).toBe(lang);
+ expect(article.url).toBe(`${SITE_URL}/${lang}/docs/${slug}`);
+ expect(article.headline.length).toBeGreaterThan(0);
+ });
+});
+
+describe("Organization", () => {
+ test("links to the GitHub repository", () => {
+ const org = buildOrganization() as { sameAs: string[] };
+ expect(org.sameAs).toContain("https://github.com/DataDave-Dev/weftmap");
+ });
+});
diff --git a/src/lib/structured-data.ts b/src/lib/structured-data.ts
new file mode 100644
index 0000000..f628f34
--- /dev/null
+++ b/src/lib/structured-data.ts
@@ -0,0 +1,108 @@
+import type { Locale } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { REPO_URL } from "@/lib/constants";
+import { LANDING_NAMES } from "@/lib/landing-languages";
+import { SITE_NAME, SITE_URL, getSeoEntry } from "@/lib/seo";
+import type { DocSlug } from "@/lib/docs";
+
+/**
+ * JSON-LD builders. Every payload is assembled from dictionary data and repo
+ * constants — never from user input. `graphs/[id]` renders user-supplied titles
+ * and therefore gets no structured data at all.
+ */
+
+const ORGANIZATION_ID = `${SITE_URL}/#organization`;
+
+type Json = Record;
+
+function localeHome(lang: Locale): string {
+ return `${SITE_URL}/${lang}`;
+}
+
+export function buildOrganization(): Json {
+ return {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "@id": ORGANIZATION_ID,
+ name: SITE_NAME,
+ url: SITE_URL,
+ logo: `${SITE_URL}/icon.svg`,
+ sameAs: [REPO_URL],
+ };
+}
+
+export function buildSoftwareApplication(lang: Locale): Json {
+ return {
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: SITE_NAME,
+ description: getSeoEntry(lang, "home").description,
+ url: localeHome(lang),
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "Any",
+ inLanguage: lang,
+ isAccessibleForFree: true,
+ license: "https://opensource.org/licenses/MIT",
+ programmingLanguage: LANDING_NAMES,
+ offers: {
+ "@type": "Offer",
+ price: "0",
+ priceCurrency: "USD",
+ },
+ author: { "@id": ORGANIZATION_ID },
+ };
+}
+
+/**
+ * Built from the same `faqs` array the visible section renders. Google
+ * penalizes FAQ markup describing content that isn't on the page, and sourcing
+ * both from one array makes that divergence structurally impossible.
+ */
+export function buildFaqSchema(lang: Locale): Json {
+ const { faqs } = getDictionary(lang);
+ return {
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: faqs.map((faq) => ({
+ "@type": "Question",
+ name: faq.q,
+ acceptedAnswer: { "@type": "Answer", text: faq.a },
+ })),
+ };
+}
+
+export type Crumb = { name: string; path: string };
+
+export function buildBreadcrumbs(lang: Locale, trail: Crumb[]): Json {
+ return {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: trail.map((crumb, index) => ({
+ "@type": "ListItem",
+ position: index + 1,
+ name: crumb.name,
+ item: crumb.path
+ ? `${localeHome(lang)}/${crumb.path}`
+ : localeHome(lang),
+ })),
+ };
+}
+
+export function buildTechArticle(lang: Locale, slug: DocSlug): Json {
+ const { title, description } = getSeoEntry(lang, `docs.${slug}`);
+ return {
+ "@context": "https://schema.org",
+ "@type": "TechArticle",
+ headline: title,
+ description,
+ inLanguage: lang,
+ url: `${localeHome(lang)}/docs/${slug}`,
+ author: { "@id": ORGANIZATION_ID },
+ publisher: { "@id": ORGANIZATION_ID },
+ isPartOf: {
+ "@type": "WebSite",
+ name: SITE_NAME,
+ url: SITE_URL,
+ },
+ };
+}