From d8173cc190686634cd43f0a83b4f6178a133e9f5 Mon Sep 17 00:00:00 2001
From: Claas Augner <495429+caugner@users.noreply.github.com>
Date: Thu, 18 Apr 2024 18:56:37 +0200
Subject: [PATCH] feat(experiment): rewrite Web/API page titles (#10926)
Problem: We identified a drop in search engine impressions in April 2023, which coincides
with the Web/API retitle project (https://github.com/mdn/mdn/issues/284), and we cannot
rule out that there is a causal relationship.
Solution: Run an experiment with 500 randomly sampled Web/API pages (of 2719 affected),
partially reverting the change to their `
` (but not to the ``) by essentially
replacing `Foo: bar property` with `Foo.bar property`.
---
build/index.ts | 4 +-
build/seo.ts | 526 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 529 insertions(+), 1 deletion(-)
create mode 100644 build/seo.ts
diff --git a/build/index.ts b/build/index.ts
index 0bc3210fce02..ba4a2a08bfbe 100644
--- a/build/index.ts
+++ b/build/index.ts
@@ -44,6 +44,7 @@ import {
postProcessSmallerHeadingIDs,
} from "./utils.js";
import { getWebFeatureStatus } from "./web-features.js";
+import { rewritePageTitleForSEO } from "./seo.js";
export { default as SearchIndex } from "./search-index.js";
export { gather as gatherGitHistory } from "./git-history.js";
export { buildSPAs } from "./spas.js";
@@ -536,7 +537,8 @@ export async function buildDocument(
// a breadcrumb in the React component.
addBreadcrumbData(document.url, doc);
- doc.pageTitle = getPageTitle(doc);
+ const pageTitle = getPageTitle(doc);
+ doc.pageTitle = rewritePageTitleForSEO(doc.mdn_url, pageTitle);
// Decide whether it should be indexed (sitemaps, robots meta tag, search-index)
doc.noIndexing =
diff --git a/build/seo.ts b/build/seo.ts
new file mode 100644
index 000000000000..1eea663a83b2
--- /dev/null
+++ b/build/seo.ts
@@ -0,0 +1,526 @@
+// URLs of 500 pages randomly sampled from 2719 affected Web/API pages.
+const TEST_GROUP = new Set(
+ [
+ "Element/getClientRects",
+ "CSSImportRule/media",
+ "CSSFontPaletteValuesRule/basePalette",
+ "KeyboardEvent/code",
+ "GPUDevice/features",
+ "File/lastModified",
+ "CompositionEvent/CompositionEvent",
+ "IdleDetector/requestPermission",
+ "Notification/permission",
+ "GPUAdapter/limits",
+ "AudioParam/exponentialRampToValueAtTime",
+ "Navigator/unregisterProtocolHandler",
+ "ImageDecoder/complete",
+ "HTMLMediaElement/buffered",
+ "Node/compareDocumentPosition",
+ "OES_draw_buffers_indexed/blendFuncSeparateiOES",
+ "AudioBufferSourceNode/AudioBufferSourceNode",
+ "CSSLayerStatementRule/nameList",
+ "Document/fgColor",
+ "Document/open",
+ "File/File",
+ "HighlightRegistry/delete",
+ "HIDDevice/forget",
+ "CSSRotate/z",
+ "AbortController/signal",
+ "HTMLAreaElement/toString",
+ "Notification/badge",
+ "CookieStoreManager/subscribe",
+ "CSSPrimitiveValue/primitiveType",
+ "CSSPseudoElement/type",
+ "HTMLIFrameElement/referrerPolicy",
+ "AudioData/numberOfChannels",
+ "ElementInternals/ariaRowIndex",
+ "BarProp/visible",
+ "ImageTrack/frameCount",
+ "CanvasRenderingContext2D/imageSmoothingQuality",
+ "GeolocationCoordinates/speed",
+ "Ink/requestPresenter",
+ "DataTransfer/setData",
+ "GeolocationCoordinates/longitude",
+ "MerchantValidationEvent/validationURL",
+ "EncodedAudioChunk/byteLength",
+ "CSSTranslate/x",
+ "FontData/family",
+ "FileSystemHandle/remove",
+ "OverconstrainedError/constraint",
+ "Geolocation/watchPosition",
+ "OfflineAudioContext/OfflineAudioContext",
+ "GPUTexture/depthOrArrayLayers",
+ "OfflineAudioContext/startRendering",
+ "AudioTrack/id",
+ "NodeIterator/previousNode",
+ "Performance/setResourceTimingBufferSize",
+ "Animation/playState",
+ "FileSystem/root",
+ "IntersectionObserver/thresholds",
+ "BluetoothRemoteGATTDescriptor/writeValue",
+ "AnimationEvent/elapsedTime",
+ "LockManager/query",
+ "NavigationDestination/getState",
+ "Performance/measure",
+ "Document/lastModified",
+ "MutationEvent/attrChange",
+ "KeyframeEffect/pseudoElement",
+ "EventSource/close",
+ "GPUBuffer/destroy",
+ "PaymentAddress/sortingCode",
+ "MediaCapabilities/decodingInfo",
+ "MediaSource/removeSourceBuffer",
+ "DelayNode/delayTime",
+ "CanvasRenderingContext2D/isContextLost",
+ "GPUAdapter/isFallbackAdapter",
+ "MouseEvent/metaKey",
+ "AudioParam/linearRampToValueAtTime",
+ "CSSScale/z",
+ "MediaTrackConstraints/facingMode",
+ "BluetoothCharacteristicProperties/writeWithoutResponse",
+ "DOMTokenList/replace",
+ "EncodedVideoChunk/type",
+ "NDEFRecord/lang",
+ "Navigator/onLine",
+ "MediaStream/getTracks",
+ "MediaKeySession/update",
+ "MediaTrackSupportedConstraints/frameRate",
+ "OfflineAudioContext/length",
+ "Cache/delete",
+ "MessageEvent/source",
+ "MediaStreamAudioDestinationNode/MediaStreamAudioDestinationNode",
+ "History/back",
+ "Element/outerHTML",
+ "GPUCanvasContext/getCurrentTexture",
+ "Element/className",
+ "MediaError/msExtendedCode",
+ "ExtendableCookieChangeEvent/changed",
+ "MediaRecorder/start",
+ "PaymentRequestEvent/instrumentKey",
+ "GPUDevice/createComputePipeline",
+ "GeolocationCoordinates/accuracy",
+ "GPUCommandEncoder/copyTextureToBuffer",
+ "HTMLSelectElement/remove",
+ "HTMLSelectElement/autofocus",
+ "IDBObjectStore/getKey",
+ "MutationEvent/newValue",
+ "PerformanceElementTiming/url",
+ "ElementInternals/validity",
+ "Navigator/xr",
+ "CSSContainerRule/containerQuery",
+ "MediaSource/clearLiveSeekableRange",
+ "InputEvent/inputType",
+ "FontFaceSet/check",
+ "HTMLFormControlsCollection/namedItem",
+ "CSSKeyframesRule/name",
+ "MouseEvent/buttons",
+ "CanvasRenderingContext2D/strokeRect",
+ "Element/clientWidth",
+ "BatteryManager/level",
+ "Crypto/getRandomValues",
+ "Performance/timing",
+ "Document/clear",
+ "History/replaceState",
+ "MediaList/mediaText",
+ "DOMTokenList/item",
+ "CSSUnparsedValue/length",
+ "Gyroscope/z",
+ "MediaStreamTrack/getConstraints",
+ "NavigationDestination/url",
+ "BluetoothCharacteristicProperties/reliableWrite",
+ "HIDDevice/collections",
+ "IDBFactory/open",
+ "CSSTransformValue/entries",
+ "CanvasRenderingContext2D/direction",
+ "CSSPrimitiveValue/getCounterValue",
+ "GPUAdapterInfo/description",
+ "DataTransfer/files",
+ "MediaQueryList/addListener",
+ "CookieChangeEvent/changed",
+ "EventTarget/dispatchEvent",
+ "Clipboard/readText",
+ "Performance/timeOrigin",
+ "GPUQueue/onSubmittedWorkDone",
+ "Node/isSameNode",
+ "NotificationEvent/action",
+ "MediaQueryList/media",
+ "ElementInternals/ariaColCount",
+ "CSSImportRule/layerName",
+ "Document/scripts",
+ "HTMLImageElement/x",
+ "HTMLFormElement/encoding",
+ "Navigator/maxTouchPoints",
+ "console/profileEnd",
+ "GPUCommandEncoder/popDebugGroup",
+ "MediaDevices/getSupportedConstraints",
+ "KeyboardEvent/charCode",
+ "HTMLInputElement/setRangeText",
+ "Element/ariaColCount",
+ "KeyframeEffect/target",
+ "GeolocationCoordinates/altitudeAccuracy",
+ "Document/createTextNode",
+ "DocumentFragment/children",
+ "Element/animate",
+ "ImageDecoder/type",
+ "AnalyserNode/getByteTimeDomainData",
+ "GamepadHapticActuator/type",
+ "HTMLTableElement/summary",
+ "CookieStoreManager/getSubscriptions",
+ "HTMLTextAreaElement/labels",
+ "CSSStyleRule/styleMap",
+ "MIDIPort/state",
+ "IDBIndex/objectStore",
+ "GPUCommandEncoder/label",
+ "CSSNumericValue/parse",
+ "CSSPrimitiveValue/getFloatValue",
+ "Element/scrollIntoViewIfNeeded",
+ "HTMLAreaElement/relList",
+ "PannerNode/setPosition",
+ "Element/ariaAtomic",
+ "HTMLSelectElement/labels",
+ "FetchEvent/handled",
+ "CanMakePaymentEvent/respondWith",
+ "HTMLAnchorElement/href",
+ "PageTransitionEvent/persisted",
+ "CanvasRenderingContext2D/lineCap",
+ "NDEFReadingEvent/message",
+ "GPUTexture/label",
+ "Performance/navigation",
+ "HTMLFormElement/method",
+ "CSSCounterStyleRule/suffix",
+ "CanvasRenderingContext2D/lineWidth",
+ "NavigateEvent/scroll",
+ "Event/currentTarget",
+ "NodeList/length",
+ "ImageCapture/takePhoto",
+ "KeyboardEvent/initKeyboardEvent",
+ "IDBObjectStore/keyPath",
+ "HTMLMediaElement/canPlayType",
+ "CanvasRenderingContext2D/scale",
+ "Document/xmlEncoding",
+ "AudioData/close",
+ "FormData/getAll",
+ "GPUComputePipeline/getBindGroupLayout",
+ "Document/createEvent",
+ "OscillatorNode/detune",
+ "console/profile",
+ "CanvasRenderingContext2D/miterLimit",
+ "CanvasRenderingContext2D/closePath",
+ "BluetoothCharacteristicProperties/authenticatedSignedWrites",
+ "Magnetometer/y",
+ "Element/clientLeft",
+ "CSSKeyframesRule/appendRule",
+ "FormData/get",
+ "Navigator/mediaCapabilities",
+ "FeaturePolicy/getAllowlistForFeature",
+ "ElementInternals/ariaValueNow",
+ "ImageDecoder/reset",
+ "Blob/type",
+ "InkPresenter/presentationArea",
+ "AnimationPlaybackEvent/timelineTime",
+ "GPUCommandEncoder/insertDebugMarker",
+ "CustomEvent/CustomEvent",
+ "Gamepad/connected",
+ "HTMLShadowElement/getDistributedNodes",
+ "Element/elementTiming",
+ "Highlight/has",
+ "Document/pictureInPictureElement",
+ "Keyboard/getLayoutMap",
+ "AudioWorkletProcessor/port",
+ "DocumentType/before",
+ "Document/pictureInPictureEnabled",
+ "AbortSignal/abort",
+ "NavigationCurrentEntryChangeEvent/navigationType",
+ "MediaRecorder/videoBitsPerSecond",
+ "GPUTexture/usage",
+ "BaseAudioContext/createChannelSplitter",
+ "Gyroscope/y",
+ "MediaTrackSupportedConstraints/noiseSuppression",
+ "InputDeviceInfo/getCapabilities",
+ "CSSKeywordValue/CSSKeywordValue",
+ "AudioDecoder/AudioDecoder",
+ "PannerNode/setVelocity",
+ "HTMLAreaElement/protocol",
+ "MutationObserver/takeRecords",
+ "CacheStorage/delete",
+ "BroadcastChannel/name",
+ "Element/lastElementChild",
+ "DeviceMotionEvent/acceleration",
+ "MediaList/item",
+ "FileReader/readyState",
+ "HTMLAnchorElement/origin",
+ "HTMLMediaElement/seekable",
+ "HTMLAreaElement/pathname",
+ "GPUPipelineError/reason",
+ "AbortSignal/aborted",
+ "IDBFactory/deleteDatabase",
+ "CSSMathSum/CSSMathSum",
+ "CSSStyleDeclaration/getPropertyCSSValue",
+ "HTMLElement/offsetHeight",
+ "ElementInternals/checkValidity",
+ "HTMLObjectElement/willValidate",
+ "CanvasRenderingContext2D/fillRect",
+ "Navigator/taintEnabled",
+ "GPUDevice/destroy",
+ "CloseEvent/reason",
+ "GPUQuerySet/count",
+ "AudioTrackList/length",
+ "LargestContentfulPaint/toJSON",
+ "MIDIPort/close",
+ "Navigator/windowControlsOverlay",
+ "IDBObjectStore/openKeyCursor",
+ "InputEvent/dataTransfer",
+ "MouseEvent/y",
+ "CSSPositionValue/y",
+ "HTMLProgressElement/labels",
+ "FileSystemHandle/name",
+ "DOMPoint/DOMPoint",
+ "HTMLSelectElement/type",
+ "HTMLInputElement/checkValidity",
+ "PaymentResponse/toJSON",
+ "Element/ariaValueText",
+ "console/timeStamp",
+ "MediaStreamTrackProcessor/MediaStreamTrackProcessor",
+ "GPUTexture/destroy",
+ "Clipboard/writeText",
+ "CSSKeyframeRule/keyText",
+ "ImageBitmap/close",
+ "DecompressionStream/DecompressionStream",
+ "Accelerometer/z",
+ "MediaStream/removeTrack",
+ "DocumentFragment/querySelectorAll",
+ "HighlightRegistry/values",
+ "HTMLFormElement/length",
+ "DOMImplementation/createHTMLDocument",
+ "CSSCounterStyleRule/name",
+ "HTMLImageElement/sizes",
+ "GPUCommandEncoder/finish",
+ "Attr/name",
+ "CSSStyleSheet/CSSStyleSheet",
+ "DocumentFragment/firstElementChild",
+ "KeyboardLayoutMap/has",
+ "IIRFilterNode/getFrequencyResponse",
+ "ElementInternals/setFormValue",
+ "Document/getAnimations",
+ "Document/location",
+ "Document/body",
+ "AudioBuffer/copyToChannel",
+ "BlobEvent/data",
+ "CSSStyleDeclaration/removeProperty",
+ "AudioDecoder/decodeQueueSize",
+ "Navigator/hardwareConcurrency",
+ "PaymentRequestEvent/paymentRequestOrigin",
+ "AudioNode/channelInterpretation",
+ "MediaRecorder/MediaRecorder",
+ "IDBKeyRange/upperOpen",
+ "NodeIterator/detach",
+ "HTMLInputElement/stepDown",
+ "FontFace/variationSettings",
+ "NavigateEvent/NavigateEvent",
+ "BluetoothUUID/getService",
+ "GPUDevice/createPipelineLayout",
+ "GPURenderPassEncoder/setPipeline",
+ "AuthenticatorAssertionResponse/signature",
+ "PaymentAddress/addressLine",
+ "BackgroundFetchManager/getIds",
+ "CanvasRenderingContext2D/getContextAttributes",
+ "CharacterData/after",
+ "Element/innerHTML",
+ "BarcodeDetector/detect",
+ "Performance/memory",
+ "MediaStreamTrack/getCapabilities",
+ "CSSMathProduct/values",
+ "BackgroundFetchUpdateUIEvent/updateUI",
+ "FileReader/readAsDataURL",
+ "PannerNode/distanceModel",
+ "CSSPrimitiveValue/getRGBColorValue",
+ "FontFaceSet/clear",
+ "Navigator/appCodeName",
+ "Navigator/hid",
+ "console/count",
+ "console/timeEnd",
+ "HTMLMediaElement/muted",
+ "Notification/vibrate",
+ "CSS/escape",
+ "Document/fullscreenElement",
+ "ImageTrack/repetitionCount",
+ "Navigator/activeVRDisplays",
+ "AudioBufferSourceNode/playbackRate",
+ "CSSRotate/CSSRotate",
+ "Navigator/locks",
+ "Node/isConnected",
+ "MediaRecorder/warning_event",
+ "PaintWorkletGlobalScope/devicePixelRatio",
+ "GPUDevice/createRenderPipeline",
+ "KeyboardEvent/location",
+ "DedicatedWorkerGlobalScope/postMessage",
+ "Navigator/canShare",
+ "CredentialsContainer/get",
+ "console/dir",
+ "PaymentRequest/shippingOption",
+ "Element/getElementsByTagNameNS",
+ "MediaDeviceInfo/kind",
+ "Document/createAttributeNS",
+ "GeolocationPosition/coords",
+ "Element/remove",
+ "Element/hasAttribute",
+ "HTMLVideoElement/getVideoPlaybackQuality",
+ "GPUQueue/writeBuffer",
+ "AudioContext/sinkId",
+ "HighlightRegistry/has",
+ "ExtendableMessageEvent/origin",
+ "IntersectionObserverEntry/time",
+ "OES_draw_buffers_indexed/enableiOES",
+ "History/state",
+ "OscillatorNode/frequency",
+ "HTMLInputElement/setSelectionRange",
+ "ClipboardEvent/ClipboardEvent",
+ "LayoutShiftAttribution/toJSON",
+ "AnimationEffect/updateTiming",
+ "FontFaceSetLoadEvent/fontfaces",
+ "Highlight/entries",
+ "CSSRule/parentRule",
+ "DOMPointReadOnly/w",
+ "GPURenderPassEncoder/end",
+ "IDBRequest/readyState",
+ "MediaTrackSettings/sampleSize",
+ "IntersectionObserverEntry/intersectionRect",
+ "GamepadHapticActuator/pulse",
+ "GPUDevice/createRenderPipelineAsync",
+ "MediaRecorder/stream",
+ "MediaTrackConstraints/height",
+ "CanvasRenderingContext2D/rect",
+ "HIDDevice/receiveFeatureReport",
+ "FileReaderSync/readAsText",
+ "NavigationHistoryEntry/key",
+ "Element/closest",
+ "HTMLLabelElement/htmlFor",
+ "File/webkitRelativePath",
+ "Element/ariaRequired",
+ "HTMLVideoElement/videoWidth",
+ "Highlight/add",
+ "Element/ariaExpanded",
+ "GPUUncapturedErrorEvent/GPUUncapturedErrorEvent",
+ "PaymentRequestUpdateEvent/PaymentRequestUpdateEvent",
+ "ElementInternals/ariaExpanded",
+ "BluetoothDevice/gatt",
+ "AudioContext/createMediaElementSource",
+ "Event/target",
+ "GPUUncapturedErrorEvent/error",
+ "FormData/has",
+ "DeviceMotionEventAcceleration/x",
+ "MIDIPort/name",
+ "CanvasRenderingContext2D/roundRect",
+ "HTMLIFrameElement/allowPaymentRequest",
+ "BackgroundFetchRegistration/uploaded",
+ "FontFace/unicodeRange",
+ "BluetoothRemoteGATTServer/device",
+ "FormData/delete",
+ "ElementInternals/ariaValueMax",
+ "ContentIndex/getAll",
+ "HTMLImageElement/alt",
+ "Element/attachShadow",
+ "MediaList/length",
+ "BaseAudioContext/sampleRate",
+ "CountQueuingStrategy/size",
+ "Notification/renotify",
+ "FormData/append",
+ "Notification/requestPermission",
+ "DelayNode/DelayNode",
+ "CSSStyleSheet/addRule",
+ "CharacterData/remove",
+ "Element/insertAdjacentText",
+ "CSSUnitValue/CSSUnitValue",
+ "CSSUnitValue/unit",
+ "FileReaderSync/FileReaderSync",
+ "HTMLIFrameElement/credentialless",
+ "CanvasRenderingContext2D/createConicGradient",
+ "AudioListener/upZ",
+ "Event/isTrusted",
+ "MutationRecord/attributeName",
+ "HTMLCanvasElement/toBlob",
+ "CustomStateSet/values",
+ "CSSNumericValue/type",
+ "GPUCompilationMessage/lineNum",
+ "GPURenderBundleEncoder/pushDebugGroup",
+ "DataTransferItem/kind",
+ "GPURenderBundleEncoder/insertDebugMarker",
+ "HTMLAreaElement/referrerPolicy",
+ "InterventionReportBody/sourceFile",
+ "ImageData/colorSpace",
+ "Animation/id",
+ "Navigator/requestMIDIAccess",
+ "Navigator/permissions",
+ "Document/lastElementChild",
+ "GPURenderPassEncoder/executeBundles",
+ "PaymentRequestUpdateEvent/updateWith",
+ "MediaTrackSupportedConstraints/echoCancellation",
+ "AudioData/sampleRate",
+ "HIDDevice/sendFeatureReport",
+ "DOMRectReadOnly/left",
+ "MediaTrackSettings/sampleRate",
+ "AudioTrackList/getTrackById",
+ "CSSNumericValue/equals",
+ "FontFace/style",
+ "Document/fullscreenEnabled",
+ "HTMLAnchorElement/toString",
+ "KeyboardEvent/getModifierState",
+ "BluetoothRemoteGATTCharacteristic/getDescriptor",
+ "AudioNode/channelCount",
+ "CompositionEvent/data",
+ "MediaKeyMessageEvent/message",
+ "Clients/get",
+ "CSSFontFeatureValuesRule/fontFamily",
+ "Gyroscope/x",
+ "Element/ariaRelevant",
+ "PageTransitionEvent/PageTransitionEvent",
+ "CanvasRenderingContext2D/isPointInPath",
+ "NavigatorUAData/brands",
+ "FileSystemDirectoryEntry/getDirectory",
+ "CanvasRenderingContext2D/resetTransform",
+ "Highlight/type",
+ "IDBIndex/count",
+ "HTMLInputElement/labels",
+ "Metadata/modificationTime",
+ "IDBIndex/name",
+ "CSSTransformValue/length",
+ "IDBCursor/primaryKey",
+ "DOMRect/DOMRect",
+ "HTMLSelectElement/disabled",
+ "HTMLSelectElement/namedItem",
+ "HTMLSlotElement/assignedNodes",
+ "DOMPointReadOnly/y",
+ "Lock/mode",
+ "Bluetooth/getDevices",
+ "IDBVersionChangeEvent/oldVersion",
+ "ElementInternals/ariaPressed",
+ "MediaSource/endOfStream",
+ "HTMLMediaElement/readyState",
+ "Blob/Blob",
+ "CSSScale/x",
+ "DOMPointReadOnly/z",
+ "MediaTrackSettings/deviceId",
+ "AudioContext/setSinkId",
+ ].map((slugSuffix) => `/en-US/docs/Web/API/${slugSuffix}`.toLowerCase())
+);
+
+export function rewritePageTitleForSEO(
+ mdn_url: string,
+ s: string | null
+): string | null {
+ if (
+ typeof s !== "string" ||
+ typeof mdn_url !== "string" ||
+ !TEST_GROUP.has(mdn_url.toLowerCase())
+ ) {
+ return s;
+ }
+
+ return (
+ s
+ // "AudioBuffer: sampleRate property" -> "AudioBuffer.sampleRate property"
+ .replace(/^(.*): (.*?) (static )?(method|property)/, "$1.$2 $3$4")
+ // "AudioBuffer: AudioBuffer() constructor" -> "AudioBuffer() constructor"
+ .replace(/^(.*): (\1\(\)) constructor/, "$2 constructor") ?? null
+ );
+}