diff --git a/AGENTS.md b/AGENTS.md index fbcd930d..e096be25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,7 +121,7 @@ Short “what is X / describe X architecture” prompts (≤12 words, concept ma | Docs / legal | `docs/`, `privacy/`, `terms/` | Static content | | Templates | `templates/`, `templates/[slug]/` | Public gallery of `data/templates` (slug = id with hyphens, `lib/site/templateCatalog.ts`); diagram images are static SVGs at `templates//diagram-{light,dark}.svg` (`lib/server/templatePreview.ts`, same layout as `/editor?template=`) | -SEO: public pages build their metadata with `pageMetadata` (`lib/site/seo.ts`: canonical, OG, Twitter). The root layout sets no `alternates.canonical` (it would be inherited and mark every page a duplicate of `/`), and page titles omit the brand (the root template appends `| ArchDraw`). Brand mark: `app/icon.svg`, `app/apple-icon.png`, `app/favicon.ico`, `public/logo.*`, `app/manifest.ts`, all drawn from `components/ui/BrandMark.tsx`. +SEO (playbook and change log: `SEO.md` at the repo root; add every new SEO technique there with its PR): public pages build their metadata with `pageMetadata` (`lib/site/seo.ts`: canonical, OG, Twitter). The root layout sets no `alternates.canonical` (it would be inherited and mark every page a duplicate of `/`), and page titles omit the brand (the root template appends `| ArchDraw`). Brand mark: `app/icon.svg`, `app/apple-icon.png`, `app/favicon.ico`, `public/logo.*`, `app/manifest.ts`, all drawn from `components/ui/BrandMark.tsx`. ### Key API routes (`frontend/app/api/`) diff --git a/SEO.md b/SEO.md new file mode 100644 index 00000000..50f41a99 --- /dev/null +++ b/SEO.md @@ -0,0 +1,270 @@ +# SEO playbook: the ArchDraw case study + +How we took ArchDraw (archdraw.hiabhee.online, a Next.js App Router site) from "launched" to "built to rank", and how to repeat it on another site. Each section gives the technique, why it works, what we did here, and the details that matter. + +Keep this file current. When we add or change an SEO technique, add it here, with its PR. + +--- + +## 0. Results so far (November 2026) + +| Measure | Before | After | +|---|---|---| +| Indexable URLs in the sitemap | 78 | ~130 (36 templates, 24 tutorials, 20 glossary terms, 3 free tools, 3 comparisons, hubs) | +| Tutorial page HTML (ChatGPT) | 66 words, no H1 | ~2,200 words, H1 "ChatGPT System Design" | +| Template page (ChatGPT) | 572 words | 1,230 words + FAQ rich results | +| Smallest template page (MVC) | 285 words | 706 words | +| Crawlable links from `/tutorials` to tutorials | 0 | 24 | +| Lighthouse SEO | 100 | 100 | +| Lighthouse Agentic Browsing | 2/3 | 3/3 expected (WebMCP tools registered) | + +Rankings take weeks to move. Re-measure in Search Console 4–6 weeks after each deploy (section 12). + +--- + +## 1. Start with an audit, not with content + +**Technique.** Before writing anything, fetch every important page the way a crawler does and record what it really receives. + +**What we checked, per URL:** HTTP status, ``, meta description, `<meta name="robots">`, `X-Robots-Tag` header, `<link rel="canonical">`, H1 and H2s, word count of the server HTML (scripts and styles stripped), structured data types, and internal links. + +**How:** a short script reads `sitemap.xml`, fetches each URL with `curl`, and parses the HTML. We ran it against both localhost and production. + +**What it found here** (each is a common failure on other sites too): +- Pages rendered in the browser shipped almost no HTML. The tutorials were a full-screen client app: 66 words and no H1. +- A list page navigated with `onClick` handlers, so crawlers saw no links to the pages it listed. +- The root layout set a canonical URL, which every child page inherited, marking them all as duplicates of `/`. +- Titles repeated the brand, because the template already appends `| ArchDraw`. +- The home page targeted only the product's newest feature, not the category people search for. + +**Rule:** the audit is the source of truth. Re-run it after every SEO change. + +--- + +## 2. Technical foundation + +### 2.1 One metadata helper for every public page +`frontend/lib/site/seo.ts` exports `pageMetadata({ title, description, path, type?, image?, keywords?, noIndex? })`. It returns the title, a **self-referencing canonical**, Open Graph and Twitter data in one call, plus `breadcrumbJsonLd(trail)` and `absoluteUrl(path)`. + +- **Why:** consistency. Every page gets a canonical, an `og:url` and a social title, and nobody forgets one. +- **Detail:** the root layout sets **no** `alternates.canonical`, because a child page inherits it unless it sets its own. The title template is `%s | ArchDraw`, so page titles omit the brand. + +### 2.2 Titles and descriptions +- Write the title as the search query: "ChatGPT Architecture Diagram (Free Template)", "Load Balancer in System Design: How It Works". +- Keep titles to about 60 characters including the brand, and descriptions to 160 or fewer. Tests enforce both for generated pages. + +### 2.3 Crawling and indexing +- **`app/robots.ts`:** allow the public sections, disallow private ones (`/editor`, `/dashboard`, `/api/`, `/share`, `/embed/`, `/live/`, `/admin`, `/auth`). Keep `/api/og/` allowed, so social preview images can be fetched. +- **`noindex`** goes on private and user-generated surfaces through layout metadata: the editor, dashboard, live and watch links, and shared and embedded diagrams. Unknown slugs get a "not found" page that is also `noindex`. +- **`app/sitemap.ts`** generates entries from the data (templates, tutorials, glossary, blogs) with a fixed build date, so the sitemap doesn't change on every request. +- **Static generation:** `generateStaticParams` plus `dynamicParams = false` on data-driven routes, so unknown slugs return 404 instead of empty pages. + +### 2.4 Structured data (JSON-LD) +| Page type | Schema | +|---|---| +| Site-wide | `Organization` (with logo), `WebSite`, `SoftwareApplication` | +| Galleries and hubs | `CollectionPage` + `ItemList` | +| Template pages | `CreativeWork` + `BreadcrumbList` + `FAQPage` | +| Tutorial pages | `TechArticle` + `LearningResource` (`timeRequired`, `educationalLevel`) + `BreadcrumbList` | +| Glossary | `DefinedTermSet` (hub) and `DefinedTerm` + `FAQPage` (term pages) | +| Free tools | `WebApplication` with an `Offer` of price 0, plus `FAQPage` | + +**Rule:** only mark up content that is visible on the page. FAQ markup must match an FAQ the reader can see. + +### 2.5 Brand and icons +- Use one mark everywhere: `app/icon.svg` (switches with `prefers-color-scheme`), `app/apple-icon.png`, `app/favicon.ico` (16, 32 and 48px, square), `public/logo.png` (512px, used as the `Organization` logo), and the web manifest. +- **Lesson:** Google's favicon service (`google.com/s2/favicons`) and apps that use it, including Claude's connector list, cache old icons for weeks. After a brand change, request indexing of the home page and remove and re-add integrations. +- The MCP server also sends `title`, `websiteUrl` and `icons` in its connect info (server 1.2.1), so AI clients show the right mark. + +### 2.6 Agent and AI discoverability +- **Machine-readable summaries:** `llms.txt` and `llms-full.txt`, an `openapi.json`, `changelog.json` and a static `catalog.json` of templates and tutorials. +- **AI crawlers** (GPTBot, ClaudeBot, PerplexityBot and others) are explicitly allowed in `robots.txt`. +- **WebMCP** (Lighthouse "Agentic Browsing"): `components/providers/WebMcpTools.tsx` registers tools with `document.modelContext.registerTool`: search templates, search tutorials, open a template, generate a diagram and open a page. + - The tools only navigate to same-site paths they build themselves; an agent can't pass in a URL. + - The origin-trial token goes in `NEXT_PUBLIC_WEBMCP_ORIGIN_TRIAL`. + +--- + +## 3. Map keywords to page types (search intent) + +**Technique.** Give each search intent exactly one kind of page, so pages don't compete with each other (keyword cannibalisation). + +| Intent | Example queries | Page type | URL | +|---|---|---|---| +| Learn to design a system | "system design", "how to design WhatsApp", "X system design" | Tutorial | `/tutorials/<id>` | +| See or download a diagram | "architecture diagram", "ChatGPT architecture diagram" | Template | `/templates/<slug>` | +| Understand a building block | "what is a load balancer", "API gateway system design" | Glossary term | `/system-design/<term>` | +| Use a tool | "free architecture diagram generator", "text to diagram", "QPS calculator" | Free tool | `/free-*`, `/system-design-calculator` | +| Compare tools | "Eraser alternative", "draw.io vs" | Comparison | `/alternatives/<slug>` | + +Each page type says its phrase in the title, the H1, the description and the link text pointing to it. Tutorials say "System Design"; templates say "Architecture Diagram"; the glossary hub says "System Design Concepts", so it doesn't compete with `/tutorials`. + +**Be realistic about head terms.** "System design" and "architecture diagram" belong to sites with years of backlinks. Win the long tail first (X architecture diagram, how to design X, what is X). Those pages then link to the hubs, which is how the hubs eventually rank. + +--- + +## 4. Turn hidden content into HTML + +**Technique.** Before writing new content, look for content the site already has but doesn't serve as HTML. + +- **Here:** the tutorial data already held about 46,000 words of teaching text, but the interactive app rendered it only in the browser. +- **Fix:** render a written guide on the server below the app (`components/tutorial/TutorialArticle.tsx`, built by the pure `lib/tutorial/tutorialArticle.ts`). It has the H1, every stage and component with its explanation, and links. +- **Detail:** the guide is visible, normal content, not hidden text. The app's full-screen overlays were scoped to the app (`position: absolute` inside a `relative` container) so they don't cover the guide. + +--- + +## 5. Write content that answers the query + +### 5.1 Template guides +Every template page carries a guide (`data/templates/guides.ts`) with the same sections: +- how the architecture works (2–3 paragraphs); +- **request flow, step by step** (numbered, following the diagram's real connections); +- key design decisions, each with its reason and its cost; +- how it scales; +- three questions people search for, also as `FAQPage`. + +**Guardrails (worth copying):** +- Write against the actual diagram. A test checks that each guide belongs to a real template, has every section and names at least 60% of that template's components, so guides can't drift from their diagrams. +- Describe real products from public information only, and say so on the page ("A typical design for a ChatGPT-style system… not OpenAI's internal architecture"). +- **Don't auto-fill from shared snippets.** Generic tooltip text was written for one context ("streams video") and would have been wrong elsewhere. + +### 5.2 New pages where demand has no page +- We added 10 templates for systems that had a tutorial but no diagram (WhatsApp, Twitter, Airbnb, Discord, Zoom, Spotify, LinkedIn, Figma, GitHub, rate limiter). +- **Quality bar:** generating them straight from the tutorials gave eight-node diagrams with odd links, so we drew them by hand (11–18 components, labelled connections). Every component the tutorial teaches is included, so the template and the lesson agree. +- **Deliberately skipped:** YouTube, Stripe, Shopify, DoorDash and Notion, because existing generic pages already cover them. A second near-identical page would compete with the first. + +### 5.3 Glossary (programmatic, but each page is distinct) +- `/system-design` plus 20 term pages from `data/glossary/terms.ts`. Each page covers what it is, how it works, when to use it, trade-offs, common technologies and an FAQ. +- **The trick that makes them unique:** each term has a `match` pattern, and `lib/site/glossary.ts` uses it to classify every template and tutorial component. Each term page then lists the real diagrams that use it. +- **Detail:** classify by a component's name first and its subtitle second, so "API Gateway (Auth + routing)" counts as a gateway, not an auth service. Tests check that every term links to at least one template or tutorial. + +--- + +## 6. Internal linking + +Internal links tell search engines what matters and spread ranking strength across the site. What we did: +- **Real `<a href>` links everywhere.** Cards that navigate with `onClick` are invisible to crawlers. We kept the card's click and made the title a `Link` (with `stopPropagation`, so the two don't both fire). +- **Links between page types,** with link text matching the target's search phrase: + - templates ↔ tutorials for the same system; + - template components → glossary terms; + - tutorial guides → "Concepts in this design"; + - glossary terms → templates and tutorials; + - free tools → each other, the templates and the tutorials. +- **Hubs high in the structure:** Templates and Tutorials lead the nav, the home page has a "Learn" section linking both hubs plus six pages of each, and the footer lists every hub and tool. +- **When two pages cover one system,** the dedicated page wins. `templateForTutorial` prefers a template whose main tutorial is this one. + +--- + +## 7. Free tools (link bait that also converts) + +**Technique.** Free, useful, no-sign-up tools rank for "free X" queries, earn natural backlinks and turn visitors into users. + +**Rule: one distinct tool per page.** Near-duplicate pages aimed at keyword variants ("free diagram generator", "AI diagram generator", "online diagram maker") are **doorway pages** under Google's spam policies and can hurt the whole site. Merge variants into one page and redirect the extras. + +What we built: +| Page | What it does | Detail | +|---|---|---| +| `/free-architecture-diagram-generator` | AI generator: describe a system or paste a GitHub repo, and it opens the editor and generates | Covers both "free" and "AI" searches. `/ai-architecture-diagram-generator` redirects to it permanently (308). | +| `/free-diagram-generator` | Mermaid text in, a laid-out diagram out, download PNG or SVG, "Edit in ArchDraw" | Uses the real pipeline (`POST /api/diagram/load` with `dryRun` + `preview`, which stores nothing). The SVG is shown as an `<img>`, so nothing in it can run as script. Fair-use rate limit. | +| `/system-design-calculator` | Back-of-the-envelope estimator: queries per second, peak load, storage, bandwidth | Pure, tested maths (`lib/site/capacityEstimate.ts`), example presets, latency and powers-of-two tables, links to the glossary | + +Ideas for later, each a distinct, real tool: +- an architecture diagram icon pack (download SVGs); +- a Mermaid syntax validator; +- a system design interview question bank with a practice timer; +- an SLA / uptime calculator ("99.9% = 8.76 h a year"); +- a database choice helper (SQL vs NoSQL questionnaire). + +--- + +## 8. Comparison and alternative pages + +- `/alternatives` plus one page per competitor (Eraser, draw.io, Lucidchart). Each has an at-a-glance table, "What they do well", "What we do differently", **"Choose them if"** and "Choose us if", and an FAQ. +- **Honesty rules:** + - Research current features before writing. All three competitors now have AI generation, so "they have no AI" would have been false. + - Leave out prices and quotas, which change often; link to their pricing instead. + - Include only table rows you're sure of; we removed one we couldn't verify. + - Show the date the comparison was checked. +- **Why "Choose them if":** it makes the page credible, and the visitors who do choose you are a better fit. + +--- + +## 9. Lead capture inside content + +- A prompt box on the generator and comparison pages opens the editor and starts generating straight away (`/editor?generate=<text>`). +- Example prompts are one-click links, marked `rel="nofollow"` because they lead to an app URL, not content. +- Each template page has "Open in editor", plus a link to the matching tutorial. +- **Copy matches what the product really allows:** + - guests can generate and export PNG or JSON; + - saving, sharing and SVG export need sign-in. + + Untrue "no account needed" claims cost trust. + +--- + +## 10. Images and previews + +- Template diagrams are served as static SVGs (`/templates/<slug>/diagram-{light,dark}.svg`), using the same layout as the editor, with descriptive `alt` text ("WhatsApp Architecture Diagram"). +- Social cards (`/api/og/*`) all draw the current brand mark from one component (`lib/site/ogBrandMark.tsx`). +- **Next:** a social preview image per template showing its own diagram, and PNG copies plus an image sitemap for Google Images, which shows image results for "X architecture diagram" searches. + +--- + +## 11. Quality gates (make SEO hard to break) + +Tests in the repository that fail when SEO regresses: +- `lib/site/__tests__/templateCatalog.test.ts`: + - URL-safe, unique slugs; + - titles shaped like "X Architecture Diagram"; + - descriptions of 160 characters or fewer; + - template ↔ tutorial links resolve. +- `lib/tutorial/__tests__/tutorialArticle.test.ts`: "X System Design" titles of 60 characters or fewer, and every step written out. +- `data/templates/__tests__/guides.test.ts`: guides are complete and tied to their diagram. +- `lib/site/__tests__/glossary.test.ts`: complete terms, a link for every term, and classification by name before subtitle. +- `lib/tutorial/__tests__/playthrough.test.ts`: every tutorial can actually be completed. This one is about product quality, which is what keeps visitors from bouncing. +- Layout quality tests (`layoutAesthetics`, `layoutReadability`) hold every shipped diagram to the same standard, so new template pages don't look broken. + +--- + +## 12. Launch and measurement checklist + +After each deploy: +1. Run the audit script (section 1) against production: every sitemap URL returns 200, is indexable and has a self-referencing canonical. +2. Search Console: + - resubmit `sitemap.xml`; + - use URL Inspection → "Request indexing" for the new hubs and a few new pages; + - check structured data with the Rich Results Test. +3. Lighthouse: SEO 100, Best Practices 100, Agentic Browsing 3/3. For WebMCP, use Chrome with `chrome://flags/#enable-webmcp-testing`. +4. After 4–6 weeks: + - in Search Console's Performance report, find queries in positions 8–30 and improve those pages first; + - check the Pages report for "Duplicate without user-selected canonical" and "Crawled – currently not indexed". +5. Off-site (needed for head terms): + - post on Show HN and Product Hunt; + - submit to AI-tool and MCP directories; + - open PRs adding templates to awesome-system-design lists; + - cross-post deep dives to dev.to with a canonical link back. + +--- + +## 13. Lessons learned + +- **Search engines only count what's in the HTML.** A rich client app can be close to empty for them. +- **One page per intent.** Cannibalisation and doorway pages do more harm than missing pages. +- **Accuracy is an SEO feature.** Honest comparisons, "representative design" notes and tested guides build the trust that earns links. +- **Product bugs are SEO bugs.** A tutorial whose steps can't be completed sends visitors back to the results page. Our playthrough test found that 22 of 24 tutorials had steps that never accepted a correct connection. +- **Caches outlive deploys.** Favicons, social cards and connector icons can stay stale for weeks after a brand change. + +--- + +## 14. Change log + +| Date | Change | PR | +|---|---|---| +| 2026-11-07 | Brand mark everywhere, per-page canonicals, noindex on private pages, robots and sitemap, template gallery, legal pages, `llms.txt` | #22 | +| 2026-11-07 | Search-intent split (tutorials vs templates), server-rendered tutorial guides, crawlable tutorial links, nav, home and footer links; tutorial connection-check fix | #24 | +| 2026-11-07 | MCP server icons (1.2.1), plugin icon, social card marks | #26 | +| 2026-11-07 | WebMCP tools and `catalog.json` (Lighthouse Agentic Browsing) | #27 | +| 2026-11-07 | Written guides on all 26 template pages, with FAQ structured data | #28, #29 | +| 2026-11-07 | Ten new templates for systems that had only a tutorial | #30 | +| 2026-11-07 | System design concepts glossary (20 terms), with links in from templates and tutorials | #31 | +| 2026-11-07 | Free tools (AI generator, text-to-diagram, system design calculator) and comparison pages; this playbook | #32 | diff --git a/frontend/app/alternatives/[slug]/page.tsx b/frontend/app/alternatives/[slug]/page.tsx new file mode 100644 index 00000000..2ac89a0b --- /dev/null +++ b/frontend/app/alternatives/[slug]/page.tsx @@ -0,0 +1,160 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { Check } from 'lucide-react'; +import { LandingNav } from '@/components/landing/LandingNav'; +import { Footer } from '@/components/landing/Footer'; +import { GeneratorPromptBox } from '@/components/landing/GeneratorPromptBox'; +import { ALTERNATIVES, getAlternative } from '@/data/alternatives'; +import { breadcrumbJsonLd, pageMetadata } from '@/lib/site/seo'; + +interface Props { + params: Promise<{ slug: string }>; +} + +export const dynamicParams = false; + +export function generateStaticParams() { + return ALTERNATIVES.map((a) => ({ slug: a.slug })); +} + +export async function generateMetadata({ params }: Props): Promise<Metadata> { + const { slug } = await params; + const alt = getAlternative(slug); + if (!alt) return { title: 'Comparison not found', robots: { index: false } }; + return pageMetadata({ + title: `${alt.name} Alternative: ArchDraw vs ${alt.name}`, + description: alt.summary, + path: `/alternatives/${alt.slug}`, + type: 'article', + keywords: [`${alt.name.toLowerCase()} alternative`, `archdraw vs ${alt.name.toLowerCase()}`, `${alt.name.toLowerCase()} vs`, 'architecture diagram tool'], + }); +} + +const h2 = 'text-xl font-semibold tracking-[-.03em] text-text-primary'; +const li = 'flex gap-2.5 leading-7 text-text-secondary'; + +function Ticks({ items }: { items: string[] }) { + return ( + <ul className="mt-4 space-y-2"> + {items.map((s) => ( + <li key={s.slice(0, 40)} className={li}><Check size={16} aria-hidden="true" className="mt-1.5 flex-shrink-0 text-text-muted" />{s}</li> + ))} + </ul> + ); +} + +export default async function AlternativePage({ params }: Props) { + const { slug } = await params; + const alt = getAlternative(slug); + if (!alt) notFound(); + const path = `/alternatives/${alt.slug}`; + const checked = new Date(`${alt.checkedOn}T00:00:00Z`).toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }); + + const jsonLd = { + '@context': 'https://schema.org', + '@graph': [ + { '@type': 'FAQPage', mainEntity: alt.faq.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) }, + breadcrumbJsonLd([['Alternatives', '/alternatives'], [`ArchDraw vs ${alt.name}`, path]]), + ], + }; + + return ( + <div className="min-h-screen bg-surface-page text-text-primary"> + <LandingNav /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> + <main className="mx-auto w-full max-w-3xl px-4 pb-24 pt-24 sm:px-6"> + <nav aria-label="Breadcrumb" className="mb-8 text-xs text-text-muted"> + <ol className="flex flex-wrap items-center gap-1.5"> + <li><Link href="/" className="transition-colors hover:text-text-primary">Home</Link></li> + <li aria-hidden="true">/</li> + <li><Link href="/alternatives" className="transition-colors hover:text-text-primary">Alternatives</Link></li> + <li aria-hidden="true">/</li> + <li aria-current="page" className="text-text-secondary">ArchDraw vs {alt.name}</li> + </ol> + </nav> + + <header> + <p className="mb-3 font-mono text-[10px] font-bold uppercase tracking-[.16em] text-brand-text">Comparison</p> + <h1 className="text-[clamp(2rem,4.5vw,3rem)] font-semibold leading-[1.05] tracking-[-.04em]">ArchDraw vs {alt.name}</h1> + <div className="mt-5 space-y-4">{alt.about.map((p) => <p key={p.slice(0, 40)} className="text-lg leading-8 text-text-secondary">{p}</p>)}</div> + </header> + + <section aria-labelledby="table-title" className="mt-12"> + <h2 id="table-title" className={h2}>At a glance</h2> + <div className="mt-4 overflow-x-auto rounded-xl border border-border-default"> + <table className="w-full min-w-[520px] text-left text-sm"> + <thead className="bg-surface-panel text-text-primary"> + <tr> + <th scope="col" className="px-4 py-3 font-semibold"> </th> + <th scope="col" className="px-4 py-3 font-semibold">ArchDraw</th> + <th scope="col" className="px-4 py-3 font-semibold">{alt.name}</th> + </tr> + </thead> + <tbody className="divide-y divide-border-default bg-surface-card"> + {alt.rows.map((r) => ( + <tr key={r.feature}> + <th scope="row" className="px-4 py-3 font-medium text-text-primary">{r.feature}</th> + <td className="px-4 py-3 text-text-secondary">{r.archdraw}</td> + <td className="px-4 py-3 text-text-secondary">{r.them}</td> + </tr> + ))} + </tbody> + </table> + </div> + <p className="mt-2 text-xs text-text-muted"> + Checked {checked} from public information. Features and plans change; see <a href={alt.pricingUrl} rel="noopener nofollow" className="underline hover:text-text-secondary">{alt.name}'s site</a> for current details. + </p> + </section> + + <div className="mt-12 grid gap-10 sm:grid-cols-2"> + <section aria-labelledby="strengths-title"> + <h2 id="strengths-title" className={h2}>What {alt.name} does well</h2> + <Ticks items={alt.theirStrengths} /> + </section> + <section aria-labelledby="diff-title"> + <h2 id="diff-title" className={h2}>What ArchDraw does differently</h2> + <Ticks items={alt.archdrawDifferences} /> + </section> + </div> + + <div className="mt-12 grid gap-10 sm:grid-cols-2"> + <section aria-labelledby="them-title"> + <h2 id="them-title" className={h2}>Choose {alt.name} if</h2> + <Ticks items={alt.chooseThem} /> + </section> + <section aria-labelledby="us-title"> + <h2 id="us-title" className={h2}>Choose ArchDraw if</h2> + <Ticks items={alt.chooseArchDraw} /> + </section> + </div> + + <section aria-labelledby="try-title" className="mt-14"> + <h2 id="try-title" className={h2}>Try ArchDraw on your own system</h2> + <p className="mt-2 text-sm text-text-secondary">Free during the beta. Describe a system or paste a GitHub repository.</p> + <div className="mt-4"><GeneratorPromptBox placeholder="e.g. A chat app with WebSockets, message history in Cassandra and push notifications" /></div> + </section> + + <section aria-labelledby="faq-title" className="mt-14"> + <h2 id="faq-title" className={h2}>Frequently asked questions</h2> + <div className="mt-5 divide-y divide-border-default rounded-xl border border-border-default bg-surface-card"> + {alt.faq.map((f) => ( + <div key={f.question} className="px-5 py-4"> + <h3 className="text-sm font-semibold">{f.question}</h3> + <p className="mt-2 text-sm leading-6 text-text-secondary">{f.answer}</p> + </div> + ))} + </div> + <p className="mt-6 text-sm text-text-secondary"> + Other comparisons:{' '} + {ALTERNATIVES.filter((a) => a.slug !== alt.slug).map((a, i) => ( + <span key={a.slug}>{i > 0 && ', '}<Link href={`/alternatives/${a.slug}`} className="font-medium text-brand-text hover:underline">ArchDraw vs {a.name}</Link></span> + ))} + . + </p> + </section> + </main> + <Footer /> + </div> + ); +} diff --git a/frontend/app/alternatives/page.tsx b/frontend/app/alternatives/page.tsx new file mode 100644 index 00000000..6bc828db --- /dev/null +++ b/frontend/app/alternatives/page.tsx @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { ArrowRight } from 'lucide-react'; +import { LandingNav } from '@/components/landing/LandingNav'; +import { Footer } from '@/components/landing/Footer'; +import { ALTERNATIVES } from '@/data/alternatives'; +import { breadcrumbJsonLd, pageMetadata } from '@/lib/site/seo'; + +export const metadata = pageMetadata({ + title: 'ArchDraw Alternatives and Comparisons', + description: 'How ArchDraw compares with Eraser, draw.io and Lucidchart for software architecture diagrams, including when each tool is the better choice.', + path: '/alternatives', + keywords: ['architecture diagram tool comparison', 'eraser alternative', 'draw.io alternative', 'lucidchart alternative'], +}); + +export default function AlternativesPage() { + return ( + <div className="min-h-screen bg-surface-page text-text-primary"> + <LandingNav /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ '@context': 'https://schema.org', ...breadcrumbJsonLd([['Alternatives', '/alternatives']]) }) }} /> + <main className="mx-auto w-full max-w-3xl px-4 pb-24 pt-28 sm:px-6"> + <p className="mb-4 font-mono text-[10px] font-bold uppercase tracking-[.16em] text-brand-text">Compare</p> + <h1 className="text-[clamp(2.2rem,5vw,3.4rem)] font-semibold leading-[1.02] tracking-[-.045em]">ArchDraw compared</h1> + <p className="mt-5 text-lg leading-8 text-text-secondary"> + ArchDraw is built for one job: software architecture diagrams, generated from a description or a codebase and kept editable. Here is how it compares with other tools, including when they are the better choice. + </p> + <ul className="mt-10 space-y-4"> + {ALTERNATIVES.map((a) => ( + <li key={a.slug}> + <Link href={`/alternatives/${a.slug}`} className="group flex items-center justify-between gap-4 rounded-xl border border-border-default bg-surface-card p-5 transition-colors hover:border-border-strong"> + <span> + <span className="block text-base font-semibold">ArchDraw vs {a.name}</span> + <span className="mt-1 block text-sm leading-6 text-text-secondary">{a.about[0]}</span> + </span> + <ArrowRight size={16} aria-hidden="true" className="flex-shrink-0 text-text-muted transition-transform group-hover:translate-x-0.5" /> + </Link> + </li> + ))} + </ul> + <p className="mt-10 text-sm text-text-secondary"> + Or just try it: the <Link href="/free-architecture-diagram-generator" className="font-medium text-brand-text hover:underline">free AI architecture diagram generator</Link>. + </p> + </main> + <Footer /> + </div> + ); +} diff --git a/frontend/app/free-architecture-diagram-generator/page.tsx b/frontend/app/free-architecture-diagram-generator/page.tsx new file mode 100644 index 00000000..ca61120e --- /dev/null +++ b/frontend/app/free-architecture-diagram-generator/page.tsx @@ -0,0 +1,151 @@ +import Link from 'next/link'; +import { ArrowRight, Bot, Code2, GitBranch, MessageSquareText } from 'lucide-react'; +import { LandingNav } from '@/components/landing/LandingNav'; +import { Footer } from '@/components/landing/Footer'; +import { GeneratorPromptBox } from '@/components/landing/GeneratorPromptBox'; +import { TEMPLATES } from '@/data/templates/index'; +import { templateHeading, templateSlug } from '@/lib/site/templateCatalog'; +import { absoluteUrl, breadcrumbJsonLd, pageMetadata } from '@/lib/site/seo'; + +const PATH = '/free-architecture-diagram-generator'; + +export const metadata = pageMetadata({ + title: 'Free AI Architecture Diagram Generator', + description: 'Free AI architecture diagram generator: describe a system or paste a GitHub repo and get an editable, auto-laid-out software architecture diagram.', + path: PATH, + keywords: ['free architecture diagram generator', 'ai architecture diagram generator', 'ai diagram generator', 'system architecture diagram generator', 'architecture diagram maker', 'github repo to diagram', 'text to architecture diagram'], +}); + +const EXAMPLES = [ + 'A food delivery app with ordering, restaurant menus, courier dispatch, payments and push notifications', + 'A RAG chatbot over company documents with embeddings, a vector database and streaming answers', + 'An e-commerce checkout with cart, inventory reservation, payments and order events on Kafka', + 'A multi-tenant SaaS backend with an API gateway, auth, Postgres per tenant and background jobs', + 'A real-time multiplayer game backend with matchmaking, game servers, leaderboards and Redis', + 'An IoT platform ingesting device telemetry through MQTT into a stream processor and time-series database', +]; + +const WAYS = [ + { icon: MessageSquareText, title: 'Describe it', body: 'Write what the system does in plain English. Add details (the databases you use, the queues, the clients) for a closer match.' }, + { icon: GitBranch, title: 'Paste a GitHub repo', body: 'ArchDraw reads the repository\'s architecture files and code, checks its findings against the README, and draws the system.', href: '/repo-diagram' }, + { icon: Code2, title: 'Paste Mermaid', body: 'Open the code panel and paste a Mermaid graph; ArchDraw lays it out and styles it, and keeps the code and canvas in sync.' }, + { icon: Bot, title: 'Ask your AI agent', body: 'Connect Claude or another MCP client and ask it to draw or update a diagram; it returns a link to the editable result.', href: '/mcp' }, +]; + +const FAQ = [ + { question: 'How does the AI architecture diagram generator work?', answer: 'An AI model plans the architecture from your description and writes it as a Mermaid graph. ArchDraw then builds the components, groups and connections and lays them out with a deterministic layout engine, so you get a tidy, editable diagram rather than an image.' }, + { question: 'Is it free?', answer: 'Yes. ArchDraw is free during its public beta. You can generate diagrams without an account; sign in with Google or GitHub to save them, share links and export SVG.' }, + { question: 'Can I edit the generated diagram?', answer: 'Yes. Everything is editable on the canvas: move, rename, restyle, add components and connections, or edit the diagram as Mermaid. You can also ask the AI to change it.' }, + { question: 'Can it diagram my existing codebase?', answer: 'Yes, for public GitHub repositories. Paste the repository URL and ArchDraw builds the diagram from the repo\'s files, verified against the code and the README.' }, + { question: 'What can I export?', answer: 'PNG and JSON without an account, SVG when signed in, and the diagram as Mermaid from the code panel. Signed-in users can also share a read-only link or embed it.' }, +]; + +const JSON_LD = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'SoftwareApplication', + name: 'ArchDraw Free AI Architecture Diagram Generator', + applicationCategory: 'DesignApplication', + operatingSystem: 'Web', + url: absoluteUrl(PATH), + offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, + publisher: { '@id': `${absoluteUrl('/')}#organization` }, + }, + { '@type': 'FAQPage', mainEntity: FAQ.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) }, + breadcrumbJsonLd([['Free architecture diagram generator', PATH]]), + ], +}; + +const h2 = 'text-2xl font-semibold tracking-[-.03em] text-text-primary'; + +export default function GeneratorPage() { + const featured = ['chatgpt', 'microservices', 'netflix', 'event_driven', 'whatsapp', 'serverless'].flatMap((id) => TEMPLATES.find((t) => t.id === id) ?? []); + return ( + <div className="min-h-screen bg-surface-page text-text-primary"> + <LandingNav /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} /> + <main className="mx-auto w-full max-w-[1100px] px-4 pb-24 pt-28 sm:px-8"> + <header className="mx-auto max-w-3xl text-center"> + <p className="mb-4 font-mono text-[10px] font-bold uppercase tracking-[.16em] text-brand-text">Free AI tool</p> + <h1 className="text-[clamp(2.4rem,5.5vw,4.2rem)] font-semibold leading-[1] tracking-[-.045em]">Free AI architecture diagram generator</h1> + <p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-text-secondary"> + Describe a system or paste a GitHub repository, and get a clean, editable software architecture diagram in seconds, laid out for you. + </p> + </header> + + <div className="mx-auto mt-10 max-w-3xl"> + <GeneratorPromptBox placeholder="e.g. A ride-sharing backend with rider and driver apps, matching, live driver locations, trips and payments" /> + </div> + + <section aria-labelledby="examples-title" className="mx-auto mt-10 max-w-3xl"> + <h2 id="examples-title" className="text-sm font-semibold text-text-secondary">Try an example</h2> + <ul className="mt-3 grid gap-2 sm:grid-cols-2"> + {EXAMPLES.map((example) => ( + <li key={example}> + <a href={`/editor?generate=${encodeURIComponent(example)}`} rel="nofollow" className="block h-full rounded-lg border border-border-default bg-surface-card px-4 py-3 text-sm leading-6 text-text-secondary transition-colors hover:border-border-strong hover:text-text-primary"> + {example} + </a> + </li> + ))} + </ul> + </section> + + <section aria-labelledby="ways-title" className="pt-20"> + <h2 id="ways-title" className={h2}>Four ways to create an architecture diagram</h2> + <div className="mt-6 grid gap-4 sm:grid-cols-2"> + {WAYS.map(({ icon: Icon, title, body, href }) => ( + <div key={title} className="rounded-xl border border-border-default bg-surface-card p-5"> + <Icon size={20} aria-hidden="true" className="text-brand-text" /> + <h3 className="mt-3 text-base font-semibold">{title}</h3> + <p className="mt-2 text-sm leading-6 text-text-secondary">{body}</p> + {href && <Link href={href} className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-brand-text hover:underline">Learn more <ArrowRight size={14} aria-hidden="true" /></Link>} + </div> + ))} + </div> + </section> + + <section aria-labelledby="how-title" className="mx-auto max-w-3xl pt-20"> + <h2 id="how-title" className={h2}>How it works</h2> + <ol className="mt-5 list-decimal space-y-3 pl-5 text-base leading-7 text-text-secondary marker:text-text-muted"> + <li className="pl-1"><strong className="font-semibold text-text-primary">Plan.</strong> An AI model reads your description and plans the components, how they connect and how they group into areas.</li> + <li className="pl-1"><strong className="font-semibold text-text-primary">Write it as Mermaid.</strong> The plan becomes a Mermaid graph, a text format you can read and edit.</li> + <li className="pl-1"><strong className="font-semibold text-text-primary">Lay it out.</strong> A deterministic layout engine places every component and routes the connections, so related parts line up and the diagram stays readable.</li> + <li className="pl-1"><strong className="font-semibold text-text-primary">Edit and share.</strong> The result is a real diagram, not a picture: change anything on the canvas or in the code, then export or share it.</li> + </ol> + </section> + + <section aria-labelledby="templates-title" className="pt-20"> + <h2 id="templates-title" className={h2}>Or start from a template</h2> + <ul className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> + {featured.map((t) => ( + <li key={t.id}> + <Link href={`/templates/${templateSlug(t)}`} className="group flex items-center justify-between rounded-lg border border-border-default bg-surface-card px-4 py-3 text-sm font-medium transition-colors hover:border-border-strong"> + {templateHeading(t)} + <ArrowRight size={14} aria-hidden="true" className="text-text-muted transition-transform group-hover:translate-x-0.5" /> + </Link> + </li> + ))} + </ul> + <p className="mt-4 text-sm text-text-secondary"> + Already have diagram code? Try the <Link href="/free-diagram-generator" className="font-medium text-brand-text hover:underline">free diagram generator</Link>, which draws Mermaid instantly. Browse all <Link href="/templates" className="font-medium text-brand-text hover:underline">architecture diagram templates</Link>, or compare ArchDraw with{' '} + <Link href="/alternatives" className="font-medium text-brand-text hover:underline">other diagram tools</Link>. + </p> + </section> + + <section aria-labelledby="faq-title" className="mx-auto max-w-3xl pt-20"> + <h2 id="faq-title" className={h2}>Frequently asked questions</h2> + <div className="mt-5 divide-y divide-border-default rounded-xl border border-border-default bg-surface-card"> + {FAQ.map((f) => ( + <div key={f.question} className="px-5 py-4"> + <h3 className="text-sm font-semibold">{f.question}</h3> + <p className="mt-2 text-sm leading-6 text-text-secondary">{f.answer}</p> + </div> + ))} + </div> + </section> + </main> + <Footer /> + </div> + ); +} diff --git a/frontend/app/free-diagram-generator/page.tsx b/frontend/app/free-diagram-generator/page.tsx new file mode 100644 index 00000000..cc644d96 --- /dev/null +++ b/frontend/app/free-diagram-generator/page.tsx @@ -0,0 +1,121 @@ +import Link from 'next/link'; +import { LandingNav } from '@/components/landing/LandingNav'; +import { Footer } from '@/components/landing/Footer'; +import { DiagramFromTextTool } from '@/components/landing/tools/DiagramFromTextTool'; +import { absoluteUrl, breadcrumbJsonLd, pageMetadata } from '@/lib/site/seo'; + +const PATH = '/free-diagram-generator'; + +export const metadata = pageMetadata({ + title: 'Free Diagram Generator: Text to Diagram Online', + description: 'Free online diagram generator: write a flowchart or system diagram as text (Mermaid) and get a clean, laid-out diagram to download as PNG or SVG. No sign-up.', + path: PATH, + keywords: ['free diagram generator', 'text to diagram', 'mermaid to png', 'mermaid diagram generator', 'flowchart generator', 'online diagram maker'], +}); + +const SYNTAX: Array<{ code: string; meaning: string }> = [ + { code: 'graph LR', meaning: 'Start a diagram that flows left to right (use graph TD for top to bottom).' }, + { code: 'A[Web App]', meaning: 'A box with id A and the label "Web App".' }, + { code: 'DB[(Postgres)]', meaning: 'A database (cylinder) shape.' }, + { code: 'LB{{Load Balancer}}', meaning: 'A hexagon, used for gateways and load balancers.' }, + { code: 'A --> B', meaning: 'An arrow from A to B.' }, + { code: 'A -->|HTTPS| B', meaning: 'An arrow with a label.' }, + { code: 'A -.-> B', meaning: 'A dashed arrow, for asynchronous calls.' }, + { code: 'subgraph Backend ... end', meaning: 'A group around the components inside it.' }, +]; + +const FAQ = [ + { question: 'Is this diagram generator really free?', answer: 'Yes. Draw as many diagrams as you like and download them as PNG or SVG without an account. A fair-use limit of a few hundred diagrams an hour applies.' }, + { question: 'What kinds of diagrams can it draw?', answer: 'Flowcharts and box-and-arrow diagrams written in Mermaid graph syntax: system architecture, data flows, processes and pipelines, with groups, labelled arrows and shapes for databases, queues and gateways.' }, + { question: 'Do I need to know Mermaid?', answer: 'No. Start from the example and change the names, or use the free AI architecture diagram generator, which writes the diagram from a plain-English description.' }, + { question: 'Is my diagram stored?', answer: 'No. Drawing and downloading does not save anything. Only "Edit in ArchDraw" saves a copy, so it can open in the editor.' }, +]; + +const JSON_LD = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebApplication', + name: 'Free Diagram Generator', + url: absoluteUrl(PATH), + applicationCategory: 'DesignApplication', + operatingSystem: 'Web', + offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, + publisher: { '@id': `${absoluteUrl('/')}#organization` }, + }, + { '@type': 'FAQPage', mainEntity: FAQ.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) }, + breadcrumbJsonLd([['Free diagram generator', PATH]]), + ], +}; + +const h2 = 'text-2xl font-semibold tracking-[-.03em] text-text-primary'; + +export default function FreeDiagramGeneratorPage() { + return ( + <div className="min-h-screen bg-surface-page text-text-primary"> + <LandingNav /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} /> + <main className="mx-auto w-full max-w-[1200px] px-4 pb-24 pt-28 sm:px-8"> + <header className="max-w-3xl"> + <p className="mb-4 font-mono text-[10px] font-bold uppercase tracking-[.16em] text-brand-text">Free tool · No sign-up</p> + <h1 className="text-[clamp(2.3rem,5vw,3.8rem)] font-semibold leading-[1] tracking-[-.045em]">Free diagram generator</h1> + <p className="mt-5 text-lg leading-8 text-text-secondary"> + Write a diagram as text and get a clean, automatically laid-out diagram. Download it as PNG or SVG, or open it in the editor to keep working. + </p> + </header> + + <div className="mt-10"><DiagramFromTextTool /></div> + + <div className="mx-auto max-w-3xl"> + <section aria-labelledby="syntax-title" className="pt-20"> + <h2 id="syntax-title" className={h2}>How to write a diagram</h2> + <p className="mt-3 leading-7 text-text-secondary">The generator reads Mermaid graph syntax, a simple text format for boxes and arrows. These lines cover almost everything:</p> + <div className="mt-5 overflow-x-auto rounded-xl border border-border-default"> + <table className="w-full min-w-[480px] text-left text-sm"> + <tbody className="divide-y divide-border-default bg-surface-card"> + {SYNTAX.map((row) => ( + <tr key={row.code}> + <td className="whitespace-nowrap px-4 py-3 font-mono text-[13px] text-text-primary">{row.code}</td> + <td className="px-4 py-3 text-text-secondary">{row.meaning}</td> + </tr> + ))} + </tbody> + </table> + </div> + </section> + + <section aria-labelledby="why-title" className="pt-16"> + <h2 id="why-title" className={h2}>Why draw diagrams as text</h2> + <ul className="mt-4 list-disc space-y-2 pl-5 leading-7 text-text-secondary marker:text-text-muted"> + <li className="pl-1">Layout is automatic: add a component and the diagram rearranges itself, so it never turns into a tangle.</li> + <li className="pl-1">Text fits in a README, a pull request or a design doc, and changes show up in diffs.</li> + <li className="pl-1">AI assistants write Mermaid well, so you can ask one for a first draft and paste it here.</li> + </ul> + </section> + + <section aria-labelledby="more-title" className="pt-16"> + <h2 id="more-title" className={h2}>More free tools</h2> + <ul className="mt-4 space-y-2 leading-7 text-text-secondary"> + <li><Link href="/free-architecture-diagram-generator" className="font-medium text-brand-text hover:underline">Free AI architecture diagram generator</Link>: describe a system in plain English or paste a GitHub repo.</li> + <li><Link href="/system-design-calculator" className="font-medium text-brand-text hover:underline">System design calculator</Link>: estimate requests per second, storage and bandwidth.</li> + <li><Link href="/templates" className="font-medium text-brand-text hover:underline">Architecture diagram templates</Link>: start from ChatGPT, Netflix, WhatsApp and more.</li> + </ul> + </section> + + <section aria-labelledby="faq-title" className="pt-16"> + <h2 id="faq-title" className={h2}>Frequently asked questions</h2> + <div className="mt-5 divide-y divide-border-default rounded-xl border border-border-default bg-surface-card"> + {FAQ.map((f) => ( + <div key={f.question} className="px-5 py-4"> + <h3 className="text-sm font-semibold">{f.question}</h3> + <p className="mt-2 text-sm leading-6 text-text-secondary">{f.answer}</p> + </div> + ))} + </div> + </section> + </div> + </main> + <Footer /> + </div> + ); +} diff --git a/frontend/app/robots.ts b/frontend/app/robots.ts index b66965f4..175881c2 100644 --- a/frontend/app/robots.ts +++ b/frontend/app/robots.ts @@ -24,7 +24,7 @@ const DISCOVERY_ALLOW = [ '/robots.txt', ]; -const ALLOWED = ['/', '/docs', '/blogs', '/templates', '/pricing', '/faq', '/about', '/contact', '/tutorials', '/system-design', '/mcp', '/agent-skill', '/repo-diagram', '/api/og/', '/changelog.json', '/manifest.webmanifest', ...DISCOVERY_ALLOW]; +const ALLOWED = ['/', '/docs', '/blogs', '/templates', '/pricing', '/faq', '/about', '/contact', '/tutorials', '/system-design', '/free-architecture-diagram-generator', '/free-diagram-generator', '/system-design-calculator', '/alternatives', '/mcp', '/agent-skill', '/repo-diagram', '/api/og/', '/changelog.json', '/manifest.webmanifest', ...DISCOVERY_ALLOW]; // AI crawler user-agents — explicitly allowlisted per checklist §2 WAF note + §3 robots.txt. const AI_BOTS = [ diff --git a/frontend/app/sitemap.ts b/frontend/app/sitemap.ts index 719f2103..6f382b16 100644 --- a/frontend/app/sitemap.ts +++ b/frontend/app/sitemap.ts @@ -3,6 +3,7 @@ import { TUTORIALS } from '@/data/tutorials'; import { blogs } from '@/data/blogs'; import { allTemplateSlugs } from '@/lib/site/templateCatalog'; import { GLOSSARY } from '@/data/glossary/terms'; +import { ALTERNATIVES } from '@/data/alternatives'; // Sitemap: canonical URLs only. `/learn/*` route does not exist — removed 2026-09-02 (was doubling sitemap size + 404s in Search Console). // `lastModified` uses stable build-time date so sitemap is cacheable (previously `new Date()` per entry → ETag churn). @@ -56,6 +57,11 @@ export default function sitemap(): MetadataRoute.Sitemap { { url: `${base}/privacy`, lastModified: BUILD_DATE, changeFrequency: 'yearly', priority: 0.3 }, { url: `${base}/terms`, lastModified: BUILD_DATE, changeFrequency: 'yearly', priority: 0.3 }, { url: `${base}/system-design`, lastModified: BUILD_DATE, changeFrequency: 'monthly', priority: 0.8 }, + { url: `${base}/free-architecture-diagram-generator`, lastModified: BUILD_DATE, changeFrequency: 'monthly', priority: 0.9 }, + { url: `${base}/free-diagram-generator`, lastModified: BUILD_DATE, changeFrequency: 'monthly', priority: 0.9 }, + { url: `${base}/system-design-calculator`, lastModified: BUILD_DATE, changeFrequency: 'monthly', priority: 0.8 }, + { url: `${base}/alternatives`, lastModified: BUILD_DATE, changeFrequency: 'monthly', priority: 0.6 }, + ...ALTERNATIVES.map((a) => ({ url: `${base}/alternatives/${a.slug}`, lastModified: BUILD_DATE, changeFrequency: 'monthly' as const, priority: 0.6 })), ...templateEntries, ...glossaryEntries, ...blogEntries, diff --git a/frontend/app/system-design-calculator/page.tsx b/frontend/app/system-design-calculator/page.tsx new file mode 100644 index 00000000..a7f0017c --- /dev/null +++ b/frontend/app/system-design-calculator/page.tsx @@ -0,0 +1,123 @@ +import Link from 'next/link'; +import { LandingNav } from '@/components/landing/LandingNav'; +import { Footer } from '@/components/landing/Footer'; +import { CapacityCalculator } from '@/components/landing/tools/CapacityCalculator'; +import { absoluteUrl, breadcrumbJsonLd, pageMetadata } from '@/lib/site/seo'; + +const PATH = '/system-design-calculator'; + +export const metadata = pageMetadata({ + title: 'System Design Calculator (Back-of-the-Envelope)', + description: 'Free back-of-the-envelope calculator for system design interviews: estimate QPS, peak traffic, storage and bandwidth from users and usage.', + path: PATH, + keywords: ['system design calculator', 'back of the envelope estimation', 'capacity estimation', 'qps calculator', 'system design interview estimation'], +}); + +/** Widely quoted orders of magnitude (popularised by Jeff Dean and Peter Norvig); real hardware varies. */ +const LATENCY: Array<[string, string]> = [ + ['L1 cache reference', '~1 ns'], + ['Main memory reference', '~100 ns'], + ['Read 1 MB sequentially from memory', '~10 µs'], + ['Round trip within a data center', '~0.5 ms'], + ['Read 1 MB sequentially from SSD', '~1 ms'], + ['Disk seek (spinning disk)', '~10 ms'], + ['Round trip between continents', '~150 ms'], +]; + +const POWERS: Array<[string, string, string]> = [ + ['2¹⁰', 'Thousand', '1 KB'], + ['2²⁰', 'Million', '1 MB'], + ['2³⁰', 'Billion', '1 GB'], + ['2⁴⁰', 'Trillion', '1 TB'], + ['2⁵⁰', 'Quadrillion', '1 PB'], +]; + +const FAQ = [ + { question: 'What is back-of-the-envelope estimation in system design?', answer: 'Quick, rough arithmetic to size a system before designing it: how many requests per second it must handle, how much data it stores and how much bandwidth it needs. It shows which parts need caching, sharding or a CDN.' }, + { question: 'How do you calculate QPS from daily active users?', answer: 'Multiply daily active users by requests per user per day, then divide by 86,400 seconds in a day. Multiply by a peak factor (often 2 to 5) to size for the busiest hour.' }, + { question: 'How accurate are these estimates?', answer: 'They are orders of magnitude, not forecasts. They are meant to tell 100 requests per second apart from 100,000, which is what drives design decisions.' }, +]; + +const JSON_LD = { + '@context': 'https://schema.org', + '@graph': [ + { '@type': 'WebApplication', name: 'System Design Calculator', url: absoluteUrl(PATH), applicationCategory: 'UtilitiesApplication', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, publisher: { '@id': `${absoluteUrl('/')}#organization` } }, + { '@type': 'FAQPage', mainEntity: FAQ.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) }, + breadcrumbJsonLd([['System design calculator', PATH]]), + ], +}; + +const h2 = 'text-2xl font-semibold tracking-[-.03em] text-text-primary'; + +export default function SystemDesignCalculatorPage() { + return ( + <div className="min-h-screen bg-surface-page text-text-primary"> + <LandingNav /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} /> + <main className="mx-auto w-full max-w-[1200px] px-4 pb-24 pt-28 sm:px-8"> + <header className="max-w-3xl"> + <p className="mb-4 font-mono text-[10px] font-bold uppercase tracking-[.16em] text-brand-text">Free tool</p> + <h1 className="text-[clamp(2.2rem,5vw,3.6rem)] font-semibold leading-[1.02] tracking-[-.045em]">System design calculator</h1> + <p className="mt-5 text-lg leading-8 text-text-secondary"> + Back-of-the-envelope estimates for system design interviews and real projects: enter users and usage, get requests per second, storage and bandwidth. + </p> + </header> + + <div className="mt-10"><CapacityCalculator /></div> + + <div className="mx-auto grid max-w-5xl gap-12 pt-20 lg:grid-cols-2"> + <section aria-labelledby="latency-title"> + <h2 id="latency-title" className={h2}>Latency numbers to know</h2> + <p className="mt-2 text-sm text-text-secondary">Orders of magnitude, popularised by Jeff Dean and Peter Norvig. Real hardware varies.</p> + <table className="mt-4 w-full text-left text-sm"> + <tbody className="divide-y divide-border-default"> + {LATENCY.map(([what, time]) => ( + <tr key={what}><td className="py-2.5 pr-4 text-text-secondary">{what}</td><td className="py-2.5 text-right font-mono text-text-primary">{time}</td></tr> + ))} + </tbody> + </table> + </section> + <section aria-labelledby="powers-title"> + <h2 id="powers-title" className={h2}>Powers of two</h2> + <p className="mt-2 text-sm text-text-secondary">The shortcuts used in every estimate.</p> + <table className="mt-4 w-full text-left text-sm"> + <tbody className="divide-y divide-border-default"> + {POWERS.map(([power, name, size]) => ( + <tr key={power}><td className="py-2.5 font-mono text-text-primary">{power}</td><td className="py-2.5 text-text-secondary">{name}</td><td className="py-2.5 text-right font-mono text-text-primary">{size}</td></tr> + ))} + </tbody> + </table> + </section> + </div> + + <div className="mx-auto max-w-3xl"> + <section aria-labelledby="use-title" className="pt-16"> + <h2 id="use-title" className={h2}>From numbers to a design</h2> + <ul className="mt-4 list-disc space-y-2 pl-5 leading-7 text-text-secondary marker:text-text-muted"> + <li className="pl-1">Thousands of reads per second on the same data: add a <Link href="/system-design/cache" className="font-medium text-brand-text hover:underline">cache</Link>.</li> + <li className="pl-1">Large media and global users: serve files from a <Link href="/system-design/cdn" className="font-medium text-brand-text hover:underline">CDN</Link> in front of <Link href="/system-design/object-storage" className="font-medium text-brand-text hover:underline">object storage</Link>.</li> + <li className="pl-1">Writes beyond one database server: partition the data, often in a <Link href="/system-design/nosql-database" className="font-medium text-brand-text hover:underline">NoSQL database</Link>.</li> + <li className="pl-1">Bursty work that can wait: put it on a <Link href="/system-design/message-queue" className="font-medium text-brand-text hover:underline">message queue</Link>.</li> + </ul> + <p className="mt-4 text-sm text-text-secondary"> + Then draw it: try the <Link href="/free-architecture-diagram-generator" className="font-medium text-brand-text hover:underline">free AI architecture diagram generator</Link>, or practise with the <Link href="/tutorials" className="font-medium text-brand-text hover:underline">system design tutorials</Link>. + </p> + </section> + + <section aria-labelledby="faq-title" className="pt-16"> + <h2 id="faq-title" className={h2}>Frequently asked questions</h2> + <div className="mt-5 divide-y divide-border-default rounded-xl border border-border-default bg-surface-card"> + {FAQ.map((f) => ( + <div key={f.question} className="px-5 py-4"> + <h3 className="text-sm font-semibold">{f.question}</h3> + <p className="mt-2 text-sm leading-6 text-text-secondary">{f.answer}</p> + </div> + ))} + </div> + </section> + </div> + </main> + <Footer /> + </div> + ); +} diff --git a/frontend/components/landing/Footer.tsx b/frontend/components/landing/Footer.tsx index 6c5836bb..4a3e13b9 100644 --- a/frontend/components/landing/Footer.tsx +++ b/frontend/components/landing/Footer.tsx @@ -6,9 +6,13 @@ const COLUMNS: Array<{ title: string; links: Array<{ label: string; href: string title: 'Product', links: [ { label: 'Editor', href: '/editor' }, + { label: 'AI diagram generator', href: '/free-architecture-diagram-generator' }, + { label: 'Free diagram generator', href: '/free-diagram-generator' }, + { label: 'System design calculator', href: '/system-design-calculator' }, { label: 'Architecture diagram templates', href: '/templates' }, { label: 'Repo to diagram', href: '/repo-diagram' }, { label: 'Pricing', href: '/pricing' }, + { label: 'Compare', href: '/alternatives' }, ], }, { diff --git a/frontend/components/landing/GeneratorPromptBox.tsx b/frontend/components/landing/GeneratorPromptBox.tsx new file mode 100644 index 00000000..7f970efd --- /dev/null +++ b/frontend/components/landing/GeneratorPromptBox.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Sparkles } from 'lucide-react'; + +const MAX_PROMPT = 2000; + +/** + * Describe a system (or paste a GitHub URL) and open the editor generating it, through + * the editor's existing `?generate=` entry point. + */ +export function GeneratorPromptBox({ placeholder }: { placeholder: string }) { + const router = useRouter(); + const [prompt, setPrompt] = useState(''); + const text = prompt.trim(); + + function submit(e: React.FormEvent) { + e.preventDefault(); + if (!text) return; + router.push(`/editor?generate=${encodeURIComponent(text.slice(0, MAX_PROMPT))}`); + } + + return ( + <form onSubmit={submit} className="rounded-2xl border border-border-default bg-surface-card p-3 shadow-2"> + <label htmlFor="generator-prompt" className="sr-only">Describe the system to draw</label> + <textarea + id="generator-prompt" + value={prompt} + onChange={(e) => setPrompt(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit(e); }} + rows={3} + maxLength={MAX_PROMPT} + placeholder={placeholder} + className="w-full resize-none bg-transparent px-2 py-1.5 text-base leading-7 text-text-primary outline-none placeholder:text-text-muted" + /> + <div className="flex items-center justify-between gap-3 px-2 pt-2"> + <span className="text-xs text-text-muted">Describe a system or paste a GitHub repo URL</span> + <button type="submit" disabled={!text} className="inline-flex items-center gap-2 rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-hover disabled:opacity-50"> + <Sparkles size={16} aria-hidden="true" /> Generate diagram + </button> + </div> + </form> + ); +} diff --git a/frontend/components/landing/tools/CapacityCalculator.tsx b/frontend/components/landing/tools/CapacityCalculator.tsx new file mode 100644 index 00000000..f6e70792 --- /dev/null +++ b/frontend/components/landing/tools/CapacityCalculator.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { compact, estimateCapacity, PRESETS, type CapacityInput } from '@/lib/site/capacityEstimate'; + +const FIELDS: Array<{ key: keyof CapacityInput; label: string; hint: string; step: number; percent?: boolean }> = [ + { key: 'dailyActiveUsers', label: 'Daily active users', hint: 'People using it on a typical day', step: 1000 }, + { key: 'requestsPerUserPerDay', label: 'Requests per user per day', hint: 'Reads and writes together', step: 1 }, + { key: 'writeShare', label: 'Writes (% of requests)', hint: 'Posts, messages, uploads', step: 1, percent: true }, + { key: 'peakFactor', label: 'Peak ÷ average', hint: 'Busiest hour vs the daily average', step: 0.5 }, + { key: 'responseKB', label: 'Response size (KB)', hint: 'Average data returned per request', step: 1 }, + { key: 'storedPerWriteKB', label: 'Stored per write (KB)', hint: 'New data each write adds', step: 1 }, + { key: 'retentionYears', label: 'Retention (years)', hint: 'How long data is kept', step: 1 }, + { key: 'replicas', label: 'Replicas', hint: 'Copies of each piece of data', step: 1 }, +]; + +function Stat({ label, value, unit }: { label: string; value: number; unit: string }) { + return ( + <div className="rounded-xl border border-border-default bg-surface-card p-4"> + <p className="text-xs text-text-muted">{label}</p> + <p className="mt-1 text-2xl font-semibold tracking-tight text-text-primary">{compact(value)}<span className="ml-1 text-sm font-medium text-text-secondary">{unit}</span></p> + </div> + ); +} + +/** Back-of-the-envelope estimator: users and usage in, QPS, storage and bandwidth out. */ +export function CapacityCalculator() { + const [input, setInput] = useState<CapacityInput>(PRESETS[2].input); + const [preset, setPreset] = useState(PRESETS[2].id); + const e = useMemo(() => estimateCapacity(input), [input]); + + const set = (key: keyof CapacityInput, raw: string, percent?: boolean) => { + const n = Number(raw); + setPreset(''); + setInput((prev) => ({ ...prev, [key]: percent ? n / 100 : n })); + }; + + return ( + <div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"> + <div className="rounded-2xl border border-border-default bg-surface-card p-5"> + <div className="flex flex-wrap gap-2" role="group" aria-label="Examples"> + {PRESETS.map((p) => ( + <button key={p.id} type="button" onClick={() => { setInput(p.input); setPreset(p.id); }} aria-pressed={preset === p.id} + className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${preset === p.id ? 'border-text-primary bg-text-primary text-surface-page' : 'border-border-default text-text-secondary hover:text-text-primary'}`}> + {p.label} + </button> + ))} + </div> + <div className="mt-5 grid gap-4 sm:grid-cols-2"> + {FIELDS.map((f) => ( + <label key={f.key} className="block"> + <span className="text-sm font-medium text-text-primary">{f.label}</span> + <input + type="number" + min={0} + step={f.step} + value={f.percent ? Math.round(input[f.key] * 1000) / 10 : input[f.key]} + onChange={(ev) => set(f.key, ev.target.value, f.percent)} + className="mt-1.5 w-full rounded-lg border border-border-default bg-surface-page px-3 py-2 text-sm tabular-nums text-text-primary outline-none focus:border-border-strong" + /> + <span className="mt-1 block text-xs text-text-muted">{f.hint}</span> + </label> + ))} + </div> + </div> + + <div className="space-y-4" aria-live="polite"> + <div className="grid gap-3 sm:grid-cols-3"> + <Stat label="Average requests / second" value={e.averageQps} unit="QPS" /> + <Stat label="Peak requests / second" value={e.peakQps} unit="QPS" /> + <Stat label="Requests per day" value={e.requestsPerDay} unit="" /> + <Stat label="Reads / second" value={e.readQps} unit="QPS" /> + <Stat label="Writes / second" value={e.writeQps} unit="QPS" /> + <Stat label="New storage per day" value={e.storagePerDayGB} unit="GB" /> + <Stat label="Storage per year" value={e.storagePerYearTB} unit="TB" /> + <Stat label="Total storage (retention)" value={e.totalStorageTB} unit="TB" /> + <Stat label="Peak outbound bandwidth" value={e.peakEgressMBps} unit="MB/s" /> + </div> + <p className="text-xs leading-5 text-text-muted"> + Estimates: requests/day = users × requests per user; QPS = requests/day ÷ 86,400; storage = writes × size × replicas. Real systems also need headroom, indexes and backups. + </p> + </div> + </div> + ); +} diff --git a/frontend/components/landing/tools/DiagramFromTextTool.tsx b/frontend/components/landing/tools/DiagramFromTextTool.tsx new file mode 100644 index 00000000..5ed35c36 --- /dev/null +++ b/frontend/components/landing/tools/DiagramFromTextTool.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Download, ExternalLink, Loader2, Play } from 'lucide-react'; + +export const DEFAULT_DIAGRAM = `graph LR + User[User] --> Web[Web App] + Web -->|HTTPS| API[API Gateway] + subgraph Backend + API --> Orders[Order Service] + API --> Auth[Auth Service] + Orders --> Queue[(Message Queue)] + end + Orders --> DB[(Postgres)] + Queue --> Email[Email Worker]`; + +type Style = 'studio' | 'sketch'; +const MAX_CHARS = 20_000; + +/** + * Free diagram generator: Mermaid flowchart text in, a laid-out diagram out, through the + * same pipeline as the editor (`POST /api/diagram/load` with `dryRun` + `preview`, which + * stores nothing). The SVG is shown as an <img>, so nothing in it can run as script. + */ +export function DiagramFromTextTool({ initial = DEFAULT_DIAGRAM }: { initial?: string }) { + const router = useRouter(); + const [code, setCode] = useState(initial); + const [style, setStyle] = useState<Style>('studio'); + const [svg, setSvg] = useState<string | null>(null); + const [error, setError] = useState<string | null>(null); + const [busy, setBusy] = useState<'render' | 'open' | null>(null); + const firstRender = useRef(true); + + const render = useCallback(async (text: string, look: Style) => { + if (!text.trim()) return; + setBusy('render'); + setError(null); + try { + const res = await fetch('/api/diagram/load', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ mermaid: text.slice(0, MAX_CHARS), dryRun: true, preview: true, previewStyle: look }), + }); + const body = await res.json().catch(() => ({})); + if (!res.ok || !body.previewSvg) { + setError(res.status === 429 ? 'Too many diagrams in a short time. Wait a minute and try again.' : (body.error || 'That diagram could not be drawn. Check the syntax and try again.')); + return; + } + setSvg(body.previewSvg as string); + } catch { + setError('Could not reach the diagram service. Check your connection and try again.'); + } finally { + setBusy(null); + } + }, []); + + useEffect(() => { + if (!firstRender.current) return; + firstRender.current = false; + void render(initial, 'studio'); + }, [initial, render]); + + function download(kind: 'svg' | 'png') { + if (!svg) return; + const blob = new Blob([svg], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + const save = (href: string, name: string) => { + const a = document.createElement('a'); + a.href = href; + a.download = name; + a.click(); + }; + if (kind === 'svg') { + save(url, 'diagram.svg'); + setTimeout(() => URL.revokeObjectURL(url), 1000); + return; + } + const img = new Image(); + img.onload = () => { + const scale = 2; + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth * scale; + canvas.height = img.naturalHeight * scale; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.scale(scale, scale); + ctx.drawImage(img, 0, 0); + URL.revokeObjectURL(url); + save(canvas.toDataURL('image/png'), 'diagram.png'); + }; + img.src = url; + } + + async function openInEditor() { + if (!code.trim()) return; + setBusy('open'); + setError(null); + try { + const res = await fetch('/api/diagram/load', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ mermaid: code.slice(0, MAX_CHARS) }), + }); + const body = await res.json().catch(() => ({})); + if (!res.ok || !body.sessionId) { + setError(res.status === 429 ? 'Too many diagrams in a short time. Wait a minute and try again.' : 'Could not open the diagram in the editor.'); + return; + } + router.push(`/editor?session=${encodeURIComponent(body.sessionId)}`); + } catch { + setError('Could not reach the diagram service. Check your connection and try again.'); + } finally { + setBusy(null); + } + } + + return ( + <div className="grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"> + <div className="flex flex-col rounded-2xl border border-border-default bg-surface-card"> + <label htmlFor="diagram-code" className="border-b border-border-default px-4 py-2.5 text-xs font-semibold text-text-secondary">Diagram text (Mermaid)</label> + <textarea + id="diagram-code" + value={code} + onChange={(e) => setCode(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) void render(code, style); }} + spellCheck={false} + maxLength={MAX_CHARS} + rows={16} + className="min-h-[320px] flex-1 resize-y bg-transparent px-4 py-3 font-mono text-[13px] leading-6 text-text-primary outline-none" + /> + <div className="flex flex-wrap items-center gap-2 border-t border-border-default px-3 py-2.5"> + <button type="button" onClick={() => void render(code, style)} disabled={busy !== null || !code.trim()} className="inline-flex items-center gap-2 rounded-lg bg-brand px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-hover disabled:opacity-50"> + {busy === 'render' ? <Loader2 size={15} className="animate-spin" aria-hidden="true" /> : <Play size={15} aria-hidden="true" />} Draw diagram + </button> + <label className="sr-only" htmlFor="diagram-style">Style</label> + <select id="diagram-style" value={style} onChange={(e) => { const s = e.target.value as Style; setStyle(s); void render(code, s); }} className="rounded-lg border border-border-default bg-surface-page px-2.5 py-2 text-sm text-text-primary"> + <option value="studio">Clean</option> + <option value="sketch">Hand-drawn</option> + </select> + <span className="ml-auto text-xs text-text-muted">⌘/Ctrl + Enter</span> + </div> + </div> + + <div className="flex min-h-[380px] flex-col rounded-2xl border border-border-default bg-surface-panel"> + <div className="flex flex-wrap items-center gap-2 border-b border-border-default px-3 py-2"> + <span className="text-xs font-semibold text-text-secondary">Preview</span> + <div className="ml-auto flex flex-wrap gap-2"> + <button type="button" onClick={() => download('png')} disabled={!svg} className="inline-flex items-center gap-1.5 rounded-md border border-border-default bg-surface-card px-2.5 py-1.5 text-xs font-medium text-text-primary disabled:opacity-50"><Download size={13} aria-hidden="true" /> PNG</button> + <button type="button" onClick={() => download('svg')} disabled={!svg} className="inline-flex items-center gap-1.5 rounded-md border border-border-default bg-surface-card px-2.5 py-1.5 text-xs font-medium text-text-primary disabled:opacity-50"><Download size={13} aria-hidden="true" /> SVG</button> + <button type="button" onClick={() => void openInEditor()} disabled={busy !== null || !code.trim()} className="inline-flex items-center gap-1.5 rounded-md border border-border-default bg-surface-card px-2.5 py-1.5 text-xs font-medium text-text-primary disabled:opacity-50"> + {busy === 'open' ? <Loader2 size={13} className="animate-spin" aria-hidden="true" /> : <ExternalLink size={13} aria-hidden="true" />} Edit in ArchDraw + </button> + </div> + </div> + <div className="flex flex-1 items-center justify-center overflow-auto p-4" aria-live="polite"> + {error ? ( + <p role="alert" className="max-w-sm text-center text-sm text-text-secondary">{error}</p> + ) : svg ? ( + // eslint-disable-next-line @next/next/no-img-element -- generated SVG shown as an image so it cannot run script + <img src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`} alt="Generated diagram" className="h-auto max-h-[560px] w-full object-contain" /> + ) : ( + <Loader2 size={20} className="animate-spin text-text-muted" aria-label="Drawing" /> + )} + </div> + </div> + </div> + ); +} diff --git a/frontend/data/alternatives/index.ts b/frontend/data/alternatives/index.ts new file mode 100644 index 00000000..c9f0b6c2 --- /dev/null +++ b/frontend/data/alternatives/index.ts @@ -0,0 +1,165 @@ +/** + * Comparison pages (`/alternatives`, `/alternatives/<slug>`). Claims about other products + * stay to stable, public facts (open source or not, focus, free tier exists, AI features + * exist); prices and quotas change often and are left out, with a link to their pricing. + * Keep `checkedOn` current when an entry is reviewed, and each page says when it was. + */ + +export interface ComparisonRow { + feature: string; + archdraw: string; + them: string; +} + +export interface Alternative { + slug: string; + /** The product compared, as its makers write it. */ + name: string; + pricingUrl: string; + /** Meta description (≤ 160 characters). */ + summary: string; + about: string[]; + theirStrengths: string[]; + archdrawDifferences: string[]; + chooseThem: string[]; + chooseArchDraw: string[]; + rows: ComparisonRow[]; + faq: Array<{ question: string; answer: string }>; + checkedOn: string; +} + +export const ALTERNATIVES: Alternative[] = [ + { + slug: 'eraser', + name: 'Eraser', + pricingUrl: 'https://www.eraser.io/pricing', + summary: 'ArchDraw vs Eraser for architecture diagrams: AI generation, diagrams from GitHub repos, Mermaid, and live maps of Claude Code sessions. An honest comparison.', + about: [ + 'Eraser is a workspace for engineering teams that combines docs, a whiteboard and diagrams. It has its own diagram-as-code syntax and an AI feature, DiagramGPT, that turns a description into a diagram.', + 'ArchDraw is built only for software architecture diagrams. It generates them from a prompt or a GitHub repository, edits them on a canvas or as Mermaid, and can show a live map of the Claude Code sessions working on your codebase.', + ], + theirStrengths: [ + 'Docs and diagrams in one place, useful for design documents and RFCs.', + 'A mature diagram-as-code language and editor.', + 'Integrations with tools engineering teams already use.', + ], + archdrawDifferences: [ + 'Diagrams from a GitHub repository: ArchDraw reads the repo\'s architecture files and code and draws the system.', + 'Mermaid in and out: paste Mermaid or edit the diagram as Mermaid next to the canvas.', + 'An MCP server, so Claude and other AI agents can draw and update diagrams for you.', + 'Live projects: one command maps your repository and shows each Claude Code session on it as it works.', + 'Open source, with free system design tutorials and architecture diagram templates.', + ], + chooseThem: [ + 'You want design docs and diagrams in one shared workspace.', + 'Your team already writes diagrams in Eraser\'s syntax.', + ], + chooseArchDraw: [ + 'You want an architecture diagram of an existing codebase without drawing it.', + 'You work with Mermaid, or want AI agents such as Claude to draw diagrams for you.', + 'You run several Claude Code sessions and want to see where each one is working.', + ], + rows: [ + { feature: 'Main focus', archdraw: 'Software architecture diagrams', them: 'Engineering docs, whiteboard and diagrams' }, + { feature: 'AI from a text prompt', archdraw: 'Yes', them: 'Yes (DiagramGPT)' }, + { feature: 'Diagrams as code', archdraw: 'Mermaid (import, export, live code panel)', them: 'Eraser\'s own diagram-as-code syntax' }, + { feature: 'Open source', archdraw: 'Yes', them: 'No' }, + { feature: 'Free to start', archdraw: 'Yes, free during the beta', them: 'Free plan with limits' }, + ], + faq: [ + { question: 'Is ArchDraw a free alternative to Eraser?', answer: 'ArchDraw is free during its public beta. You can generate diagrams without an account and sign in with Google or GitHub to save, share and export SVG.' }, + { question: 'Can ArchDraw import Eraser diagrams?', answer: 'Not directly. ArchDraw imports Mermaid and its own JSON, so a diagram can be moved by exporting it from Eraser in a format you can convert to Mermaid, or by describing it to ArchDraw\'s AI.' }, + ], + checkedOn: '2026-11-07', + }, + { + slug: 'drawio', + name: 'draw.io', + pricingUrl: 'https://www.drawio.com/', + summary: 'ArchDraw vs draw.io (diagrams.net) for software architecture diagrams: AI generation, GitHub repos to diagrams, Mermaid and automatic layout compared.', + about: [ + 'draw.io (also known as diagrams.net) is a free, open-source, general-purpose diagramming tool with large shape libraries, a desktop app and integrations with Confluence, Jira and cloud drives. It has added AI generation (the Generate tool) and an MCP server.', + 'ArchDraw is focused on software architecture. It generates diagrams from a prompt, a GitHub repository or Mermaid, lays them out automatically, and keeps them editable on a canvas.', + ], + theirStrengths: [ + 'Free and open source, with no account required and files stored where you choose.', + 'Huge shape libraries for every kind of diagram: network, UML, BPMN, floor plans and more.', + 'Precise manual control over every shape and connector.', + 'Deep Confluence and Jira integration.', + ], + archdrawDifferences: [ + 'Automatic, consistent layout: diagrams are laid out for you, so a generated or edited diagram stays tidy.', + 'Diagrams from a GitHub repository, built from the repo\'s architecture files and code.', + 'Purpose-built architecture components and styles instead of a general shape library.', + 'Live maps of Claude Code sessions on your codebase.', + ], + chooseThem: [ + 'You need many diagram types beyond software architecture.', + 'You want full manual control over every shape and pixel.', + 'Your team lives in Confluence and Jira.', + ], + chooseArchDraw: [ + 'You want a clean architecture diagram quickly, from a prompt or a repository.', + 'You prefer automatic layout to placing every box by hand.', + 'You work with Mermaid or with AI coding agents.', + ], + rows: [ + { feature: 'Main focus', archdraw: 'Software architecture diagrams', them: 'General-purpose diagramming' }, + { feature: 'AI from a text prompt', archdraw: 'Yes', them: 'Yes (Generate tool)' }, + { feature: 'Layout', archdraw: 'Automatic', them: 'Mostly manual, with layout helpers' }, + { feature: 'Mermaid', archdraw: 'Import, export and live code panel', them: 'Mermaid import' }, + { feature: 'Open source', archdraw: 'Yes', them: 'Yes' }, + { feature: 'Free', archdraw: 'Yes, free during the beta', them: 'Yes' }, + ], + faq: [ + { question: 'Is ArchDraw better than draw.io?', answer: 'For software architecture diagrams generated from a description or a codebase, ArchDraw is faster because it creates and lays out the diagram for you. For other diagram types or pixel-level manual control, draw.io is the more general tool.' }, + { question: 'Can I use draw.io and ArchDraw together?', answer: 'Yes. Generate the architecture in ArchDraw and export it as SVG or PNG to place in draw.io, Confluence or your docs.' }, + ], + checkedOn: '2026-11-07', + }, + { + slug: 'lucidchart', + name: 'Lucidchart', + pricingUrl: 'https://www.lucidchart.com/pages/pricing', + summary: 'ArchDraw vs Lucidchart for software architecture diagrams: AI generation, diagrams from GitHub repos, Mermaid and pricing approach compared.', + about: [ + 'Lucidchart is a commercial, collaborative diagramming platform used across many teams for flowcharts, org charts, process maps, UML and cloud architecture. It includes AI features (Lucid AI) and enterprise administration.', + 'ArchDraw is a focused, open-source tool for software architecture diagrams: it generates them from a prompt or a GitHub repository and keeps them editable as a canvas or as Mermaid.', + ], + theirStrengths: [ + 'One tool for every team and diagram type across a company.', + 'Real-time collaboration with comments and enterprise controls.', + 'Cloud architecture shape libraries for AWS, Azure and Google Cloud.', + ], + archdrawDifferences: [ + 'Built for software engineers: diagrams from a prompt, a GitHub repository or Mermaid.', + 'An MCP server so Claude and other AI agents can draw diagrams for you.', + 'Live maps of Claude Code sessions on your codebase.', + 'Open source and free during the beta.', + ], + chooseThem: [ + 'Your whole company standardises on one diagramming tool for many diagram types.', + 'You need enterprise administration, SSO and compliance features.', + ], + chooseArchDraw: [ + 'You are a developer who wants an architecture diagram without drawing it.', + 'You want diagrams from code and Mermaid, or drawn by AI agents.', + 'You want an open-source tool you can try for free right now.', + ], + rows: [ + { feature: 'Main focus', archdraw: 'Software architecture diagrams', them: 'Diagramming for every team' }, + { feature: 'AI from a text prompt', archdraw: 'Yes', them: 'Yes (Lucid AI)' }, + { feature: 'Open source', archdraw: 'Yes', them: 'No' }, + { feature: 'Free to start', archdraw: 'Yes, free during the beta', them: 'Free plan with a small number of documents' }, + ], + faq: [ + { question: 'Is there a free Lucidchart alternative for architecture diagrams?', answer: 'Yes. ArchDraw is free during its beta and focused on software architecture: describe a system or paste a GitHub repository and it draws an editable diagram.' }, + { question: 'Can I export ArchDraw diagrams to use elsewhere?', answer: 'Yes. Export PNG and JSON without an account and SVG when signed in, or copy the diagram as Mermaid from the code panel, to use in docs, slides or other tools.' }, + ], + checkedOn: '2026-11-07', + }, +]; + +export function getAlternative(slug: string): Alternative | undefined { + return ALTERNATIVES.find((a) => a.slug === slug); +} diff --git a/frontend/lib/site/__tests__/capacityEstimate.test.ts b/frontend/lib/site/__tests__/capacityEstimate.test.ts new file mode 100644 index 00000000..99218f96 --- /dev/null +++ b/frontend/lib/site/__tests__/capacityEstimate.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { compact, estimateCapacity, PRESETS } from '@/lib/site/capacityEstimate'; + +describe('capacity estimate', () => { + it('works out QPS, storage and bandwidth from users and usage', () => { + const e = estimateCapacity({ dailyActiveUsers: 86_400, requestsPerUserPerDay: 10, writeShare: 0.1, peakFactor: 2, responseKB: 1024, storedPerWriteKB: 1024, retentionYears: 1, replicas: 1 }); + expect(e.averageQps).toBe(10); + expect(e.peakQps).toBe(20); + expect(e.writeQps).toBeCloseTo(1); + expect(e.readQps).toBeCloseTo(9); + // 86,400 writes a day × 1 MB = 84.375 GB/day. + expect(e.storagePerDayGB).toBeCloseTo(84.375); + expect(e.egressMBps).toBe(10); + }); + + it('clamps nonsense input instead of returning NaN', () => { + const e = estimateCapacity({ dailyActiveUsers: -5, requestsPerUserPerDay: Number.NaN, writeShare: 3, peakFactor: 0, responseKB: 1, storedPerWriteKB: 1, retentionYears: 1, replicas: 0 }); + expect(Object.values(e).every(Number.isFinite)).toBe(true); + expect(e.averageQps).toBe(0); + }); + + it('formats numbers compactly and has working presets', () => { + expect(compact(12_345_678)).toBe('12.3M'); + expect(compact(950)).toBe('950'); + expect(compact(0.42)).toBe('0.42'); + for (const p of PRESETS) expect(estimateCapacity(p.input).averageQps).toBeGreaterThan(0); + }); +}); diff --git a/frontend/lib/site/capacityEstimate.ts b/frontend/lib/site/capacityEstimate.ts new file mode 100644 index 00000000..ae6087a0 --- /dev/null +++ b/frontend/lib/site/capacityEstimate.ts @@ -0,0 +1,90 @@ +/** + * Back-of-the-envelope capacity estimates for the system design calculator + * (`/system-design-calculator`). Pure: inputs in, rounded figures out. + */ + +export interface CapacityInput { + dailyActiveUsers: number; + /** Requests (reads + writes) one user makes per day. */ + requestsPerUserPerDay: number; + /** Share of requests that are writes, 0–1. */ + writeShare: number; + /** Peak traffic as a multiple of the daily average. */ + peakFactor: number; + /** Average response size returned per request, KB. */ + responseKB: number; + /** Data stored per write, KB. */ + storedPerWriteKB: number; + /** How long data is kept, years. */ + retentionYears: number; + /** Copies of each piece of data (replication factor). */ + replicas: number; +} + +export interface CapacityEstimate { + requestsPerDay: number; + averageQps: number; + peakQps: number; + readQps: number; + writeQps: number; + storagePerDayGB: number; + storagePerYearTB: number; + totalStorageTB: number; + egressMBps: number; + peakEgressMBps: number; +} + +const SECONDS_PER_DAY = 86_400; +const KB_PER_GB = 1024 * 1024; + +const clamp = (n: number, min: number, max = Number.MAX_SAFE_INTEGER) => (Number.isFinite(n) ? Math.min(Math.max(n, min), max) : min); + +export function estimateCapacity(raw: CapacityInput): CapacityEstimate { + const input = { + dailyActiveUsers: clamp(raw.dailyActiveUsers, 0), + requestsPerUserPerDay: clamp(raw.requestsPerUserPerDay, 0), + writeShare: clamp(raw.writeShare, 0, 1), + peakFactor: clamp(raw.peakFactor, 1, 100), + responseKB: clamp(raw.responseKB, 0), + storedPerWriteKB: clamp(raw.storedPerWriteKB, 0), + retentionYears: clamp(raw.retentionYears, 0, 100), + replicas: clamp(Math.round(raw.replicas), 1, 10), + }; + const requestsPerDay = input.dailyActiveUsers * input.requestsPerUserPerDay; + const averageQps = requestsPerDay / SECONDS_PER_DAY; + const writesPerDay = requestsPerDay * input.writeShare; + const storagePerDayGB = (writesPerDay * input.storedPerWriteKB * input.replicas) / KB_PER_GB; + const egressMBps = (averageQps * input.responseKB) / 1024; + return { + requestsPerDay, + averageQps, + peakQps: averageQps * input.peakFactor, + readQps: averageQps * (1 - input.writeShare), + writeQps: averageQps * input.writeShare, + storagePerDayGB, + storagePerYearTB: (storagePerDayGB * 365) / 1024, + totalStorageTB: (storagePerDayGB * 365 * input.retentionYears) / 1024, + egressMBps, + peakEgressMBps: egressMBps * input.peakFactor, + }; +} + +/** 12_345_678 → "12.3M"; 950 → "950"; 0.42 → "0.42". */ +export function compact(n: number): string { + if (!Number.isFinite(n)) return '—'; + const abs = Math.abs(n); + if (abs >= 1e12) return `${(n / 1e12).toFixed(1)}T`; + if (abs >= 1e9) return `${(n / 1e9).toFixed(1)}B`; + if (abs >= 1e6) return `${(n / 1e6).toFixed(1)}M`; + if (abs >= 1e4) return `${(n / 1e3).toFixed(1)}K`; + if (abs >= 100) return Math.round(n).toLocaleString('en-US'); + if (abs >= 1) return n.toFixed(1); + return n === 0 ? '0' : n.toFixed(2); +} + +export const PRESETS: Array<{ id: string; label: string; input: CapacityInput }> = [ + { id: 'url-shortener', label: 'URL shortener', input: { dailyActiveUsers: 10_000_000, requestsPerUserPerDay: 10, writeShare: 0.01, peakFactor: 3, responseKB: 0.5, storedPerWriteKB: 0.5, retentionYears: 5, replicas: 3 } }, + { id: 'chat', label: 'Chat app', input: { dailyActiveUsers: 50_000_000, requestsPerUserPerDay: 100, writeShare: 0.4, peakFactor: 2, responseKB: 1, storedPerWriteKB: 0.2, retentionYears: 2, replicas: 3 } }, + { id: 'social-feed', label: 'Social feed', input: { dailyActiveUsers: 100_000_000, requestsPerUserPerDay: 50, writeShare: 0.02, peakFactor: 3, responseKB: 20, storedPerWriteKB: 1, retentionYears: 5, replicas: 3 } }, + { id: 'video', label: 'Video streaming', input: { dailyActiveUsers: 20_000_000, requestsPerUserPerDay: 30, writeShare: 0.001, peakFactor: 2.5, responseKB: 2048, storedPerWriteKB: 500_000, retentionYears: 10, replicas: 3 } }, +]; diff --git a/frontend/next.config.ts b/frontend/next.config.ts index c3234d48..06cc1bcc 100755 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -126,6 +126,12 @@ const nextConfig: NextConfig = { }, async redirects() { return [ + { + // One page for "free" and "AI" architecture diagram generator searches. + source: '/ai-architecture-diagram-generator', + destination: '/free-architecture-diagram-generator', + permanent: true, + }, { source: '/blog', destination: '/blogs',