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 `<h1>`) 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 + ); +}