This is an automated email from the ASF dual-hosted git repository.

moonming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 12ae102154c fix(seo): point zh docs at the Chinese hub, drop 
untranslated zh articles (#2087)
12ae102154c is described below

commit 12ae102154c0445f029eb84249e7e302d5d8b411
Author: Ming Wen <[email protected]>
AuthorDate: Thu Jul 30 11:35:26 2026 +0800

    fix(seo): point zh docs at the Chinese hub, drop untranslated zh articles 
(#2087)
    
    Three fixes to the Chinese tree's indexing, where the polarity was
    inverted: translated pages were suppressed and untranslated duplicates
    were promoted.
    
    Chinese plugin docs canonicalised to docs.api7.ai, which has no Chinese
    edition — docs.api7.ai/zh/hub/limit-count is a 404. A genuinely translated
    page (3,052 CJK characters) was telling search engines to index an English
    page instead, so a reader searching "APISIX limit-count 限流" could not
    find the translation that exists. They now canonicalise to
    docs.apiseven.com, the Chinese edition of the same hub; all six sampled
    targets return 200 and serve lang="zh" content. English pages are
    unchanged. This also resolves the one canonical that pointed at a 404:
    docs.api7.ai/hub/feishu-auth is dead, docs.apiseven.com/hub/feishu-auth
    is not.
    
    The 18 Chinese learning-center articles were never translated — 18-26 CJK
    characters against 1,100-2,600 English words, titles byte-identical to
    their English twins, self-canonical, and in the sitemap. They competed
    with the site's most valuable commercial pages (apisix-vs-kong,
    what-is-an-api-gateway) while giving Chinese readers English prose that
    browsers would not offer to translate, because lang="zh-CN" claims it is
    already Chinese. The pages are no longer built; /zh/learning-center/ lists
    the English articles and says so, and the indexed URLs 301 to their
    English equivalents.
    
    Docs body images had no loading hints and no reserved space — eight
    diagrams totalling 235 KB on one plugin page, all eager, all shifting the
    layout as they arrived. A remark plugin adds loading="lazy" and
    decoding="async"; the intrinsic size of a remote image is not knowable at
    build time, so `.prose img` reserves a 16/9 block that a declared
    width/height overrides.
---
 .htaccess                                          |  7 +++
 next/astro.config.mjs                              |  3 +-
 next/scripts/remark-image-loading.mjs              | 21 +++++++++
 next/src/components/CollectionPages.astro          | 16 +++++--
 next/src/components/StaticPages.astro              |  2 +-
 next/src/layouts/DocPage.astro                     | 15 +++++-
 next/src/lib/content.ts                            | 17 +++++--
 next/src/pages/learning-center/[slug].astro        |  4 +-
 next/src/pages/learning-center/archive.astro       |  2 +-
 next/src/pages/learning-center/index.astro         |  2 +-
 next/src/pages/learning-center/page/[n].astro      |  4 +-
 next/src/pages/learning-center/tags/[tag].astro    |  2 +-
 next/src/pages/learning-center/tags/index.astro    |  2 +-
 next/src/pages/zh/learning-center/[slug].astro     | 53 ----------------------
 next/src/pages/zh/learning-center/archive.astro    |  5 --
 next/src/pages/zh/learning-center/index.astro      |  5 +-
 next/src/pages/zh/learning-center/page/[n].astro   | 17 -------
 next/src/pages/zh/learning-center/tags/[tag].astro | 24 ----------
 next/src/pages/zh/learning-center/tags/index.astro | 24 ----------
 next/src/styles/global.css                         |  8 +++-
 20 files changed, 88 insertions(+), 145 deletions(-)

diff --git a/.htaccess b/.htaccess
index 369c473fdba..ca349b60cbe 100644
--- a/.htaccess
+++ b/.htaccess
@@ -164,3 +164,10 @@ RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:next/)?getting-started/$"
 # Renamed docs and GitHub-repo-style paths that leak into search results
 RedirectMatch 301 "^(/zh)?/docs/apisix/stand-alone/?$" 
"$1/docs/apisix/deployment-modes/"
 RedirectMatch 301 "^(/zh)?/docs/apisix/en/latest/deployment-modes(\.md)?/?$" 
