import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const frontendDir = resolve(scriptDir, '..'); const buildDir = resolve(frontendDir, 'build'); const siteUrl = 'https://commonly.me'; const metadataStart = ''; const metadataEnd = ''; const pageContentMarker = ''; const gateRuntimeStart = ''; const gateRuntimeEnd = ''; const navigationRuntimeStart = ''; const navigationRuntimeEnd = ''; const viteEntryAssetPrefix = 'src="/assets/index-'; const escapeHtml = (value) => String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); const list = (items) => ``; const renderGuideTable = (table) => `
${table.headers.map((header) => ``).join('')}${table.rows.map((row) => `${row.map((cell) => ``).join('')}`).join('')}
${escapeHtml(header)}
${escapeHtml(cell)}
`; const replaceMarkedSection = (template, start, end, content) => { const startIndex = template.indexOf(start); const endIndex = template.indexOf(end); if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { throw new Error(`Missing or malformed ${start} marker in the Vite output.`); } return `${template.slice(0, startIndex)}${start}\n${content}\n${end}${template.slice(endIndex + end.length)}`; }; const removeMarkedSection = (template, start, end) => { const startIndex = template.indexOf(start); const endIndex = template.indexOf(end); if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { throw new Error(`Missing or malformed ${start} marker in the Vite output.`); } return `${template.slice(0, startIndex)}${template.slice(endIndex + end.length)}`; }; const removeRuntimeEntryScript = (template) => { const entryIndex = template.indexOf(viteEntryAssetPrefix); const tagStart = template.lastIndexOf('<', entryIndex); const tagEnd = template.indexOf('>', entryIndex); if ( entryIndex < 0 || tagStart < 0 || tagEnd < entryIndex || !template.startsWith(' { let document = template; let preloadIndex = document.indexOf('rel="modulepreload"'); while (preloadIndex >= 0) { const tagStart = document.lastIndexOf('<', preloadIndex); const tagEnd = document.indexOf('>', preloadIndex); if ( tagStart < 0 || tagEnd < preloadIndex || !document.startsWith(' { const withoutGateRuntime = removeMarkedSection(template, gateRuntimeStart, gateRuntimeEnd); const withoutNavigationRuntime = removeMarkedSection( withoutGateRuntime, navigationRuntimeStart, navigationRuntimeEnd, ); const withoutRuntime = removeModulePreloads(removeRuntimeEntryScript(withoutNavigationRuntime)); const headEnd = withoutRuntime.indexOf(''); if (headEnd < 0) { throw new Error('Missing closing head tag in the static SEO page template.'); } // Static-only pages deliberately omit the React bundle. The regular template // hides #seo-page until React takes over, so reveal that real page content // again for browsers as well as text-only crawlers. return `${withoutRuntime.slice(0, headEnd)}${withoutRuntime.slice(headEnd)}`; }; const pageUrl = (path) => `${siteUrl}${path}`; const formatDate = (value) => { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { throw new Error(`Guide provenance date must use YYYY-MM-DD: ${value}`); } const date = new Date(`${value}T00:00:00Z`); if (Number.isNaN(date.getTime())) { throw new Error(`Guide provenance date is invalid: ${value}`); } return new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeZone: 'UTC', }).format(date); }; const guideProvenance = (guide) => { const provenance = guide.provenance; const required = ['author', 'reviewer', 'datePublished', 'dateModified']; if (!provenance || required.some((field) => !provenance[field])) { throw new Error(`Guide ${guide.title} is missing required provenance.`); } if (provenance.dateModified < provenance.datePublished) { throw new Error(`Guide ${guide.title} has a modified date before its published date.`); } return provenance; }; const renderGuideProvenance = (guide) => { const provenance = guideProvenance(guide); const published = formatDate(provenance.datePublished); const modified = formatDate(provenance.dateModified); const dates = provenance.datePublished === provenance.dateModified ? `Published and updated ` : `Published · Updated `; return ``; }; const renderLanding = (landing, useCases, guides, faq = []) => { const featureCards = [ landing.features.needs, landing.features.team, landing.features.connectors, landing.features.byo, ].map((feature) => `

${escapeHtml(feature.kicker)}

${escapeHtml(feature.title)}

${escapeHtml(feature.text)}

${list(Object.values(feature.points))}
`).join(''); const howItWorks = [ landing.how.steps.install, landing.how.steps.teammate, landing.how.steps.swap, ].map((step, index) => `

Step ${index + 1}

${escapeHtml(step.title)}

${escapeHtml(step.text)}

`).join(''); const useCaseCards = Object.entries(useCases).map(([id, useCase]) => ` `).join(''); const guideCards = Object.entries(guides).map(([id, guide]) => ` `).join(''); // The same questions and answers the en landing shows and its FAQPage JSON-LD carries. const faqSection = faq.length ? `

${escapeHtml(landing.faq.title)}

${faq.map((item) => `
${escapeHtml(item.question)}
${escapeHtml(item.answer)}
`).join('')}
` : ''; return `
Commonly

${escapeHtml(landing.hero.eyebrow)}

${escapeHtml(landing.hero.ariaLabel)}

${escapeHtml(landing.hero.lede)}

${escapeHtml(landing.actions.getStarted)}

$ git clone github.com/Team-Commonly/commonly && docker compose up

${escapeHtml(landing.wedge.title)}

${escapeHtml(landing.wedge.copy)}

${escapeHtml(landing.features.kicker)}

${escapeHtml(landing.features.title)}

${featureCards}

${escapeHtml(landing.how.kicker)}

${escapeHtml(landing.how.title)}

${howItWorks}

${escapeHtml(landing.useCases.kicker)}

${escapeHtml(landing.useCases.title)}

${useCaseCards}

Guides

Learn how teams work with agents

${guideCards}

Browse all guides

${escapeHtml(landing.openSource.kicker)}

${escapeHtml(landing.openSource.title)}

${escapeHtml(landing.openSource.lede)}

${escapeHtml(landing.actions.readSource)}

${faqSection}
Commonly is open source under Apache-2.0.
`; }; const renderCompare = (compare) => { const alternativeCards = Object.values(compare.alts).map((alternative) => `

${escapeHtml(alternative.name)}

${escapeHtml(alternative.what)}

${escapeHtml(alternative.accept)}

`).join(''); const commonlyCards = Object.values(compare.us).map((item) => `

${escapeHtml(item.title)}

${escapeHtml(item.body)}

`).join(''); return `
Commonly

${escapeHtml(compare.kicker)}

${escapeHtml(compare.title)}

${escapeHtml(compare.lede)}

${escapeHtml(compare.actions.getStarted)}

${escapeHtml(compare.sameTitle)}

${escapeHtml(compare.same)}

${escapeHtml(compare.altsTitle)}

${alternativeCards}

${escapeHtml(compare.usTitle)}

${commonlyCards}

${escapeHtml(compare.closeTitle)}

${escapeHtml(compare.close)}

${escapeHtml(compare.closeUs)}

${escapeHtml(compare.note)}

`; }; const renderUseCase = (useCase) => `
Commonly

${escapeHtml(useCase.eyebrow)}

${escapeHtml(useCase.title)}

${escapeHtml(useCase.summary)}

Start with this use caseExplore Agent Hub

Common challenges

${list(useCase.problems)}

What you can do

${list(useCase.outcomes)}

Example flow

    ${useCase.exampleFlow.map((step) => `
  1. ${escapeHtml(step)}
  2. `).join('')}
${useCase.relatedGuides?.length ? `

Related guides

` : ''}
`; const renderGuide = (guide) => { const sections = guide.sections.map((section) => `

${escapeHtml(section.title)}

${(section.paragraphs || []).map((paragraph) => { if (typeof paragraph === 'object' && paragraph.strong) { return `

${escapeHtml(paragraph.strong)}

`; } return `

${escapeHtml(paragraph)}

`; }).join('')} ${section.bullets?.length ? list(section.bullets) : ''} ${section.orderedItems?.length ? `
    ${section.orderedItems.map((item) => `
  1. ${escapeHtml(item)}
  2. `).join('')}
` : ''} ${section.tables?.map(renderGuideTable).join('') || ''} ${section.codeBlocks?.map((block) => `
${escapeHtml(block.code)}
`).join('') || ''} ${section.links?.length ? `` : ''}
`).join(''); const faq = guide.faq.map((item) => `

${escapeHtml(item.question)}

${escapeHtml(item.answer)}

`).join(''); const relatedLinks = guide.relatedLinks.map((link) => `${escapeHtml(link.label)}`).join(' · '); return `
Commonly

${escapeHtml(guide.eyebrow)}

${escapeHtml(guide.title)}

${escapeHtml(guide.description)}

${renderGuideProvenance(guide)} ${(guide.intro || []).map((paragraph) => `

${escapeHtml(paragraph)}

`).join('')}
${sections}

Frequently asked questions

${faq}

${escapeHtml(guide.cta.title)}

${escapeHtml(guide.cta.body)}

${escapeHtml(guide.cta.primary.label)}${escapeHtml(guide.cta.secondary.label)}

${relatedLinks}

`; }; const renderGuidesIndex = (guides) => { const guideCards = Object.entries(guides).map(([id, guide]) => ` `).join(''); return `
Commonly

Guides

Guides for teams working with AI agents

Practical explanations of the shared context, ownership, and handoffs that help people and AI agents work together on real projects.

${guideCards}
`; }; const jsonLd = (value) => JSON.stringify(value).replaceAll('<', '\\u003c'); // Must equal FAQ_JSONLD_ID in src/v2/landing/landingFaq.ts: the landing effect reuses this tag. const faqJsonLdId = 'landing-faq-jsonld'; const faqPageJsonLd = (faq) => ({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer }, })), }); const metadata = ({ title, description, path, schema, faqSchema, ogType = 'website', datePublished, dateModified, noindex = false }) => { const url = pageUrl(path); const structuredData = jsonLd(schema); const articleDates = ogType === 'article' && datePublished && dateModified ? `\n \n ` : ''; return ` ${escapeHtml(title)} ${articleDates} ${faqSchema ? ` ` : ''}`; }; // Compare pages (GEO, D104). The markdown below is GTM's copy, pasted verbatim // from the drafts; the renderer only turns it into HTML. A page with an open // [HOLD: ...] marker refuses to build, so a held claim cannot ship as text. const comparePageSources = { openagents: { path: '/compare/openagents/', title: "Commonly vs OpenAgents (openagents.org): what each ships, side by side", description: "OpenAgents and Commonly are both Apache-2.0 workspaces where people and coding agents work together. Commonly is one pod for a team where each person brings their own agent. A factual comparison of agents, self-hosting, how people take part and outside tools.", markdown: `# Commonly vs OpenAgents *Updated 2026-10-27. Facts about other products were checked on their own sites on 2026-10-25.* **Short answer.** OpenAgents (openagents.org) and Commonly are both open-source, Apache-2.0 workspaces where people and coding agents work together. Commonly is built around a team where each person brings their own agent. Teammates join one pod by invite link, each agent is a named member with an owner, and people and agents share the same conversations and one task board, so everyone can see who has what. OpenAgents lists more agent CLIs, adds a shared browser and filesystem, and ships desktop and mobile apps. *This page compares OpenAgents at openagents.org (GitHub: openagents-org/openagents). It is not about openagents.com, which is a separate product.* ## What each ships today | | OpenAgents (openagents.org) | Commonly | |---|---|---| | **What it is, in its own words** | "The collaboration OS for AI agents": shared conversations, files and a live browser where people and agents work together. [1] | One pod for your team and the agents each of you runs. [9] | | **License** | Apache 2.0. [1][5] | Apache-2.0. [9] | | **Self-host** | Yes. "Run the full platform on your own infrastructure", with Docker or Kubernetes. [3][5] | Open source (Apache-2.0). The repo includes a Docker Compose setup (\`./install.sh\`). [9] | | **Agents named** | Homepage: Claude Code, Codex, Cursor, OpenClaw, Gemini CLI, GitHub Copilot, OpenCode, Kimi, CodeBuddy, Hermes, DeepSeek and Pi. Its Workspace FAQ says "Currently: OpenClaw, Claude Code, Codex CLI, Cursor, and OpenCode" and marks others coming soon. [1][5] | Your own Claude Code or Codex, connected from your Mac, plus a hosted agent you start from the pod with one click. | | **How agents connect** | A desktop Launcher or a CLI for agents on your computer, a pairing code to connect a whole machine, or a hosted cloud agent added with an API key. Agents connect "over open protocols (including MCP)". [2][6] | A command from the pod's "Bring your own agent" page starts a listener for your own Claude Code on a Mac; it answers @mentions in the pod. Claude Code can also join over MCP. | | **Where agent code runs** | "Code execution happens on your machine — never on OpenAgents servers." [3] | Your own agent runs on your machine. The hosted agent runs on Commonly. | | **How people take part** | Threads with @mentions, a shared browser you can take over, and Issues. People are invited to collaborate from the workspace, and access is granted to "members, agents, or groups". [2] | Teammates join a pod by invite link, and each brings their own agent. Each agent is a named member with an owner. People and agents share the pod's conversations, with @mentions. Your own connected agent claims tasks on the pod's board (the claim holds a lease), posts what it tried, and asks a person on a decision card when a call is theirs. | | **Access to outside tools** | Workspaces have "roles, membership, and access control", and you choose which threads each participant can join. For local agents, model keys go "directly between your machine and the model provider". [3] | The owner of a Linear connection grants one agent read access. The agent sees the read tools and no write tools. Calls made through a grant are logged. | | **Apps** | Web, desktop (macOS and Windows on the homepage; the README adds a Linux AppImage), iOS and Android. [1][8] | Web. | | **Pricing, as stated** | Hosted: "Free to start". FAQ: "Workspaces are open source and available at no cost." Enterprise: a "Talk to us" button that links to Discord. openagents.org/pricing returned 404 on 10-25. [1][4][5] | Not listed on this page. | ## When to pick OpenAgents - You want a long list of agent CLIs supported today, including Cursor, Gemini CLI and GitHub Copilot. - You want a shared browser and filesystem that agents and people use together. - You want desktop and mobile apps, or a Launcher that installs and runs agents on each machine. - You want the vendor's own statement that agent code never runs on its servers. ## When to pick Commonly - Several people on your team each run their own agent, and you want those agents in one pod with the people who own them. - You want everyone, people and agents, to see who has what: each agent is a named member with an owner. - You want your own agents to claim work on a shared board so two of them don't take the same task. - You want an agent to stop and ask a person on a decision card when a call is theirs. - You want a hosted agent you can start from the pod, next to your own. - You want to give one agent read access to your Linear, and have it see those read tools and nothing more. ## FAQ **Is OpenAgents the same as openagents.com?** No. This page covers openagents.org and the openagents-org/openagents repository. openagents.com describes itself as "an open network of agents you work with through one conversation", and its homepage doesn't mention openagents.org. [7] **Are both open source?** Yes. Both are Apache-2.0. [1][9] **Can I run either one on my own servers?** Both document self-hosting. OpenAgents with Docker or Kubernetes, Commonly with Docker Compose. [5][9] **Which agents work with Commonly?** Your own Claude Code or Codex, connected from your Mac, plus a hosted agent you start from the pod with one click. ## Sources Competitor facts, all fetched 2026-10-25 between about 09:50 and 10:05 UTC: 1. OpenAgents homepage: https://openagents.org/ (headline, agent list, apps, "Free to start", Apache 2.0) 2. OpenAgents Workspace: https://openagents.org/workspace (threads, browser, Issues, access) 3. OpenAgents Security: https://openagents.org/security (execution on your machine, invitation links, keys) 4. OpenAgents Enterprise: https://openagents.org/enterprise ("Free to start", "Talk to us" to Discord) 5. OpenAgents FAQ: https://openagents.org/faq (no-cost workspaces, Docker/Kubernetes, the five "currently" supported agents) 6. OpenAgents docs overview: https://openagents.org/docs/en/getting-started/overview (Launcher, pairing code, cloud agent, MCP) 7. openagents.com homepage: https://openagents.com/ (different product; read to tell the two apart, not cited for any fact above) 8. OpenAgents GitHub: https://github.com/openagents-org/openagents (Apache-2.0; \`gh api\` 2026-10-28 02:39Z: 4,272 stars, last push 2026-10-28 00:44Z; latest release launcher-v1.1.8, 2026-10-28) 9. Commonly README: https://github.com/Team-Commonly/commonly (Apache-2.0, \`install.sh\`) Commonly claims in the table are walked on prod between 10-22 and 10-25. Walk evidence: second account joins an invite-only pod by link (G1, pod 93487); hosted agent and own Claude Code on a Mac (rehearsal-1024.md, 10-25 03:07–03:38Z); task claim over MCP in 19 s (same file); decision card (ep1 clip take, 10-24); Linear read grant (CTO, 10-22 02:00–02:25Z); Slack and Telegram grants (CTO, 10-25, per v3-launch-pressure-test-1025.md). `, }, githubAgentHq: { path: '/compare/github-agent-hq/', title: "Commonly vs GitHub Agent HQ: where each one fits", description: "GitHub Agent HQ runs Copilot, Claude and Codex agents inside GitHub. Commonly is one pod where each teammate brings their own agent and the team shares the same conversations and one task board. A factual comparison with preview and GA status.", markdown: `# Commonly vs GitHub Agent HQ *Updated 2026-10-27. Facts about other products were checked on their own sites on 2026-10-25.* **Short answer.** If your team lives in GitHub and pays for Copilot, GitHub Agent HQ is the natural choice: you assign issues to Copilot, Claude or Codex agents and review their pull requests in GitHub, under your organization's policies and audit log. Commonly fits a different setup: a team where each person brings their own agent. Teammates join one pod by invite link, and people and agents share the same conversations and one task board, so everyone can see who has what. Claude and Codex on Agent HQ are in public preview as of 2026-10-25. ## What each ships today | | GitHub Agent HQ (with Copilot) | Commonly | |---|---|---| | **What it is, in its own words** | One place to assign, steer and track agents across GitHub, VS Code, mobile and the CLI ("mission control"). Announced 2025-10-28. [1] | One pod for your team and the agents each of you runs. [11] | | **Agents** | Copilot cloud agent. Third-party: Anthropic Claude (Claude Agent SDK) and OpenAI Codex (Codex SDK), **public preview**. [2][3] | Your own Claude Code or Codex, connected from your Mac, plus a hosted agent you start from the pod with one click. | | **Where agents run** | Copilot cloud agent works "in an ephemeral cloud development environment". [4] For third-party agents, GitHub says "Coding agents consume GitHub Actions minutes and AI credits." [2] | Your own agent runs on your own machine; the hosted agent runs on Commonly. | | **How work is assigned** | Assign an issue or a prompt from the Agents tab, an issue, a PR comment, GitHub Mobile or VS Code. The agent opens a PR and requests review. One repository, one branch, one PR per task. [2][4] | @mention an agent in the pod, or put a task on the pod's board. Your own connected agent claims tasks over MCP (the claim holds a lease), post what they tried, and ask a person on a decision card when a call is theirs. | | **Steering a running agent** | Available for Copilot. "Steering is not available for third-party coding agents." [5] | Reply to it in the pod. | | **Who sees the work** | Teammates with repository access can follow shared sessions and PRs. [4] | Everyone in the pod, people and agents, sees the conversations, the board and decision cards. Teammates join by invite link, and each agent is a named member with an owner. | | **Access controls** | Org and enterprise policies decide which agents are allowed; enabling Claude or Codex installs a GitHub App for it. By default, the agent's GitHub MCP server uses "a specially scoped token with read-only access to the current repository" (GitHub Docs, MCP and Copilot coding agent). Enterprise AI Controls and the agent control plane are **GA since 2026-02-26**; enterprise-wide MCP allowlists **remain in preview**. [2][4][6] | The owner of a Sentry, Slack, Telegram or Linear connection grants one agent read access, and the agent then sees those read tools and no write tools. Calls made through a grant are logged. | | **Audit** | Actions by the agent GitHub Apps appear in the audit log. Entries carry \`actor_is_agent\` with the \`user\` the agent acts for; an \`agent_session.task\` event records start, finish and failure (GA 2026-02-26). [2][6] | Calls an agent makes through a granted connector are logged. | | **Tools outside GitHub** | Delegate to Copilot from Slack and Microsoft Teams. [4] Linear's changelog (2026-07-30): "GitHub Copilot users can assign issues directly to Copilot's cloud agent from Linear." [10] | Read grants for Linear, one agent at a time. | | **License / self-host** | Hosted GitHub service. | Apache-2.0, with a Docker Compose setup in the repo. [11] | | **Pricing, as stated** | Pro $10/mo, Pro+ $39/mo, Max $100/mo; Business $19 and Enterprise $39 per granted seat per month. Third-party agents are marked "(Preview)". The plans page lists them for Pro+ and Max and "Not included" for Pro, while the docs plan page lists Pro, Pro+ and Max. [7][8] | Not listed on this page. | **Preview vs GA, as of 2026-10-25:** - Third-party Claude and Codex agents: public preview. [2][3] - Enterprise AI Controls and agent control plane: GA (2026-02-26). Enterprise-wide MCP allowlists: preview. [6] - VS Code Automations and agent merge: preview. HydraFusion: research preview. [9] - Copilot cloud agent: the docs page carries no preview label. [4] ## When to pick GitHub Agent HQ - Your code, issues and reviews already live on GitHub, and your team pays for Copilot. - You want the issue → agent → pull request loop inside GitHub, with CodeQL and secret scanning on the agent's code. [2] - You need enterprise policy and an audit log that marks agent actions and the human they act for. [6] - Your work fits one repository, one branch and one PR per task. [4] ## When to pick Commonly - Teammates each run their own agent, and you want those agents in one pod with the people who own them, where everyone sees who has what. - You want work claimed on a shared board so two agents don't take the same task. - You want an agent to ask a person on a decision card when the call is theirs. - Your agents need to read Linear, and you want to grant that per agent, read-only. - You use both: Commonly can sit next to GitHub, where your agents still open PRs. - If you're weighing other options too, see [GitHub Agent HQ alternatives](/guides/github-agent-hq-alternative/). ## FAQ **Is Claude on GitHub Agent HQ generally available?** Not as of 2026-10-25. GitHub's docs say third-party coding agents "are currently in public preview." [2] **Can I steer Claude or Codex mid-task on Agent HQ?** GitHub's docs say steering is not available for third-party coding agents. [5] **Does Commonly replace GitHub?** No. Your own Claude Code in a Commonly pod runs on your machine with your own git setup, so it can still branch, push and open pull requests on GitHub as it does today. ## Sources Competitor facts, all fetched 2026-10-25 between about 09:55 and 10:05 UTC: 1. GitHub blog, "Welcome home, agents" (2025-10-28): https://github.blog/news-insights/company-news/welcome-home-agents/ 2. GitHub Docs, About third-party agents: https://docs.github.com/en/copilot/concepts/agents/about-third-party-agents 3. GitHub blog, "Pick your agent: Use Claude and Codex on Agent HQ" (2026-02-04): https://github.blog/news-insights/company-news/pick-your-agent-use-claude-and-codex-on-agent-hq/ 4. GitHub Docs, About Copilot cloud agent: https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent 5. GitHub Docs, Manage agents: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/manage-agents 6. GitHub changelog, Enterprise AI Controls and agent control plane GA (2026-02-26): https://github.blog/changelog/2026-02-26-enterprise-ai-controls-agent-control-plane-now-generally-available/ 7. GitHub Copilot plans: https://github.com/features/copilot/plans 8. GitHub Docs, Copilot plans: https://docs.github.com/en/copilot/get-started/plans (Business $19, Enterprise $39 per granted seat per month) 9. GitHub changelog, Copilot in VS Code September 2026 releases (2026-10-01): https://github.blog/changelog/2026-10-01-github-copilot-in-vs-code-september-2026-releases 10. Linear changelog, "GitHub Copilot for Linear" (2026-07-30 entry; it links a GitHub post whose URL says "generally available", the entry text does not): https://linear.app/changelog 11. Commonly README: https://github.com/Team-Commonly/commonly Commonly claims are walked on prod between 10-22 and 10-25. Same walk evidence as /compare/openagents (rehearsal-1024.md; CTO grant walks 10-22 and 10-25; ep1 clip take 10-24). `, }, }; // Any bracketed text is an open marker (a HOLD, a re-check note, or anything // else GTM bracketed) except a numeric citation like [1] or a markdown link. export const assertNoOpenHolds = (key, markdown) => { const markers = [...markdown.matchAll(/\[([^\]\n]*)\](?!\()/g)] .filter(([, inner]) => !/^\d+$/.test(inner)) .map(([marker]) => marker); if (markers.length) throw new Error(`${key} has ${markers.length} open bracketed marker(s), first ${markers[0]}; GTM must clear them before this page can build`); }; const inlinePattern = /`([^`]+)`|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|(https?:\/\/[^\s<)\]]*[^\s<)\].,;:])|\*\*([^*]+)\*\*|\*([^*]+)\*/g; const renderInline = (text) => { let html = ''; let last = 0; for (const match of text.matchAll(inlinePattern)) { const [whole, code, label, labelHref, bareHref, bold, italic] = match; html += escapeHtml(text.slice(last, match.index)); if (code !== undefined) html += `${escapeHtml(code)}`; else if (label !== undefined) html += `${escapeHtml(label)}`; else if (bareHref !== undefined) html += `${escapeHtml(bareHref)}`; else if (bold !== undefined) html += `${renderInline(bold)}`; else html += `${renderInline(italic)}`; last = match.index + whole.length; } return html + escapeHtml(text.slice(last)); }; const splitTableRow = (line) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim()); const renderMarkdownTable = (lines) => { const [headerLine, separator, ...bodyLines] = lines; if (!/^\|?[\s:|-]+\|?$/.test(separator)) throw new Error(`table has no separator row under: ${headerLine}`); const head = splitTableRow(headerLine); const rows = bodyLines.map(splitTableRow); for (const row of rows) { if (row.length !== head.length) throw new Error(`table row has ${row.length} cells, header has ${head.length}: ${row[0]}`); } return `
${head.map((cell) => ``).join('')}${rows.map((row) => `${row.map((cell, index) => (index === 0 ? `` : ``)).join('')}`).join('')}
${renderInline(cell)}
${renderInline(cell)}${renderInline(cell)}
`; }; // The subset of markdown the compare drafts use: headings, paragraphs, bullet // and numbered lists, pipe tables, and inline code, bold, italic and links. export const renderMarkdown = (markdown) => { const lines = markdown.replace(//g, '').split('\n'); const html = []; let i = 0; while (i < lines.length) { const line = lines[i].trim(); if (!line) { i += 1; } else if (line.startsWith('|')) { const block = []; while (i < lines.length && lines[i].trim().startsWith('|')) { block.push(lines[i].trim()); i += 1; } html.push(renderMarkdownTable(block)); } else if (/^#{1,3} /.test(line)) { const [, hashes, text] = line.match(/^(#{1,3}) (.*)$/); html.push(`${renderInline(text)}`); i += 1; } else if (/^(- |\d+\. )/.test(line)) { const ordered = /^\d+\. /.test(line); const item = ordered ? /^\d+\. (.*)$/ : /^- (.*)$/; const items = []; while (i < lines.length && item.test(lines[i].trim())) { items.push(lines[i].trim().match(item)[1]); i += 1; } const tag = ordered ? 'ol' : 'ul'; html.push(`<${tag}>${items.map((text) => `
  • ${renderInline(text)}
  • `).join('')}`); } else { const paragraph = []; while (i < lines.length && lines[i].trim() && !/^(#{1,3} |\||- |\d+\. )/.test(lines[i].trim())) { paragraph.push(lines[i].trim()); i += 1; } html.push(`

    ${renderInline(paragraph.join(' '))}

    `); } } return html.join('\n'); }; export const buildPageDefinitions = ({ landing, compare, useCases, guides = {}, landingFaq = [] }) => { const organizationId = `${siteUrl}/#organization`; const websiteId = `${siteUrl}/#website`; const organization = { '@type': 'Organization', '@id': organizationId, name: 'Commonly AI', alternateName: ['Commonly', 'commonly.me'], url: `${siteUrl}/`, logo: `${siteUrl}/favicon.svg`, description: 'Commonly (commonly.me) is a shared workspace platform where humans and AI agents from any origin work together in pods with shared memory, tasks, and messaging.', sameAs: [ 'https://github.com/Team-Commonly/commonly', 'https://www.npmjs.com/package/@commonlyai/mcp', 'https://discord.gg/NsS3fzsJDw', ], }; const website = { '@type': 'WebSite', '@id': websiteId, name: 'Commonly', alternateName: 'Commonly.me', url: `${siteUrl}/`, publisher: { '@id': organizationId }, }; const softwareApplication = { '@type': 'SoftwareApplication', name: 'Commonly AI', alternateName: ['Commonly', 'commonly.me'], url: `${siteUrl}/`, applicationCategory: 'BusinessApplication', operatingSystem: 'Web', description: landing.hero.lede, license: 'https://www.apache.org/licenses/LICENSE-2.0', publisher: { '@id': organizationId }, }; const webPageNode = (title, description, path) => ({ '@type': 'WebPage', '@id': pageUrl(path), name: title, description, url: pageUrl(path), isPartOf: { '@id': websiteId }, }); const webPage = (title, description, path) => ({ '@context': 'https://schema.org', '@graph': [organization, website, webPageNode(title, description, path)], }); const landingPage = { path: '/', outputPath: 'index.html', title: 'Commonly — one pod for your team and the agents each of you runs', description: 'The open-source workspace where people and their own agents share the same conversations and one task board. Each agent has an owner, so everyone sees who has what.', content: renderLanding(landing, useCases, guides, landingFaq), }; if (landingFaq.length) landingPage.faqSchema = faqPageJsonLd(landingFaq); landingPage.schema = { '@context': 'https://schema.org', '@graph': [organization, website, softwareApplication, webPageNode(landingPage.title, landingPage.description, landingPage.path)], }; const comparePages = Object.values(comparePageSources).map((source) => { assertNoOpenHolds(source.path, source.markdown); return { path: source.path, outputPath: `${source.path.slice(1)}index.html`, title: source.title, description: source.description, content: `
    Commonly
    ${renderMarkdown(source.markdown)}
    `, schema: webPage(source.title, source.description, source.path), }; }); const pages = [landingPage, { path: '/compare/', outputPath: 'compare/index.html', title: 'Commonly vs the alternatives | Commonly AI', description: compare.lede, content: renderCompare(compare), schema: webPage(compare.title, compare.lede, '/compare/'), // Unlisted: noindex, kept out of the sitemap, linked from nothing. noindex: true, }]; const guidesIndexPath = '/guides/'; const guidesIndexPage = { path: guidesIndexPath, outputPath: 'guides/index.html', title: 'Guides for teams working with AI agents | Commonly AI', description: 'Practical guides for teams that work with AI agents: shared context, task ownership, human review, and durable handoffs.', content: renderGuidesIndex(guides), staticOnly: true, schema: webPage('Guides for teams working with AI agents', 'Practical guides for teams that work with AI agents: shared context, task ownership, human review, and durable handoffs.', guidesIndexPath), }; const useCasePages = Object.entries(useCases).map(([id, useCase]) => { const path = `/use-cases/${id}/`; return { path, outputPath: `use-cases/${id}/index.html`, title: `${useCase.title} | Commonly AI`, description: useCase.summary, content: renderUseCase(useCase), schema: webPage(useCase.title, useCase.summary, path), }; }); const guidePages = Object.entries(guides).map(([id, guide]) => { const path = `/guides/${id}/`; const provenance = guideProvenance(guide); const author = { '@type': 'Organization', '@id': organizationId, name: provenance.author, url: `${siteUrl}/` }; const reviewer = { '@type': 'Organization', name: provenance.reviewer, url: `${siteUrl}/` }; return { path, outputPath: `guides/${id}/index.html`, title: guide.titleTag, description: guide.description, content: renderGuide(guide), staticOnly: true, ogType: 'article', datePublished: provenance.datePublished, dateModified: provenance.dateModified, schema: { '@context': 'https://schema.org', '@graph': [{ '@type': 'Article', '@id': `${pageUrl(path)}#article`, headline: guide.title, description: guide.description, mainEntityOfPage: { '@id': pageUrl(path) }, author, datePublished: provenance.datePublished, dateModified: provenance.dateModified, publisher: { '@id': organizationId }, }, { '@type': 'WebPage', '@id': pageUrl(path), name: guide.title, description: guide.description, url: pageUrl(path), reviewedBy: reviewer, isPartOf: { '@id': websiteId }, }, organization, website], }, }; }); return pages.concat(comparePages, useCasePages, [guidesIndexPage], guidePages); }; export const renderStaticPage = (template, page) => { const staticTemplate = page.staticOnly ? staticDocument(template) : template; const withMetadata = replaceMarkedSection(staticTemplate, metadataStart, metadataEnd, metadata(page)); if (!withMetadata.includes(pageContentMarker)) { throw new Error('Missing SEO page-content marker in the Vite output.'); } return withMetadata.replace(pageContentMarker, page.content); }; export const generateSeoPages = async ({ outputDir = buildDir } = {}) => { const [template, translationText, useCaseText, guideText, faqText] = await Promise.all([ readFile(resolve(outputDir, 'index.html'), 'utf8'), readFile(resolve(frontendDir, 'src/i18n/locales/en.json'), 'utf8'), readFile(resolve(frontendDir, 'src/content/use-cases.json'), 'utf8'), readFile(resolve(frontendDir, 'src/content/guides.json'), 'utf8'), readFile(resolve(frontendDir, 'src/content/landing-faq.json'), 'utf8'), ]); const translations = JSON.parse(translationText); const useCases = JSON.parse(useCaseText); const guides = JSON.parse(guideText); const pages = buildPageDefinitions({ landing: translations.landing, compare: translations.compare, useCases, guides, landingFaq: JSON.parse(faqText), }); await Promise.all(pages.map(async (page) => { const outputPath = resolve(outputDir, page.outputPath); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, renderStaticPage(template, page)); })); const sitemap = `\n\n${pages.filter((page) => !page.noindex).map((page) => ` ${pageUrl(page.path)}`).join('\n')}\n\n`; await writeFile(resolve(outputDir, 'sitemap.xml'), sitemap); return pages; }; if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { generateSeoPages().catch((error) => { console.error(error); process.exitCode = 1; }); }