Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions scripts/docs-site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
Expand Down Expand Up @@ -479,7 +491,7 @@ function siteFooter(analyticsEligible = false) {
<section class="site-footer-brand" aria-label="OpenClaw">
<a class="site-footer-logo" href="https://openclaw.ai">OpenClaw</a>
<p class="site-footer-note">An <a href="https://openclaw.org">OpenClaw Foundation</a> project.</p>
<nav class="site-footer-socials" aria-label="Community">${socials.map(([label, href, iconName]) => `<a href="${escapeAttr(href)}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}" target="_blank" rel="noopener">${icon(iconName)}</a>`).join("")}</nav>
<nav class="site-footer-socials" aria-label="Community">${socials.map(([label, href, iconName]) => `<a href="${escapeAttr(href)}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}" target="_blank" rel="noopener${iconName === "discord" ? " nofollow" : ""}">${icon(iconName)}</a>`).join("")}</nav>
</section>
${groups.map(([title, links]) => `<nav class="site-footer-links" aria-label="${title}"><h2>${title}</h2>${links.map(([label, href, target]) => `<a href="${escapeAttr(href)}"${target ? previewLinkAttrs(target, label) : ""}>${label}</a>`).join("")}</nav>`).join("")}
</div>
Expand Down Expand Up @@ -522,7 +534,7 @@ function communityInvite(locale) {
<p class="community-invite__text">${escapeHtml(strings.communityBody)}</p>
<div class="community-invite__links" dir="ltr">
<a class="community-invite__cta" href="https://www.reddit.com/r/openclaw/" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttr(joinLabel("Reddit"))}" title="${escapeAttr(joinLabel("Reddit"))}">${icon("reddit")}<span>${escapeHtml(strings.communityJoin)}</span></a>
<a class="community-invite__cta" href="https://openclaw.ai/discord?from=docs-community-invite" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttr(joinLabel("Discord"))}" title="${escapeAttr(joinLabel("Discord"))}">${icon("discord")}<span>${escapeHtml(strings.communityJoin)}</span></a>
<a class="community-invite__cta" href="https://openclaw.ai/discord?from=docs-community-invite" target="_blank" rel="noopener noreferrer nofollow" aria-label="${escapeAttr(joinLabel("Discord"))}" title="${escapeAttr(joinLabel("Discord"))}">${icon("discord")}<span>${escapeHtml(strings.communityJoin)}</span></a>
<a class="community-invite__cta" href="https://x.com/openclaw" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttr(followLabel)}" title="${escapeAttr(followLabel)}">${icon("x-social")}<span>${escapeHtml(strings.communityFollow)}</span></a>
</div>
</div>
Expand Down Expand Up @@ -557,7 +569,7 @@ function socialIconLinks(placement) {
["Reddit", "https://www.reddit.com/r/openclaw/", "reddit"],
["Discord", `https://openclaw.ai/discord?from=${placement}`, "discord"],
["X", "https://x.com/openclaw", "x-social"],
].map(([label, href, iconName]) => `<a class="top-icon-link" href="${escapeAttr(href)}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}">${icon(iconName)}</a>`).join("");
].map(([label, href, iconName]) => `<a class="top-icon-link" href="${escapeAttr(href)}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}"${iconName === "discord" ? ' rel="nofollow"' : ""}>${icon(iconName)}</a>`).join("");
}

function firstStatusLine(content) {
Expand Down
14 changes: 14 additions & 0 deletions scripts/docs-site/community-invite.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ test("community artwork opts out of Pinterest without changing the invitation or
[strings.communityJoin, "https://openclaw.ai/discord?from=docs-community-invite"],
[strings.communityFollow, "https://x.com/openclaw"],
]);
const anchors = all(node => node.name === "a");
for (const [tag, rel] of [
["docs-header", "nofollow"], ["docs-sidebar", "nofollow"],
["docs-footer", "noopener nofollow"], ["docs-community-invite", "noopener noreferrer nofollow"],
...(slug === "index" ? [["docs-home-community", "nofollow"]] : []),
]) {
const link = anchors.find(node => node.attribs.href === `https://openclaw.ai/discord?from=${tag}`);
assert.equal(link?.attribs.rel, rel, `${locale}/${slug}: ${tag}`);
}
for (const link of anchors.filter(node => !node.attribs.href?.startsWith("https://openclaw.ai/discord?"))) {
assert.ok(!link.attribs.rel?.split(/\s+/).includes("nofollow"), `${locale}/${slug}: non-invite rel unchanged`);
}
assert.equal(links[0].attribs.rel, "noopener noreferrer");
assert.equal(links[2].attribs.rel, "noopener noreferrer");
const optedOut = all(node => node.attribs && ("nopin" in node.attribs || "data-pin-nopin" in node.attribs));
assert.deepEqual(optedOut, art, `${locale}/${slug}: only invitation images opt out`);
assert.equal(all(node => node.name === "meta" && node.attribs.name === "pinterest").length, 0);
Expand Down
31 changes: 26 additions & 5 deletions scripts/docs-site/discord-article-links.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ const preserved = [
"https://discord.com/api/v10/invites/clawd", "https://discord.gg/clawd?event=123456789012345678",
"https://discord.com/invite/clawd#event", "https://discord.gg/clawd?utm_source=legacy",
"https://discord.gg.example.org/clawd", "https://example.org/?invite=https://discord.gg/clawd",
"https://openclaw.ai/discord?from=docs-plugins-building-plugins", "https://community.openclaw.ai/",
"https://openclaw.ai/discord-extra?from=docs", "https://openclaw.ai.example.org/discord?from=docs", "https://community.openclaw.ai/",
];
const tagged = ["https://openclaw.ai/discord?from=docs-plugins-building-plugins", "https://openclaw.ai/discord/?from=docs&keep=value#join"];

function article(document) {
return DomUtils.findOne(node => node.attribs?.class?.split(" ").includes("doc"), document.children);
}

test("English and localized rendered article anchors use canonical page placements, including cached articles", t => {
test("English and localized invite anchors retain canonical placements and add nofollow, including cached articles", t => {
const body = ["# Community", ...official.map((href, i) => `[Official ${i}](${href})`),
'<a class="raw-link" data-href="https://example.org/unchanged" href=\'https://discord.gg/clawd\' title="Keep href=example title">Raw HTML</a>',
'<a class="raw-link" data-href="https://example.org/unchanged" href=\'https://discord.gg/clawd\' rel="noopener noreferrer" title="Keep href=example title">Raw HTML</a>',
'<a href="https://discord.gg/clawd" rel="ugc NOFOLLOW">Already nofollow</a>',
'<a href="https://discord.gg/clawd" rel>Empty rel</a>',
...tagged.map((href, i) => `[Tagged ${i}](${href})`),
...preserved.map((href, i) => `[Preserve ${i}](${href})`),
"`https://discord.gg/clawd`", "```html\n<a href=\"https://discord.gg/clawd\">Example</a>\n```",
'<p class="display-url">https://discord.gg/clawd</p>', '<code><a href="https://discord.gg/clawd">Code anchor</a></code>',
Expand All @@ -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);
Expand Down
Loading