"$1/docs/apisix/deployment-modes/"
+
+# The Chinese learning-center articles were never translated: each served the
+# English body under lang="zh-CN" with a title identical to its English twin
+# and a self-referential canonical, so 18 near-duplicates competed with the
+# pages they duplicated. The listing at /zh/learning-center/ now links to the
+# English articles directly; these URLs are indexed, so send them there.
+RedirectMatch 301 "^/zh/learning-center/(.+)$" "/learning-center/$1"
diff --git a/next/astro.config.mjs b/next/astro.config.mjs
index 42c85749f73..89ee08ba81c 100644
--- a/next/astro.config.mjs
+++ b/next/astro.config.mjs
@@ -2,6 +2,7 @@ import { defineConfig } from 'astro/config';
 import remarkDirective from 'remark-directive';
 import { remarkAdmonitions } from './scripts/remark-admonitions.mjs';
 import { remarkHeadingIds } from './scripts/remark-heading-ids.mjs';
+import { remarkImageLoading } from './scripts/remark-image-loading.mjs';
 
 // Static-only rebuild of apisix.apache.org.
 // URL contract: every public URL is identical to the current Docusaurus site
@@ -14,7 +15,7 @@ export default defineConfig({
   // Vite's inline threshold, so the deploy overlay's _astro/ dependency holds.
   build: { format: 'directory', inlineStylesheets: 'never' },
   markdown: {
-    remarkPlugins: [remarkDirective, remarkAdmonitions, remarkHeadingIds],
+    remarkPlugins: [remarkDirective, remarkAdmonitions, remarkHeadingIds, 
remarkImageLoading],
     shikiConfig: { theme: 'github-dark-default' },
   },
 });
diff --git a/next/scripts/remark-image-loading.mjs 
b/next/scripts/remark-image-loading.mjs
new file mode 100644
index 00000000000..343aa097f6f
--- /dev/null
+++ b/next/scripts/remark-image-loading.mjs
@@ -0,0 +1,21 @@
+import { visit } from 'unist-util-visit';
+
+// Body images in synced docs are bare `![alt](url)` — no dimensions, no
+// loading hint — and most are remote (static.api7.ai), so the browser has
+// nothing to reserve space with and fetches every one eagerly. On a page like
+// /docs/apisix/plugins/openid-connect/ that is eight diagrams, ~235 KB, all
+// blocking and all shifting the layout as they arrive.
+//
+// The intrinsic size isn't knowable at build time for remote images, so the
+// space is reserved in CSS (`.prose img` carries an aspect-ratio placeholder);
+// here we add the loading hints the markup can carry.
+export function remarkImageLoading() {
+  return (tree) => {
+    visit(tree, 'image', (node) => {
+      const data = node.data || (node.data = {});
+      const props = data.hProperties || (data.hProperties = {});
+      if (props.loading === undefined) props.loading = 'lazy';
+      if (props.decoding === undefined) props.decoding = 'async';
+    });
+  };
+}
diff --git a/next/src/components/CollectionPages.astro 
b/next/src/components/CollectionPages.astro
index f64d20150d2..7ad7e7031c7 100644
--- a/next/src/components/CollectionPages.astro
+++ b/next/src/components/CollectionPages.astro
@@ -16,9 +16,17 @@ interface Props {
   sub?: string;
   kicker?: string;
   withTags?: boolean;
+  /** Locale prefix for archive/tags/pagination links; defaults to this 
page's. */
+  linkPrefix?: string;
 }
-const { locale, page, posts, urlBase, heading, sub, kicker, withTags = false } 
= Astro.props;
+const {
+  locale, page, posts, urlBase, heading, sub, kicker, withTags = false, 
linkPrefix,
+} = Astro.props;
 const prefix = localePrefix(locale);
+// Where the archive/tags/pagination siblings live. Normally alongside this
+// listing, but a locale that shows another locale's articles (zh learning
+// center) must point at that locale's pages, not at routes it doesn't build.
+const sibling = linkPrefix ?? prefix;
 const pages = paginate(posts);
 const pagePosts = pages[page - 1] ?? [];
 ---
@@ -29,10 +37,10 @@ const pagePosts = pages[page - 1] ?? [];
   kicker={kicker}
   locale={locale}
   path={page === 1 ? `${urlBase}/` : `${urlBase}/page/${page}/`}
-  base={`${prefix}${urlBase}/`}
+  base={`${sibling}${urlBase}/`}
   posts={pagePosts}
   current={page}
   total={pages.length}
-  archiveUrl={`${prefix}${urlBase}/archive/`}
-  tagsUrl={withTags ? `${prefix}${urlBase}/tags/` : undefined}
+  archiveUrl={`${sibling}${urlBase}/archive/`}
+  tagsUrl={withTags ? `${sibling}${urlBase}/tags/` : undefined}
 />
