diff --git a/scripts/docs-site/build.mjs b/scripts/docs-site/build.mjs index 588b4f6fc5..4b20ea234c 100644 --- a/scripts/docs-site/build.mjs +++ b/scripts/docs-site/build.mjs @@ -329,22 +329,34 @@ function rewriteOfficialDiscordLinks(html, slug) { if (name === "a" && attributes.href && !ancestors.some(tag => ["pre", "code", "script", "style"].includes(tag))) { let url; try { url = new URL(attributes.href); } catch {} - // Queries/fragments may carry event-specific meaning. Keep those, other - // communities, service URLs, and credential-bearing URLs untouched. - if (url && ["http:", "https:"].includes(url.protocol) && !url.username && !url.password - && !url.port && !url.search && !url.hash + const publicUrl = url && ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.port; + // Direct queries/fragments may carry event-specific meaning. Keep those, + // other communities and service URLs untouched; preserve tagged redirects. + const directInvite = publicUrl && !url.search && !url.hash && ((url.hostname === "discord.gg" && /^\/clawd\/?$/.test(url.pathname)) - || (["discord.com", "discordapp.com"].includes(url.hostname) && /^\/invite\/clawd\/?$/.test(url.pathname)))) { + || (["discord.com", "discordapp.com"].includes(url.hostname) && /^\/invite\/clawd\/?$/.test(url.pathname))); + const taggedInvite = publicUrl && url.hostname === "openclaw.ai" && /^\/discord\/?$/.test(url.pathname); + if (directInvite || taggedInvite) { const start = parser.startIndex; const end = parser.endIndex + 1; const opening = html.slice(start, end); - let replaced = false; - const replacement = opening.replace(/(\s+)([^\s"'<>/=]+)(\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/g, + let replacedHref = false; + let replacedRel = false; + const hasNofollow = (attributes.rel ?? "").split(/\s+/).some(token => token.toLowerCase() === "nofollow"); + const rel = [attributes.rel, "nofollow"].filter(Boolean).join(" "); + let replacement = opening.replace(/(\s+)([^\s"'<>/=]+)(?:(\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, (attribute, space, name, equals) => { - if (replaced || name.toLowerCase() !== "href") return attribute; - replaced = true; - return `${space}${name}${equals}"https://openclaw.ai/discord?from=${placement}"`; + if (directInvite && !replacedHref && name.toLowerCase() === "href") { + replacedHref = true; + return `${space}${name}${equals}"https://openclaw.ai/discord?from=${placement}"`; + } + if (!hasNofollow && !replacedRel && name.toLowerCase() === "rel") { + replacedRel = true; + return `${space}${name}="${escapeAttr(rel)}"`; + } + return attribute; }); + if (!hasNofollow && !replacedRel) replacement = replacement.replace(/(\s*\/?>)$/, ` rel="nofollow"$1`); edits.push({ start, end, replacement }); } } @@ -479,7 +491,7 @@ function siteFooter(analyticsEligible = false) { ${groups.map(([title, links]) => ``).join("")} @@ -522,7 +534,7 @@ function communityInvite(locale) {
${escapeHtml(strings.communityBody)}
https://discord.gg/clawd
', 'Code anchor',
@@ -45,15 +49,32 @@ test("English and localized rendered article anchors use canonical page placemen
const doc = article(parseDocument(html));
assert.ok(doc, `${locale}/${slug}`);
const anchors = DomUtils.findAll(node => node.name === "a", doc.children);
- for (let i = 0; i < official.length; i++) assert.equal(anchors.find(node => DomUtils.textContent(node) === `Official ${i}`).attribs.href, `https://openclaw.ai/discord?from=${tag}`);
+ for (let i = 0; i < official.length; i++) {
+ const link = anchors.find(node => DomUtils.textContent(node) === `Official ${i}`);
+ assert.equal(link.attribs.href, `https://openclaw.ai/discord?from=${tag}`);
+ assert.equal(link.attribs.rel, "nofollow");
+ }
const raw = anchors.find(node => node.attribs.class === "raw-link");
assert.equal(raw.attribs.href, `https://openclaw.ai/discord?from=${tag}`);
+ assert.equal(raw.attribs.rel, "noopener noreferrer nofollow");
assert.equal(raw.attribs["data-href"], "https://example.org/unchanged"); assert.equal(raw.attribs.title, "Keep href=example title");
- for (let i = 0; i < preserved.length; i++) assert.equal(anchors.find(node => DomUtils.textContent(node) === `Preserve ${i}`).attribs.href, preserved[i]);
+ assert.equal(anchors.find(node => DomUtils.textContent(node) === "Already nofollow").attribs.rel, "ugc NOFOLLOW");
+ assert.equal(anchors.find(node => DomUtils.textContent(node) === "Empty rel").attribs.rel, "nofollow");
+ for (let i = 0; i < tagged.length; i++) {
+ const link = anchors.find(node => DomUtils.textContent(node) === `Tagged ${i}`);
+ assert.equal(link.attribs.href, tagged[i]);
+ assert.equal(link.attribs.rel, "nofollow");
+ }
+ for (let i = 0; i < preserved.length; i++) {
+ const link = anchors.find(node => DomUtils.textContent(node) === `Preserve ${i}`);
+ assert.equal(link.attribs.href, preserved[i]);
+ assert.equal(link.attribs.rel, undefined);
+ }
const code = DomUtils.findAll(node => node.name === "code", doc.children).map(node => DomUtils.textContent(node));
assert.ok(code.some(text => text.includes('href="https://discord.gg/clawd"')), "fenced code is unchanged");
assert.ok(code.some(text => text === "https://discord.gg/clawd"), "inline code is unchanged");
assert.equal(anchors.find(node => DomUtils.textContent(node) === "Code anchor").attribs.href, "https://discord.gg/clawd");
+ assert.equal(anchors.find(node => DomUtils.textContent(node) === "Code anchor").attribs.rel, undefined);
const display = DomUtils.findOne(node => node.attribs?.class === "display-url", doc.children);
assert.equal(DomUtils.textContent(display), "https://discord.gg/clawd", "display text is not rewritten");
assert.equal(fs.readFileSync(path.join(f.root, "docs", locale === "en" ? "" : locale, slug + ".md"), "utf8"), body);