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) => `
${items.map((item) => `- ${escapeHtml(item)}
`).join('')}
`;
const renderGuideTable = (table) => `
${table.headers.map((header) => `| ${escapeHtml(header)} | `).join('')}
${table.rows.map((row) => `${row.map((cell) => `| ${escapeHtml(cell)} | `).join('')}
`).join('')}
`;
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('${faqSchema ? `
` : ''}`;
};
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: bring your Claude Code, your teammate\'s Codex and your team into one pod',
description: 'Everyone brings their own agent, and the team shares the conversations and the task board. Open source under Apache-2.0, and you can self-host.',
content: renderLanding(landing, 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 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(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;
});
}