Skip to content

Commit 1f45fa7

Browse files
mazzma12claude
andauthored
fix(seo): trailing-slash URLs, duplicate consolidation and sitemap lastmod (#1153)
* fix(seo): use trailing slashes in canonical and sitemap URLs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(seo): consolidate duplicate versioned and markdown pages Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(seo): add git-based lastmod to sitemap Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 828abe3 commit 1f45fa7

38 files changed

Lines changed: 149 additions & 85 deletions

File tree

crowdsec-docs/docs/appsec/configuration_creation_testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ inband_rules:
7272

7373
AppSec configuration files declare **which rules to load** in **in-band** *(blocking)* and/or **out-of-band** *(non-blocking)* mode, define how matches are handled (for example, default remediation), and let you tweak processing via hooks like `on_load`, `pre_eval`, `post_eval`, and `on_match`.
7474

75-
For the full list of keys, see [Configuration Syntax](configuration).
75+
For the full list of keys, see [Configuration Syntax](configuration.md).
7676

7777
:::info
7878
When loading multiple AppSec configs, _hooks_ and _appsec rules_ are appended, and for conflicting options (for example, `default_remediation`), the last one takes precedence.

crowdsec-docs/docs/getting_started/install_drupal_plugin.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The CrowdSec Drupal plugin provides real-time protection against malicious actor
88

99
## Overview
1010

11-
The Drupal plugin leverages the [PHP Soft Agent](install_php_softagent) to connect your site to CrowdSec's collaborative security ecosystem. This integration allows your Drupal site to:
11+
The Drupal plugin leverages the [PHP Soft Agent](install_php_softagent.md) to connect your site to CrowdSec's collaborative security ecosystem. This integration allows your Drupal site to:
1212

1313
- **Detect threats**: Automatically identify brute force attacks and scanning attempts
1414
- **Share intelligence**: Contribute attack data to the global CrowdSec network

crowdsec-docs/docs/getting_started/sdk_intro.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Why Integrate the SDK:
1515

1616
## Supported SDKs
1717

18-
* [Python SDK](install_pyagent)
19-
* [PHP SDK](install_php_softagent)
18+
* [Python SDK](install_pyagent.md)
19+
* [PHP SDK](install_php_softagent.md)
2020

2121
Whether you're building a WAF, SIEM, or a custom security tool, the CrowdSec SDKs make it easy to contribute to and benefit from a collaborative defense network.

crowdsec-docs/docs/intro.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ In addition to the core "detect and react" mechanism, CrowdSec is committed to s
5050
Under the hood, the Security Engine has various components:
5151

5252
- The [Log Processor](/log_processor/intro.mdx) handles detection. It analyzes logs from [various data sources](/log_processor/data_sources/introduction.md) and [HTTP requests](/appsec/intro.md) from compatible web servers.
53-
- The [Appsec](appsec/intro) feature is part of the Log Processor. It filters HTTP requests from compatible web servers.
53+
- The [Appsec](appsec/intro.md) feature is part of the Log Processor. It filters HTTP requests from compatible web servers.
5454
- The [Local API](local_api/intro.md) acts as a middleman:
5555
- Between the [Log Processors](/log_processor/intro.mdx) and the [Remediation Components](/u/bouncers/intro) which are in charge of enforcing decisions.
5656
- And with the [Central API](/central_api/intro.md) to share alerts and receive blocklists.
@@ -65,7 +65,7 @@ This architecture supports simple standalone setups and more distributed deploym
6565
- Centralized logs (rsyslog, Loki, ...): [Run CrowdSec next to your log pipeline](/u/user_guides/log_centralization), not on production workloads.
6666
- Kubernetes: See [our Helm chart](/u/getting_started/installation/kubernetes).
6767
- Containers: Use the [Docker data source](/log_processor/data_sources/docker.md).
68-
- WAF only: Start with the [AppSec quickstart](appsec/intro).
68+
- WAF only: Start with the [AppSec quickstart](appsec/intro.md).
6969

7070
Distributed architecture example:
7171

crowdsec-docs/docs/log_processor/scenarios/format.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ It is useful when you want to have further scenarios that will rely on past-over
575575
cache_size: 5
576576
```
577577

578-
By default, a bucket holds [capacity](format#capacity) events "in memory".
578+
By default, a bucket holds [capacity](#capacity) events "in memory".
579579
However, for a number of cases, you don't want this, as it might lead to excessive memory consumption.
580580

581581
By setting `cache_size` to a positive integer, we can control the maximum in-memory cache size of the bucket, without changing its capacity and such. It is useful when buckets are likely to stay alive for a long time or ingest a lot of events to avoid storing a lot of events in memory.

crowdsec-docs/docusaurus.config.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { execSync } from "node:child_process";
12
import type * as Preset from "@docusaurus/preset-classic";
23
import type { NavbarItem } from "@docusaurus/theme-common";
34
import type { Config } from "@docusaurus/types";
@@ -227,10 +228,6 @@ const redirects = [
227228
to: "/u/console/ip_reputation/api_keys",
228229
},
229230
{ from: "/u/cti_api/ip_report", to: "/u/console/ip_reputation/ip_report" },
230-
{
231-
from: "/u/cti_api/search_queries",
232-
to: "/u/console/ip_reputation/search_ui",
233-
},
234231
{
235232
from: "/u/cti_api/advanced_search",
236233
to: "/u/console/ip_reputation/search_ui_advanced",
@@ -264,6 +261,19 @@ function redirectsGlobalDataPlugin() {
264261
};
265262
}
266263

264+
// A shallow clone dates every file to the last commit, which would give every sitemap URL the same <lastmod>.
265+
function hasFullGitHistory(): boolean {
266+
try {
267+
return (
268+
execSync("git rev-parse --is-shallow-repository", { stdio: ["ignore", "pipe", "ignore"] })
269+
.toString()
270+
.trim() === "false"
271+
);
272+
} catch {
273+
return false;
274+
}
275+
}
276+
267277
const config: Config = {
268278
future: {
269279
v4: {
@@ -275,6 +285,8 @@ const config: Config = {
275285
tagline: "CrowdSec - Real-time & crowdsourced protection against aggressive IPs",
276286
url: "https://docs.crowdsec.net",
277287
baseUrl: "/",
288+
// Amplify serves `page/index.html` at `page/` and redirects `page` there, so canonical and sitemap URLs must end with a slash.
289+
trailingSlash: true,
278290
onBrokenLinks: "warn",
279291
onBrokenMarkdownLinks: "warn",
280292
favicon: "img/crowdsec_no_txt.png",
@@ -335,7 +347,7 @@ const config: Config = {
335347
// or nobody who dismissed the previous banner will ever see the new one.
336348
id: "banner_botdetection",
337349
content:
338-
'<a href="/docs/next/appsec/bot_detection/intro">Stop scrapers and headless browsers — discover CrowdSec Bot Detection</a>',
350+
'<a href="/docs/next/appsec/bot_detection/intro/">Stop scrapers and headless browsers — discover CrowdSec Bot Detection</a>',
339351
backgroundColor: "#F8AB13",
340352
textColor: "#131132",
341353
isCloseable: true,
@@ -386,6 +398,10 @@ const config: Config = {
386398
banner: "none",
387399
path: "/",
388400
},
401+
// Near-duplicate of the newer versions: indexing it only splits ranking signals.
402+
"v1.7": {
403+
noIndex: true,
404+
},
389405
// EOL: kept online for existing links, but out of the index.
390406
// plugin-sitemap drops noindex routes too, so it also leaves sitemap.xml.
391407
"v1.6": {
@@ -407,6 +423,16 @@ const config: Config = {
407423
theme: {
408424
customCss: "./src/css/custom.css",
409425
},
426+
sitemap: {
427+
lastmod: hasFullGitHistory() ? "datetime" : null,
428+
changefreq: null,
429+
priority: null,
430+
// Docs of the version served at /docs/ declare their /docs/next/ copy as canonical (src/theme/DocRoot/Layout).
431+
createSitemapItems: async ({ defaultCreateSitemapItems, ...params }) =>
432+
(await defaultCreateSitemapItems(params)).filter(
433+
(item) => !/^\/docs\/(?!next\/|v\d)/.test(new URL(item.url).pathname)
434+
),
435+
},
410436
} satisfies Preset.Options,
411437
],
412438
],

crowdsec-docs/src/theme/DocRoot/Layout/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* This override injects a custom SecondaryNavbar above that content, which adds:
66
* - A breadcrumb trail (Home > Section > [current page path])
77
* - A version dropdown (when multiple doc versions exist)
8+
* On older versions it also points the canonical URL at the current version's copy of the page.
89
*
910
* Docusaurus picks this file automatically because it lives at
1011
* src/theme/DocRoot/Layout/index.tsx, shadowing the original in node_modules.
1112
*/
1213

14+
import Head from "@docusaurus/Head";
1315
import Link from "@docusaurus/Link";
1416
import type { PropSidebarBreadcrumbsItem } from "@docusaurus/plugin-content-docs";
1517
import {
@@ -21,6 +23,8 @@ import {
2123
useVersions,
2224
} from "@docusaurus/plugin-content-docs/client";
2325
import { useHistorySelector } from "@docusaurus/theme-common";
26+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
27+
import { applyTrailingSlash } from "@docusaurus/utils-common";
2428
import { SECTION_MAP } from "@site/src/sectionMap";
2529
import BackToTopButton from "@theme/BackToTopButton";
2630
import type { Props } from "@theme/DocRoot/Layout";
@@ -32,6 +36,21 @@ import React, { type ReactNode, useState } from "react";
3236

3337
import styles from "./styles.module.css";
3438

39+
// Older versions duplicate the current docs; without this, each copy competes for the same searches.
40+
function CanonicalToCurrentDoc({ pluginId }: { pluginId: string }): ReactNode {
41+
const { siteConfig } = useDocusaurusContext();
42+
const { activeVersion, alternateDocVersions } = useActiveDocContext(pluginId);
43+
const currentDoc = alternateDocVersions.current;
44+
if (!activeVersion || activeVersion.name === "current" || !currentDoc) return null;
45+
46+
const path = applyTrailingSlash(currentDoc.path, { trailingSlash: siteConfig.trailingSlash, baseUrl: siteConfig.baseUrl });
47+
return (
48+
<Head>
49+
<link rel="canonical" href={`${siteConfig.url}${path}`} />
50+
</Head>
51+
);
52+
}
53+
3554
function VersionDropdown({ pluginId }: { pluginId: string }): ReactNode {
3655
const versions = useVersions(pluginId);
3756
const activeDocContext = useActiveDocContext(pluginId);
@@ -133,10 +152,12 @@ function SecondaryNavbar(): ReactNode {
133152

134153
export default function DocRootLayout({ children }: Props): ReactNode {
135154
const sidebar = useDocsSidebar();
155+
const activePlugin = useActivePlugin();
136156
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
137157

138158
return (
139159
<div className={styles.docsWrapper}>
160+
<CanonicalToCurrentDoc pluginId={activePlugin?.pluginId ?? "default"} />
140161
<BackToTopButton />
141162
<SecondaryNavbar />
142163
<div className={styles.docRoot}>

crowdsec-docs/src/theme/NotFound/Content/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ export default function NotFoundContent({ className }: Props): ReactNode {
1515

1616
useEffect(() => {
1717
if (!pluginData?.redirects) return;
18-
const path = window.location.pathname.replace(siteConfig.baseUrl.replace(/\/$/, ""), "") || "/";
19-
const match = pluginData.redirects.find((r) => r.from === path);
18+
// Amplify appends a trailing slash before serving the 404 page, while most `from` paths have none.
19+
const withoutTrailingSlash = (p: string) => p.replace(/(.)\/$/, "$1");
20+
const path = withoutTrailingSlash(window.location.pathname.replace(siteConfig.baseUrl.replace(/\/$/, ""), "") || "/");
21+
const match = pluginData.redirects.find((r) => withoutTrailingSlash(r.from) === path);
2022
if (match) {
2123
window.location.replace(match.to);
2224
}

crowdsec-docs/unversioned/cti_api/search_queries.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ It is possible to search any field available in the JSON document below:
5858

5959
We provide all the possible values for each of the following fields:
6060

61-
- [`behaviors`](taxonomy/behaviors)
62-
- [`classifications.classifications`](taxonomy/classifications)
63-
- [`classifications.false_positives`](taxonomy/false_positives)
64-
- [`attack_details`](taxonomy/scenarios)
61+
- [`behaviors`](taxonomy/behaviors.mdx)
62+
- [`classifications.classifications`](taxonomy/classifications.mdx)
63+
- [`classifications.false_positives`](taxonomy/false_positives.mdx)
64+
- [`attack_details`](taxonomy/scenarios.mdx)
6565

6666
To perform a search query, specify the field from the JSON document you wish to search followed by a colon `:` and the value you are looking for between double quotes.
6767

crowdsec-docs/unversioned/tracker_api/api_authentication.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import TabItem from "@theme/TabItem";
1010

1111
To use the Live Exploit Tracker API, you need an **API key**. Contact the CrowdSec team to obtain yours if you haven't already.
1212

13-
The same API key works for both the [web interface](./web_interface) and the REST API.
13+
The same API key works for both the [web interface](./web_interface.mdx) and the REST API.
1414

1515
## Base URL
1616

@@ -33,7 +33,7 @@ curl -X 'GET' \
3333

3434
## SDKs
3535

36-
CrowdSec provides official SDKs for convenient API access with typed models, authentication handling, and a better developer experience. See the [SDKs & Libraries](./api_sdks) page for installation instructions, examples, and the full list of available SDKs.
36+
CrowdSec provides official SDKs for convenient API access with typed models, authentication handling, and a better developer experience. See the [SDKs & Libraries](./api_sdks.mdx) page for installation instructions, examples, and the full list of available SDKs.
3737

3838
**Quick start with the Python SDK:**
3939

@@ -66,9 +66,9 @@ The API returns standard HTTP status codes:
6666

6767
## Next Steps
6868

69-
- [Explore CVEs](./api_cves) — List, search, and get detailed intelligence
70-
- [Fingerprint Rules](./api_fingerprints) — Monitor product-level probing activity
71-
- [Integrations & Blocklists](./api_integrations) — Create firewall integrations and subscribe to CVEs
72-
- [Browse by Vendor, Product, or Tag](./api_lookups) — Explore the coverage landscape
73-
- [SDKs & Libraries](./api_sdks) — Official SDKs for Python and more
69+
- [Explore CVEs](./api_cves.mdx) — List, search, and get detailed intelligence
70+
- [Fingerprint Rules](./api_fingerprints.mdx) — Monitor product-level probing activity
71+
- [Integrations & Blocklists](./api_integrations.mdx) — Create firewall integrations and subscribe to CVEs
72+
- [Browse by Vendor, Product, or Tag](./api_lookups.mdx) — Explore the coverage landscape
73+
- [SDKs & Libraries](./api_sdks.mdx) — Official SDKs for Python and more
7474
- [API Reference](https://admin.api.crowdsec.net/v1/docs#tag/Cves) — Full interactive API documentation

0 commit comments

Comments
 (0)