diff --git a/scripts/docs-site/analytics-behavior.test.mjs b/scripts/docs-site/analytics-behavior.test.mjs index 3bdba94035..0d9df9802a 100644 --- a/scripts/docs-site/analytics-behavior.test.mjs +++ b/scripts/docs-site/analytics-behavior.test.mjs @@ -90,7 +90,7 @@ function wireEvents(collected) { test("selection placement and sanitized destination share one event without changing native clicks", async t => { const destination = "https://discord.com/invite/clawd"; const { page, context, events, collected } = await openSite(t, { - articleLinks: `\n[Community](${destination})\n\n[External reference](https://example.org/guide?ref=docs#intro)\n\n[Product](https://openclaw.ai/install?ref=docs#intro)\n`, + articleLinks: `\n[Community](${destination})\n\n[External reference](https://example.org/guide?ref=docs#intro)\n\n[Product](https://openclaw.ai/install?ref=docs#intro)\n\n[Redirect slash](https://openclaw.ai/discord/?from=docs-home-community&unknown=ignored#join)\n\n[Not Discord](https://openclaw.ai/discord-extra?from=docs-header)\n`, }); page.setDefaultTimeout(5000); // Abort only after observing native navigation so the fixture stays loaded. @@ -152,11 +152,14 @@ test("selection placement and sanitized destination share one event without chan } } } - for (const [selector, placement] of [[".community-invite", "community_invite"], [".site-footer", "footer"], [".doc", "article"]]) { - await clickAndObserve(`${selector} a[href="${destination}"]`, placement, "social_link", "discord", destination, true); + for (const [selector, placement, tag] of [[".site-header", "header", "docs-header"], [".community-invite", "community_invite", "docs-community-invite"], [".site-footer", "footer", "docs-footer"]]) { + await clickAndObserve(`${selector} a[href="https://openclaw.ai/discord?from=${tag}"]`, placement, "social_link", "discord", "https://openclaw.ai/discord"); } + await clickAndObserve(`.doc a[href="${destination}"]`, "article", "social_link", "discord", destination, true); + await clickAndObserve('.doc a[href^="https://openclaw.ai/discord/?"]', "article", "social_link", "discord", "https://openclaw.ai/discord/"); + await clickAndObserve('.doc a[href^="https://openclaw.ai/discord-extra"]', "article", "site_link", "openclaw.ai/discord-extra", "https://openclaw.ai/discord-extra"); await clickAndObserve('.doc a[href^="https://example.org/"]', "article", "public_reference", "https://example.org/guide", "https://example.org/guide"); - await clickAndObserve('.doc a[href^="https://openclaw.ai/"]', "article", "site_link", "openclaw.ai/install", "https://openclaw.ai/install"); + await clickAndObserve('.doc a[href^="https://openclaw.ai/install"]', "article", "site_link", "openclaw.ai/install", "https://openclaw.ai/install"); for (const label of ["Edit source", "Raise issue"]) { const link = page.getByRole("link", { name: label, exact: true }); const url = new URL(await link.getAttribute("href")); @@ -165,15 +168,15 @@ test("selection placement and sanitized destination share one event without chan } const beforePrivate = (await events("select_content")).length; - const nativeBeforePrivate = count("click", destination).length; + const nativeBeforePrivate = count("click", "https://openclaw.ai/discord").length; await page.locator("[data-chat-toggle]").click(); - await page.locator(`.site-footer a[href="${destination}"]`).click(); + await page.locator('.site-footer a[href="https://openclaw.ai/discord?from=docs-footer"]').click(); await page.waitForTimeout(1200); await page.locator("[data-chat-minimize]").click(); await page.waitForTimeout(1200); assert.equal((await events("select_content")).length, beforePrivate, "private selection is not replayed on close"); - assert.equal(count("click", destination).length, nativeBeforePrivate, "private native click is not replayed on close"); - await clickAndObserve(`.site-footer a[href="${destination}"]`, "footer", "social_link", "discord", destination, true); + assert.equal(count("click", "https://openclaw.ai/discord").length, nativeBeforePrivate, "private native click is not replayed on close"); + await clickAndObserve('.site-footer a[href="https://openclaw.ai/discord?from=docs-footer"]', "footer", "social_link", "discord", "https://openclaw.ai/discord"); }); test("selection destination stays absent for same-origin, non-anchor, unknown and blocked contexts", async t => { @@ -205,7 +208,7 @@ test("selection destination stays absent for same-origin, non-anchor, unknown an assert.equal((await events("select_content")).length, before); await setBrowserPrivacy(page, true); const requestsBefore = googleRequests.length; - await page.locator('.site-footer a[href="https://discord.com/invite/clawd"]').click(); + await page.locator('.site-footer a[href="https://openclaw.ai/discord?from=docs-footer"]').click(); await page.waitForTimeout(1200); await setBrowserPrivacy(page, false); assert.equal((await events("select_content")).length, before); diff --git a/scripts/docs-site/analytics-events.mjs b/scripts/docs-site/analytics-events.mjs index 1c4f5cca72..70ce54b3bf 100644 --- a/scripts/docs-site/analytics-events.mjs +++ b/scripts/docs-site/analytics-events.mjs @@ -220,7 +220,8 @@ export function createDocsAnalyticsEvents(analytics) { const destination = url.origin === window.location.origin ? {} : { link_url: url.origin + url.pathname, link_domain: url.hostname }; const artifact = artifactChoice(url); if (link.matches(".search-result")) flushSearch(); - if (socialHosts[url.hostname]) select("social_link", socialHosts[url.hostname], location, destination); + if (url.origin === "https://openclaw.ai" && /^\/discord\/?$/.test(url.pathname)) select("social_link", "discord", location, destination); + else if (socialHosts[url.hostname]) select("social_link", socialHosts[url.hostname], location, destination); else if (canonicalHosts.has(url.hostname) && !/\/ask-molty\/|\/__elements/i.test(url.pathname)) { select(link.matches(".search-result") ? "search_result" : artifact ? "download" : url.hostname === "docs.openclaw.ai" ? "documentation" : "site_link", url.hostname === "docs.openclaw.ai" ? url.pathname : url.hostname + url.pathname, location, { ...artifact, ...destination }); } else if (link.closest(".page-actions")) select("page_action", /chatgpt/.test(url.hostname) ? "ask_chatgpt" : /claude/.test(url.hostname) ? "ask_claude" : "view_markdown", location, destination); diff --git a/scripts/docs-site/build.mjs b/scripts/docs-site/build.mjs index 45e026ac0f..9e4a0faa94 100644 --- a/scripts/docs-site/build.mjs +++ b/scripts/docs-site/build.mjs @@ -405,7 +405,7 @@ function siteHeader(page) {
- +
`; @@ -416,7 +416,7 @@ function siteFooter(analyticsEligible = false) { const socials = [ ["X", "https://x.com/openclaw", "x-social"], ["GitHub", "https://github.com/openclaw/openclaw", "github"], - ["Discord", "https://discord.com/invite/clawd", "discord"], + ["Discord", "https://openclaw.ai/discord?from=docs-footer", "discord"], ["Reddit", "https://www.reddit.com/r/openclaw/", "reddit"], ["TikTok", "https://www.tiktok.com/@open_claw", "tiktok"], ["LinkedIn", "https://www.linkedin.com/company/the-openclaw-foundation", "linkedin"], @@ -452,7 +452,7 @@ ${rewriteInternalUrls(docsQuickNav(page.slug, icon, page.locale), page.locale)} return `
${escapeHtml(tab.title)}${releaseVersionHtml(release, page.locale)}${icon("chevron-down")}
${tab.groups.map((group) => navGroupHtml(page, group)).join("")}
`; }).join("")} `; @@ -476,7 +476,7 @@ function communityInvite(locale) {

${escapeHtml(strings.communityBody)}

@@ -505,11 +505,11 @@ function localeDisplayName(code) { return localePickerLabels[code] ?? localeLabels[code] ?? code; } -function socialIconLinks() { +function socialIconLinks(placement) { return [ ["GitHub", "https://github.com/openclaw/openclaw", "github"], ["Reddit", "https://www.reddit.com/r/openclaw/", "reddit"], - ["Discord", "https://discord.com/invite/clawd", "discord"], + ["Discord", `https://openclaw.ai/discord?from=${placement}`, "discord"], ["X", "https://x.com/openclaw", "x-social"], ].map(([label, href, iconName]) => `${icon(iconName)}`).join(""); } diff --git a/scripts/docs-site/community-invite-visual.mjs b/scripts/docs-site/community-invite-visual.mjs index 27b68fb2e9..953ec11118 100644 --- a/scripts/docs-site/community-invite-visual.mjs +++ b/scripts/docs-site/community-invite-visual.mjs @@ -25,7 +25,7 @@ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const base = `http://127.0.0.1:${server.address().port}`; const expectedLinks = [ ["Join", "https://www.reddit.com/r/openclaw/"], - ["Join", "https://discord.com/invite/clawd"], + ["Join", "https://openclaw.ai/discord?from=docs-community-invite"], ["Follow", "https://x.com/openclaw"], ]; let checked = 0; diff --git a/scripts/docs-site/community-invite.test.mjs b/scripts/docs-site/community-invite.test.mjs index 3c07393eac..0886c06f22 100644 --- a/scripts/docs-site/community-invite.test.mjs +++ b/scripts/docs-site/community-invite.test.mjs @@ -47,7 +47,7 @@ test("community artwork opts out of Pinterest without changing the invitation or const links = DomUtils.findAll(node => node.name === "a", invite.children); assert.deepEqual(links.map(node => [DomUtils.textContent(node).trim(), node.attribs.href]), [ [strings.communityJoin, "https://www.reddit.com/r/openclaw/"], - [strings.communityJoin, "https://discord.com/invite/clawd"], + [strings.communityJoin, "https://openclaw.ai/discord?from=docs-community-invite"], [strings.communityFollow, "https://x.com/openclaw"], ]); const optedOut = all(node => node.attribs && ("nopin" in node.attribs || "data-pin-nopin" in node.attribs)); diff --git a/scripts/docs-site/discord-source-tags.test.mjs b/scripts/docs-site/discord-source-tags.test.mjs new file mode 100644 index 0000000000..f0a2eadf19 --- /dev/null +++ b/scripts/docs-site/discord-source-tags.test.mjs @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { parseDocument, DomUtils } from "htmlparser2"; +import { communitySection } from "./home-sections.mjs"; +import { localeLabels } from "./config.mjs"; + +test("every locale uses the registered home Discord placement without a new acquisition campaign", () => { + for (const locale of Object.keys(localeLabels)) { + const document = parseDocument(communitySection(() => "", locale)); + const link = DomUtils.findOne(node => node.attribs?.class === "home-discord-button", document.children); + const url = new URL(link.attribs.href); + assert.equal(url.origin + url.pathname, "https://openclaw.ai/discord", locale); + assert.deepEqual([...url.searchParams], [["from", "docs-home-community"]], locale); + assert.ok(DomUtils.findOne(node => node.name === "a" && node.attribs.href === "https://community.openclaw.ai/", document.children), locale); + } +}); diff --git a/scripts/docs-site/home-sections.mjs b/scripts/docs-site/home-sections.mjs index f522ca8d3c..9b3be3a6a3 100644 --- a/scripts/docs-site/home-sections.mjs +++ b/scripts/docs-site/home-sections.mjs @@ -20,5 +20,5 @@ export function communitySection(icon, locale = "en") { ['X', xDescription, 'https://x.com/openclaw', 'x-social'], ]; const xLogo = ''; - return `

${escapeUiText(heading)}

${escapeUiText(title)}

${escapeUiText(description)}

${escapeUiText(join)} ${icon('arrow-right')}
${links.map(([title, text, href, glyph]) => `${glyph === 'x-social' ? xLogo : icon(glyph)}

${escapeUiText(title)}${icon('arrow-right')}

${escapeUiText(text)}

`).join('')}
`; + return `

${escapeUiText(heading)}

${escapeUiText(title)}

${escapeUiText(description)}

${escapeUiText(join)} ${icon('arrow-right')}
${links.map(([title, text, href, glyph]) => `${glyph === 'x-social' ? xLogo : icon(glyph)}

${escapeUiText(title)}${icon('arrow-right')}

${escapeUiText(text)}

`).join('')}
`; } diff --git a/scripts/docs-site/preview-build.test.mjs b/scripts/docs-site/preview-build.test.mjs index 9533ea9f89..4a743414c2 100644 --- a/scripts/docs-site/preview-build.test.mjs +++ b/scripts/docs-site/preview-build.test.mjs @@ -47,7 +47,9 @@ test("requested pages outside navigation fit inside the 30-page preview, includi } assert.match(html, /