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
14 changes: 12 additions & 2 deletions .github/workflows/docusaurus_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.ref || github.ref }}
# A release dispatch always builds the trusted default branch. We do not
# honor an externally supplied client_payload.ref, which could otherwise
# point the privileged build (App token + npm ci) at arbitrary code.
ref: ${{ github.event_name == 'repository_dispatch' && github.event.repository.default_branch || github.ref }}
token: ${{ steps.app-token.outputs.token }}

- name: Resolve source branch
Expand Down Expand Up @@ -101,15 +104,22 @@ jobs:

- name: Commit docs version snapshot
if: env.RELEASE_VERSION != ''
env:
SOURCE_BRANCH: ${{ steps.branch.outputs.source_branch }}
run: |
if git diff --quiet -- versions.json versioned_docs versioned_sidebars; then
echo "No versioned docs changes to commit"
exit 0
fi

if [[ ! "${SOURCE_BRANCH}" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "Refusing to push to unexpected branch name: '${SOURCE_BRANCH}'"
exit 1
fi

git add versions.json versioned_docs versioned_sidebars
git commit -m "docs: snapshot ${RELEASE_VERSION}"
git push origin HEAD:${{ steps.branch.outputs.source_branch }}
git push origin "HEAD:refs/heads/${SOURCE_BRANCH}"

- name: Build Docusaurus site
run: npm run build
Expand Down
83 changes: 82 additions & 1 deletion src/components/SiteConsent/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import styles from "./styles.module.css";
const CONSENT_KEY = "maintainerr-docs-consent";
const MATOMO_SCRIPT_ID = "maintainerr-matomo-script";

// Set by the swizzled NotFound page so the next page view is tagged as a 404.
let pendingNotFound = false;

export function flagNotFound() {
pendingNotFound = true;
}

function getStoredConsent() {
if (typeof window === "undefined") {
return null;
Expand Down Expand Up @@ -49,10 +56,32 @@ function trackPageView(location) {

const fullPath = `${location.pathname}${location.search}${location.hash}`;
window._paq.push(["setCustomUrl", fullPath]);
window._paq.push(["setDocumentTitle", document.title]);

if (pendingNotFound) {
pendingNotFound = false;
// Matomo's recommended 404 tagging: title prefixed with "404/URL = ...".
window._paq.push([
"setDocumentTitle",
`404/URL = ${encodeURIComponent(fullPath)}/From = ${encodeURIComponent(
document.referrer,
)}`,
]);
} else {
window._paq.push(["setDocumentTitle", document.title]);
}

window._paq.push(["trackPageView"]);
}

function trackSiteSearch(keyword) {
if (typeof window === "undefined" || !window._paq) {
return;
}

// category and result count are left unset (false) — cookieless, aggregate.
window._paq.push(["trackSiteSearch", keyword, false, false]);
}

export function trackMatomoEvent(category, action, name) {
if (typeof window === "undefined" || !window._paq) {
return;
Expand Down Expand Up @@ -95,6 +124,58 @@ export default function SiteConsent() {
trackPageView(location);
}, [consent, location, matomoConfig.enabled]);

// Track docs-search queries (cookieless). The local search updates results
// as you type, so debounce and also fire on Enter; min length avoids noise.
useEffect(() => {
if (consent !== "accepted" || !matomoConfig.enabled) {
return;
}

const SELECTOR = ".navbar__search-input";
let timer;
let lastTracked = "";

function record(value) {
const keyword = value.trim();
if (keyword.length < 3 || keyword === lastTracked) {
return;
}
lastTracked = keyword;
trackSiteSearch(keyword);
}

function onInput(event) {
const target = event.target;
if (!target.matches || !target.matches(SELECTOR)) {
return;
}
clearTimeout(timer);
const { value } = target;
timer = setTimeout(() => record(value), 1500);
}

function onKeydown(event) {
if (event.key !== "Enter") {
return;
}
const target = event.target;
if (!target.matches || !target.matches(SELECTOR)) {
return;
}
clearTimeout(timer);
record(target.value);
}

document.addEventListener("input", onInput, true);
document.addEventListener("keydown", onKeydown, true);

return () => {
clearTimeout(timer);
document.removeEventListener("input", onInput, true);
document.removeEventListener("keydown", onKeydown, true);
};
}, [consent, matomoConfig.enabled]);

function updateConsent(nextConsent) {
window.localStorage.setItem(CONSENT_KEY, nextConsent);
setConsent(nextConsent);
Expand Down
17 changes: 17 additions & 0 deletions src/theme/NotFound/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { useLayoutEffect } from "react";
import NotFound from "@theme-original/NotFound";
import { useLocation } from "@docusaurus/router";
import { flagNotFound } from "@site/src/components/SiteConsent";

// Wraps the default 404 page so SiteConsent tags the next Matomo page view as
// a 404. useLayoutEffect runs before SiteConsent's (passive) page-view effect,
// so the flag is set in time; keyed on pathname to cover 404 -> 404 navigation.
export default function NotFoundWrapper(props) {
const location = useLocation();

useLayoutEffect(() => {
flagNotFound();
}, [location.pathname]);

return <NotFound {...props} />;
}