(null)
+ const hydrating = useIsHydrating()
useEffect(() => {
// Failed navigations clear pendingHref; cancellations keep the newer click highlighted.
@@ -158,6 +159,8 @@ export const SidebarProduct = () => {
}, [router.events])
useEffect(() => {
+ // Wait for the saved expand state, which SidebarExpandStateProvider applies after hydration.
+ if (hydrating) return
// Article filter query params are shallow same-page updates; the grid manages their scroll.
if (/[?&]articles-(filter|category|page)=/.test(router.asPath)) return
// Brand expands every active ancestor, so scroll to the aria-current page item.
@@ -174,7 +177,7 @@ export const SidebarProduct = () => {
const delta =
activeRect.top - containerRect.top - (container.clientHeight - activeRect.height) / 2
container.scrollBy({ top: delta, behavior: 'instant' })
- }, [])
+ }, [hydrating])
if (!sidebarTree) {
return null
@@ -225,7 +228,7 @@ export const SidebarProduct = () => {
return (
-
+
{isRestPage ? restSection() : productSection()}
diff --git a/src/landings/components/useSidebarExpandState.tsx b/src/landings/components/useSidebarExpandState.tsx
index d08b29c27838..850d368ef657 100644
--- a/src/landings/components/useSidebarExpandState.tsx
+++ b/src/landings/components/useSidebarExpandState.tsx
@@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react
import type { ReactNode } from 'react'
import Cookies from '@/frame/components/lib/cookies'
+import { useIsHydrating } from '@/frame/components/hooks/useIsHydrating'
import { SIDEBAR_EXPANDED_COOKIE_NAME } from '@/frame/lib/constants'
// Persists the docs sidebar's expand/collapse state across navigations. The tree
@@ -15,10 +16,10 @@ import { SIDEBAR_EXPANDED_COOKIE_NAME } from '@/frame/lib/constants'
// aria-current chain for uncontrolled items, a controlled item must fold that in
// itself, which is what the onActiveChain fallback does here.
//
-// Server-side rendering reads the cookie in getMainContext and passes it as initial,
-// so the first server and client render match with no post-mount flash. Without
-// initial, for example outside the server-side data path, it falls back to the
-// SSR-safe cookie lib on the client.
+// Page HTML and _next/data are publicly cached and do not vary on Cookie, so the
+// server and hydration render the default state. The cookie applies in the re-render
+// right after hydration, and client-side mounts read it on their first render.
+// SidebarProduct waits for the same useIsHydrating flip to center the saved tree.
type ExpandedStore = Record
@@ -29,12 +30,16 @@ type ExpandStateContextValue = {
const ExpandStateContext = createContext(null)
+// A shared empty store keeps the context value stable when nothing is saved.
+const EMPTY_STORE: ExpandedStore = {}
+
function readStore(): ExpandedStore {
try {
const raw = Cookies.get(SIDEBAR_EXPANDED_COOKIE_NAME)
- return raw ? (JSON.parse(raw) as ExpandedStore) : {}
+ const parsed = raw ? JSON.parse(raw) : null
+ return parsed && typeof parsed === 'object' ? (parsed as ExpandedStore) : EMPTY_STORE
} catch {
- return {}
+ return EMPTY_STORE
}
}
@@ -46,19 +51,16 @@ function persistStore(store: ExpandedStore) {
}
}
-export function SidebarExpandStateProvider({
- children,
- initial,
-}: {
- children: ReactNode
- initial?: ExpandedStore | null
-}) {
- // Seed from the server-read cookie, or read the cookie client-side when initial is absent.
- const [store, setStore] = useState(() => initial ?? readStore())
+export function SidebarExpandStateProvider({ children }: { children: ReactNode }) {
+ const hydrating = useIsHydrating()
+ const saved = useMemo(() => (hydrating ? EMPTY_STORE : readStore()), [hydrating])
+ // Stays null until the reader toggles a category, so the saved store shows after hydration.
+ const [edited, setEdited] = useState(null)
+ const store = edited ?? saved
const setExpanded = useCallback((key: string, expanded: boolean) => {
- setStore((prev) => {
- const next = { ...prev, [key]: expanded }
+ setEdited((prev) => {
+ const next = { ...(prev ?? readStore()), [key]: expanded }
persistStore(next)
return next
})
diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts
index 09e63a409af4..59d26a54f161 100644
--- a/src/languages/lib/correct-translation-content.ts
+++ b/src/languages/lib/correct-translation-content.ts
@@ -698,6 +698,11 @@ export function correctTranslatedContentStrings(
'do {% endif %}{% if ideEntry.versionType == "extension" %}{% data variables.copilot.copilot_extension %} IDE.',
'do {% if ideEntry.versionType == "extension" %}{% data variables.copilot.copilot_extension %}{% endif %} IDE.',
)
+
+ content = content.replaceAll(
+ '{% ifversion ai-powered-security-detections %} e AI Scan funcionam em solicitações de pull',
+ '{% ifversion ai-powered-security-detections %} e AI Scan{% endif %} funcionam em solicitações de pull',
+ )
}
if (context.code === 'zh') {
@@ -858,6 +863,21 @@ export function correctTranslatedContentStrings(
'你可以使用圆括号嵌套筛选器,最多可达五层深度。{% ifversion ghes < 3.18 %} 目前无法在括号中包含`repo`、`org`或`user`限定符。\n\n{% endwebui %}',
'你可以使用圆括号嵌套筛选器,最多可达五层深度。{% ifversion ghes < 3.18 %} 目前无法在括号中包含`repo`、`org`或`user`限定符。{% endif %}\n\n{% endwebui %}',
)
+
+ content = content.replaceAll(
+ '了解 {% endif %}{% data variables.product.prodname_codeql %} 和 AI 扫描{% ifversion ai-powered-security-detections %}如何',
+ '了解 {% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} 和 AI 扫描{% endif %}如何',
+ )
+
+ content = content.replaceAll(
+ '{% data variables.product.prodname_code_quality_short %}{% endif %}、{% data variables.product.prodname_copilot_short %}{% ifversion code-quality %}和',
+ '{% data variables.product.prodname_copilot_short %}{% ifversion code-quality %}、{% data variables.product.prodname_code_quality_short %}{% endif %}和',
+ )
+
+ content = content.replaceAll(
+ '才需要使用旧代码搜索语法{% else %}。您可以通过在任何组合中使用这些代码搜索限定符来搜索代码并缩小结果范围{% data variables.product.github %}。',
+ '才需要使用旧代码搜索语法{% else %}您可以通过在任何组合中使用这些代码搜索限定符来搜索代码并缩小结果范围{% data variables.product.github %}{% endif %}。',
+ )
}
if (context.code === 'ru') {
@@ -1494,6 +1514,12 @@ export function correctTranslatedContentStrings(
'{% data variables.copilot.copilot_autofix_short %} 제안{% endif %} 사용 여부에 따라 수정된 경고 수 {% ifversion code-scanning-autofix %}, 해결되지 않은 상태로 병합된 수',
'{% ifversion code-scanning-autofix %} {% data variables.copilot.copilot_autofix_short %} 제안 사용 여부에 따라{% endif %} 수정된 경고 수, 해결되지 않은 상태로 병합된 수',
)
+
+ // The intro is a YAML block scalar, so whitespace after the stray endif varies.
+ content = content.replace(
+ /\{% endif %\}\s*\{% data variables\.product\.prodname_codeql %\} 및 AI Scan\{% ifversion ai-powered-security-detections %\}이/g,
+ '{% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} 및 AI Scan{% endif %}이',
+ )
}
if (context.code === 'de') {
diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts
index b3b69b0f6259..0392fc1d04ef 100644
--- a/src/languages/tests/correct-translation-content.ts
+++ b/src/languages/tests/correct-translation-content.ts
@@ -2775,6 +2775,63 @@ Para más información, consulta "[AUTOTITLE](/path)".
})
})
+ describe('technical-content#8077 per-file fixes', () => {
+ async function expectRenders(content: string) {
+ await render(content, 'free-pro-team@latest')
+ await render(content, ghesVersion)
+ }
+
+ test('pt: viewing-metrics-for-pull-request-alerts intro adds missing endif', async () => {
+ const broken =
+ 'Monitore como {% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} e AI Scan funcionam em solicitações de pull em suas organizações.'
+ const fixed =
+ 'Monitore como {% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} e AI Scan{% endif %} funcionam em solicitações de pull em suas organizações.'
+ expect(fix(broken, 'pt')).toBe(fixed)
+ expect(fix(fixed, 'pt')).toBe(fixed)
+ await expectRenders(fixed)
+ })
+
+ test('zh: code-scanning-on-pull-requests intro swaps endif and ifversion', async () => {
+ const broken =
+ '了解 {% endif %}{% data variables.product.prodname_codeql %} 和 AI 扫描{% ifversion ai-powered-security-detections %}如何查找并帮助防止整个组织中的拉取请求漏洞。'
+ const fixed =
+ '了解 {% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} 和 AI 扫描{% endif %}如何查找并帮助防止整个组织中的拉取请求漏洞。'
+ expect(fix(broken, 'zh')).toBe(fixed)
+ expect(fix(fixed, 'zh')).toBe(fixed)
+ await expectRenders(fixed)
+ })
+
+ test('zh: download-license-use intro restores ifversion order', async () => {
+ const broken =
+ '获取有关{% data variables.product.github %}、{% data variables.product.prodname_code_quality_short %}{% endif %}、{% data variables.product.prodname_copilot_short %}{% ifversion code-quality %}和{% data variables.product.prodname_AS %}许可证消耗情况的数据。'
+ const fixed =
+ '获取有关{% data variables.product.github %}、{% data variables.product.prodname_copilot_short %}{% ifversion code-quality %}、{% data variables.product.prodname_code_quality_short %}{% endif %}和{% data variables.product.prodname_AS %}许可证消耗情况的数据。'
+ expect(fix(broken, 'zh')).toBe(fixed)
+ expect(fix(fixed, 'zh')).toBe(fixed)
+ await expectRenders(fixed)
+ })
+
+ test('zh: searching-code intro adds missing endif', async () => {
+ const broken =
+ '{% ifversion code-search-upgrade %}仅在使用代码搜索 API 时,才需要使用旧代码搜索语法{% else %}。您可以通过在任何组合中使用这些代码搜索限定符来搜索代码并缩小结果范围{% data variables.product.github %}。'
+ const fixed =
+ '{% ifversion code-search-upgrade %}仅在使用代码搜索 API 时,才需要使用旧代码搜索语法{% else %}您可以通过在任何组合中使用这些代码搜索限定符来搜索代码并缩小结果范围{% data variables.product.github %}{% endif %}。'
+ expect(fix(broken, 'zh')).toBe(fixed)
+ expect(fix(fixed, 'zh')).toBe(fixed)
+ await expectRenders(fixed)
+ })
+
+ test('ko: code-scanning-on-pull-requests intro swaps endif and ifversion', async () => {
+ const broken =
+ '{% endif %}\n {% data variables.product.prodname_codeql %} 및 AI Scan{% ifversion ai-powered-security-detections %}이 조직 전체의 풀 리퀘스트에서 취약성을 찾아내고 방지하는 데 도움을 주는 방법을 이해합니다.'
+ const fixed =
+ '{% data variables.product.prodname_codeql %}{% ifversion ai-powered-security-detections %} 및 AI Scan{% endif %}이 조직 전체의 풀 리퀘스트에서 취약성을 찾아내고 방지하는 데 도움을 주는 방법을 이해합니다.'
+ expect(fix(broken, 'ko')).toBe(fixed)
+ expect(fix(fixed, 'ko')).toBe(fixed)
+ await expectRenders(fixed)
+ })
+ })
+
describe('es: you-can-fork.md per-file fix', () => {
test('replaces leading elsif with ifversion', () => {
const broken = '{% elsif ghes or ghec %} Puedes bifurcar...'
diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx
index 0c37d2e46c75..04c7cdc98ae7 100644
--- a/src/search/components/input/AskAIResults.tsx
+++ b/src/search/components/input/AskAIResults.tsx
@@ -169,6 +169,8 @@ export function AskAIResults({
try {
const response = await executeAISearch(version, query, debug)
+ // Parent overlay state is shared, so a stale request must not touch it after unmount.
+ if (isCancelled) return
if (!response.ok) {
// Classified non-OK responses include upstreamStatus from the proxy or upstream.
let responseJson
@@ -177,6 +179,7 @@ export function AskAIResults({
} catch (error) {
console.error('Failed to parse JSON:', error)
}
+ if (isCancelled) return
const upstreamStatus = responseJson?.upstreamStatus
// Missing upstreamStatus leaves this as an unclassified non-OK response.
if (!upstreamStatus) {
@@ -284,6 +287,7 @@ export function AskAIResults({
while (!done && !isCancelled) {
const { value, done: readerDone } = await reader.read()
+ if (isCancelled) return
done = readerDone
// Buffer newline-delimited JSON until a whole record arrives; leftover means JSON.
diff --git a/src/search/scripts/scrape/lib/build-records-from-api.ts b/src/search/scripts/scrape/lib/build-records-from-api.ts
index f9f68c2086b3..9015ab803299 100644
--- a/src/search/scripts/scrape/lib/build-records-from-api.ts
+++ b/src/search/scripts/scrape/lib/build-records-from-api.ts
@@ -34,6 +34,36 @@ import type {
// Strip them so they stay out of search results.
const ALERT_MARKER_REGEXP = /\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]\n?/gi
+// The markdown body keeps raw HTML, such as octicon SVGs, kbd tags, and editorial
+// comments. Tags become a space so text on either side stays separate tokens.
+// The tag pattern follows CommonMark raw HTML so quoted attributes may contain >.
+const HTML_COMMENT_OR_TAG_REGEXP =
+ /
+
+Inline text.
+`
+ const text = markdownToPlainText(markdown)
+ expect(text).toContain('Before.')
+ expect(text).toContain('Inline')
+ expect(text).toContain('text.')
+ expect(text).not.toContain('