diff --git a/next/src/components/StaticPages.astro 
b/next/src/components/StaticPages.astro
index 97451d47175..b741971d877 100644
--- a/next/src/components/StaticPages.astro
+++ b/next/src/components/StaticPages.astro
@@ -24,7 +24,7 @@ const DOCS_PROJECTS = [
 ];
 
 const comparisons = kind === 'comparisons'
-  ? getLearningPosts(locale).filter((post) => post.tags.includes('comparison'))
+  ? getLearningPosts().filter((post) => post.tags.includes('comparison'))
   : [];
 const aisixIndex = comparisons.findIndex((post) => post.slug === 
'apisix-vs-aisix');
 const kongIndex = comparisons.findIndex((post) => post.slug === 
'apisix-vs-kong');
diff --git a/next/src/layouts/DocPage.astro b/next/src/layouts/DocPage.astro
index aa9733694e4..ed1daaeacb2 100644
--- a/next/src/layouts/DocPage.astro
+++ b/next/src/layouts/DocPage.astro
@@ -39,6 +39,19 @@ const jsonLd = [{
   mainEntityOfPage: `${SITE}${locale === 'zh' ? '/zh' : ''}${path}`,
 }];
 
+/**
+ * Upstream plugin docs carry a cross-site canonical to the English hub on
+ * docs.api7.ai. Emitting that unchanged on a translated Chinese page points
+ * search engines at a URL with no Chinese edition (docs.api7.ai/zh/hub/* is
+ * 404), so the translation we do have gets de-indexed in favour of a page
+ * that cannot rank for those queries. docs.apiseven.com is the Chinese
+ * edition of the same hub, so zh pages canonicalise there instead.
+ */
+const ZH_HUB = 'https://docs.apiseven.com';
+const rawCanonical = entry.mod.frontmatter.canonical;
+const canonicalOverride = locale === 'zh' && 
rawCanonical?.startsWith('https://docs.api7.ai/')
+  ? rawCanonical.replace('https://docs.api7.ai', ZH_HUB)
+  : rawCanonical;
 ---
 <Base
   title={entry.title}
@@ -46,7 +59,7 @@ const jsonLd = [{
   locale={locale}
   path={path}
   jsonLd={jsonLd}
-  canonicalOverride={entry.mod.frontmatter.canonical}
+  canonicalOverride={canonicalOverride}
   search
 >
   <div class="container docs-layout">
diff --git a/next/src/lib/content.ts b/next/src/lib/content.ts
index 14f477d0db1..2cd6adb79e4 100644
--- a/next/src/lib/content.ts
+++ b/next/src/lib/content.ts
@@ -188,11 +188,20 @@ export function getBlogPosts(locale: Locale): Post[] {
     .sort(byDateDesc);
 }
 
-export function getLearningPosts(locale: Locale): Post[] {
-  // learning-center content is EN-only today; zh URLs serve the same entries
-  // (the current site does the same via Docusaurus i18n fallback).
+/**
+ * Learning-center articles. The content is English-only, so these always
+ * resolve to English URLs — the Chinese listing links straight to them.
+ *
+ * We used to publish /zh/learning-center/<slug>/ as well, serving the English
+ * body under lang="zh-CN" with a title identical to the English page and a
+ * self-referential canonical. That produced 18 near-duplicate competitors to
+ * the site's most valuable commercial pages, and told browsers the page was
+ * already Chinese so they would not offer to translate it. A reader is better
+ * served by an honest English URL.
+ */
+export function getLearningPosts(): Post[] {
   return Object.entries(learningModules)
-    .map(([p, mod]) => flatPost(p, mod, '/learning-center', locale))
+    .map(([p, mod]) => flatPost(p, mod, '/learning-center', 'en'))
     .sort(byDateDesc);
 }
 
diff --git a/next/src/pages/learning-center/[slug].astro 
b/next/src/pages/learning-center/[slug].astro
index 3109de29a29..994203f469e 100644
--- a/next/src/pages/learning-center/[slug].astro
+++ b/next/src/pages/learning-center/[slug].astro
@@ -3,11 +3,11 @@ import Article from '../../layouts/Article.astro';
 import { getLearningPosts, relatedPosts } from '../../lib/content';
 
 export function getStaticPaths() {
-  return getLearningPosts('en').map((post) => ({ params: { slug: post.slug }, 
props: { post } }));
+  return getLearningPosts().map((post) => ({ params: { slug: post.slug }, 
props: { post } }));
 }
 const { post } = Astro.props;
 const path = `/learning-center/${post.slug}/`;
-const pool = getLearningPosts('en');
+const pool = getLearningPosts();
 const related = relatedPosts(post, pool);
 const idx = pool.findIndex((p) => p.url === post.url);
 const newer = idx > 0 ? pool[idx - 1] : undefined;
diff --git a/next/src/pages/learning-center/archive.astro 
b/next/src/pages/learning-center/archive.astro
index b141b99dea4..c0d3080daa0 100644
--- a/next/src/pages/learning-center/archive.astro
+++ b/next/src/pages/learning-center/archive.astro
@@ -2,4 +2,4 @@
 import BlogArchive from '../../components/BlogArchive.astro';
 import { getLearningPosts } from '../../lib/content';
 ---
-<BlogArchive locale="en" path="/learning-center/archive/" 
posts={getLearningPosts('en')} heading="Learning Center Archive" 
kicker="Learning Center" />
+<BlogArchive locale="en" path="/learning-center/archive/" 
posts={getLearningPosts()} heading="Learning Center Archive" kicker="Learning 
Center" />
diff --git a/next/src/pages/learning-center/index.astro 
b/next/src/pages/learning-center/index.astro
index aaf183e0e18..79498f59306 100644
--- a/next/src/pages/learning-center/index.astro
+++ b/next/src/pages/learning-center/index.astro
@@ -5,7 +5,7 @@ import { getLearningPosts } from '../../lib/content';
 <CollectionPages
   locale="en"
   page={1}
-  posts={getLearningPosts('en')}
+  posts={getLearningPosts()}
   urlBase="/learning-center"
   heading="Learning Center"
   kicker="Learning Center"
diff --git a/next/src/pages/learning-center/page/[n].astro 
b/next/src/pages/learning-center/page/[n].astro
index e9b00775645..fc2a3c317f3 100644
--- a/next/src/pages/learning-center/page/[n].astro
+++ b/next/src/pages/learning-center/page/[n].astro
@@ -3,13 +3,13 @@ import CollectionPages from 
'../../../components/CollectionPages.astro';
 import { getLearningPosts, paginate } from '../../../lib/content';
 
 export function getStaticPaths() {
-  return paginate(getLearningPosts('en')).slice(1).map((_, i) => ({ params: { 
n: String(i + 2) } }));
+  return paginate(getLearningPosts()).slice(1).map((_, i) => ({ params: { n: 
String(i + 2) } }));
 }
 ---
 <CollectionPages
   locale="en"
   page={Number(Astro.params.n)}
-  posts={getLearningPosts('en')}
+  posts={getLearningPosts()}
   urlBase="/learning-center"
   heading="Learning Center"
   kicker="Learning Center"
diff --git a/next/src/pages/learning-center/tags/[tag].astro 
b/next/src/pages/learning-center/tags/[tag].astro
index b759f807f88..83f28357561 100644
--- a/next/src/pages/learning-center/tags/[tag].astro
+++ b/next/src/pages/learning-center/tags/[tag].astro
@@ -3,7 +3,7 @@ import ListPage from '../../../layouts/ListPage.astro';
 import { getLearningPosts, groupByTag } from '../../../lib/content';
 
 export function getStaticPaths() {
-  const tags = groupByTag(getLearningPosts('en'));
+  const tags = groupByTag(getLearningPosts());
   return [...tags.entries()].map(([slug, group]) => ({ params: { tag: slug }, 
props: group }));
 }
 const { label, posts } = Astro.props;
diff --git a/next/src/pages/learning-center/tags/index.astro 
b/next/src/pages/learning-center/tags/index.astro
index e32ef63bb34..bd6ad8ee08d 100644
--- a/next/src/pages/learning-center/tags/index.astro
+++ b/next/src/pages/learning-center/tags/index.astro
@@ -2,7 +2,7 @@
 import Base from '../../../layouts/Base.astro';
 import { getLearningPosts, groupByTag } from '../../../lib/content';
 
-const tags = groupByTag(getLearningPosts('en'));
+const tags = groupByTag(getLearningPosts());
 const sorted = [...tags.entries()].sort((a, b) => b[1].posts.length - 
a[1].posts.length || a[0].localeCompare(b[0]));
 ---
 <Base title="Tags | Learning Center" locale="en" path="/learning-center/tags/">
diff --git a/next/src/pages/zh/learning-center/[slug].astro 
b/next/src/pages/zh/learning-center/[slug].astro
deleted file mode 100644
index b8a763ac0d5..00000000000
--- a/next/src/pages/zh/learning-center/[slug].astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-import Article from '../../../layouts/Article.astro';
-import { getLearningPosts, relatedPosts } from '../../../lib/content';
-
-export function getStaticPaths() {
-  return getLearningPosts('zh').map((post) => ({ params: { slug: post.slug }, 
props: { post } }));
-}
-const { post } = Astro.props;
-const path = `/learning-center/${post.slug}/`;
-const pool = getLearningPosts('zh');
-const related = relatedPosts(post, pool);
-const idx = pool.findIndex((p) => p.url === post.url);
-const newer = idx > 0 ? pool[idx - 1] : undefined;
-const older = idx >= 0 && idx < pool.length - 1 ? pool[idx + 1] : undefined;
-
-// learning-center pages carry structured FAQ data in frontmatter — emit 
FAQPage
-// JSON-LD exactly like the current SEO setup.
-const faq = post.mod.frontmatter.faq;
-const jsonLdExtra: object[] = Array.isArray(faq) && faq.length
-  ? [{
-      '@context': 'https://schema.org',
-      '@type': 'FAQPage',
-      mainEntity: faq.map((f: any) => ({
-        '@type': 'Question',
-        name: f.q,
-        acceptedAnswer: { '@type': 'Answer', text: f.a },
-      })),
-    }]
-  : [];
----
-<Article
-  post={post}
-  locale="zh"
-  path={path}
-  tagBase="/zh/learning-center"
-  schemaType="TechArticle"
-  jsonLdExtra={jsonLdExtra}
-  related={related}
-  newer={newer}
-  older={older}
->
-  {Array.isArray(faq) && faq.length > 0 && (
-    <section class="faq">
-      <h2>常见问题</h2>
-      {faq.map((f: any) => (
-        <details>
-          <summary>{f.q}</summary>
-          <p>{f.a}</p>
-        </details>
-      ))}
-    </section>
-  )}
-</Article>
diff --git a/next/src/pages/zh/learning-center/archive.astro 
b/next/src/pages/zh/learning-center/archive.astro
deleted file mode 100644
index b3c4f30a265..00000000000
--- a/next/src/pages/zh/learning-center/archive.astro
+++ /dev/null
@@ -1,5 +0,0 @@
----
-import BlogArchive from '../../../components/BlogArchive.astro';
-import { getLearningPosts } from '../../../lib/content';
----
-<BlogArchive locale="zh" path="/learning-center/archive/" 
posts={getLearningPosts('zh')} heading="学习中心归档" kicker="学习中心" />
diff --git a/next/src/pages/zh/learning-center/index.astro 
b/next/src/pages/zh/learning-center/index.astro
index 11b7221ad5c..606f3b02175 100644
--- a/next/src/pages/zh/learning-center/index.astro
+++ b/next/src/pages/zh/learning-center/index.astro
@@ -5,10 +5,11 @@ import { getLearningPosts } from '../../../lib/content';
 <CollectionPages
   locale="zh"
   page={1}
-  posts={getLearningPosts('zh')}
+  posts={getLearningPosts()}
   urlBase="/learning-center"
   heading="学习中心"
   kicker="学习中心"
-  sub="API 网关概念、安全、Kubernetes 与网关对比指南。"
+  sub="API 网关概念、安全、Kubernetes 与网关对比指南。文章内容为英文。"
   withTags
+  linkPrefix=""
 />
diff --git a/next/src/pages/zh/learning-center/page/[n].astro 
b/next/src/pages/zh/learning-center/page/[n].astro
deleted file mode 100644
index bc9a09a1f12..00000000000
--- a/next/src/pages/zh/learning-center/page/[n].astro
+++ /dev/null
@@ -1,17 +0,0 @@
----
-import CollectionPages from '../../../../components/CollectionPages.astro';
-import { getLearningPosts, paginate } from '../../../../lib/content';
-
-export function getStaticPaths() {
-  return paginate(getLearningPosts('zh')).slice(1).map((_, i) => ({ params: { 
n: String(i + 2) } }));
-}
----
-<CollectionPages
-  locale="zh"
-  page={Number(Astro.params.n)}
-  posts={getLearningPosts('zh')}
-  urlBase="/learning-center"
-  heading="学习中心"
-  kicker="学习中心"
-  withTags
-/>
diff --git a/next/src/pages/zh/learning-center/tags/[tag].astro 
b/next/src/pages/zh/learning-center/tags/[tag].astro
deleted file mode 100644
index 53ebed3912a..00000000000
--- a/next/src/pages/zh/learning-center/tags/[tag].astro
+++ /dev/null
@@ -1,24 +0,0 @@
----
-import ListPage from '../../../../layouts/ListPage.astro';
-import { getLearningPosts, groupByTag } from '../../../../lib/content';
-
-export function getStaticPaths() {
-  const tags = groupByTag(getLearningPosts('zh'));
-  return [...tags.entries()].map(([slug, group]) => ({ params: { tag: slug }, 
props: group }));
-}
-const { label, posts } = Astro.props;
-const tag = Astro.params.tag;
----
-<ListPage
-  title={`${label} | 学习中心`}
-  heading={label}
-  kicker={'标签'}
-  sub={`${posts.length} 篇文章`}
-  locale="zh"
-  path={`/learning-center/tags/${tag}/`}
-  base={`/zh/learning-center/tags/${tag}/`}
-  posts={posts}
-  current={1}
-  total={1}
-  tagsUrl={'/zh/learning-center/tags/'}
-/>
diff --git a/next/src/pages/zh/learning-center/tags/index.astro 
b/next/src/pages/zh/learning-center/tags/index.astro
deleted file mode 100644
index 34eee02132f..00000000000
--- a/next/src/pages/zh/learning-center/tags/index.astro
+++ /dev/null
@@ -1,24 +0,0 @@
----
-import Base from '../../../../layouts/Base.astro';
-import { getLearningPosts, groupByTag } from '../../../../lib/content';
-
-const tags = groupByTag(getLearningPosts('zh'));
-const sorted = [...tags.entries()].sort((a, b) => b[1].posts.length - 
a[1].posts.length || a[0].localeCompare(b[0]));
----
-<Base title="标签 | 学习中心" locale="zh" path="/learning-center/tags/">
-  <header class="page-head">
-    <div class="container">
-      <div class="page-title">
-        <p class="kicker">学习中心</p>
-        <h1>学习中心标签</h1>
-      </div>
-    </div>
-  </header>
-  <div class="container">
-    <nav class="tag-row tag-cloud" aria-label="标签">
-      {sorted.map(([slug, { label, posts }]) => (
-        <a class="tag" href={`/zh/learning-center/tags/${slug}/`}>{label} 
<span class="count">{posts.length}</span></a>
-      ))}
-    </nav>
-  </div>
-</Base>
diff --git a/next/src/styles/global.css b/next/src/styles/global.css
index ab89ffd5a30..cc4362736f8 100644
--- a/next/src/styles/global.css
+++ b/next/src/styles/global.css
@@ -276,7 +276,13 @@ a.tag:focus-visible { outline: 2px solid 
var(--color-primary); outline-offset: 2
 .prose a:hover { color: var(--color-primary); text-decoration-color: 
currentcolor; }
 .prose :not(pre) > code { overflow-wrap: anywhere; }
 .prose iframe { width: 100%; aspect-ratio: 16 / 9; height: auto; border: 0; 
border-radius: 8px; }
-.prose img { border-radius: 8px; }
+/* Synced docs images are remote and carry no intrinsic size, so reserve a
+   plausible block before they load — without this the whole article reflows as
+   each diagram arrives. `height: auto` still wins once the real ratio is 
known,
+   so correctly-proportioned images are unaffected. */
+.prose img { border-radius: 8px; aspect-ratio: 16 / 9; height: auto; 
object-fit: contain; }
+/* Images that declare their own dimensions know better. */
+.prose img[width][height] { aspect-ratio: auto; }
 .prose code { font-family: var(--font-mono); font-size: .86em; background: 
var(--color-code-bg); padding: .12em .35em; border-radius: 4px; }
 .prose pre { background: #0d1117; color: #e6edf3; border-radius: 10px; 
padding: 1rem 1.2rem; overflow-x: auto; font-size: .86rem; line-height: 1.6; }
 .prose .code-title { background: #21262d; color: #e6edf3; font-family: 
var(--font-mono); font-size: .78rem; padding: .45rem 1.2rem; border-radius: 
10px 10px 0 0; margin-bottom: 0; }

Reply via email to