This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new 634f9524b2a [feat] add weekly report (#3966)
634f9524b2a is described below
commit 634f9524b2a932ae7b89ad888e3a898ff6421782
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Jul 5 21:04:22 2026 +0800
[feat] add weekly report (#3966)
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../materialized-view/intro-link.mdx | 13 +
.../materialized-view/intro-link.mdx | 13 +
.../materialized-view/intro-link.mdx | 13 +
scripts/check_move.js | 99 ++-
scripts/check_move.test.js | 62 ++
sidebars.ts | 43 +-
.../community-report-next/CommunityReportNext.scss | 176 +++++
.../community-report-next/CommunityReportNext.tsx | 489 ++++++++++++++
.../community-report-next/WeeklyReport.scss | 726 +++++++++++++++++++++
src/components/home-next/NavbarNext.tsx | 13 +-
src/pages/community-report/_reports/2026-06-29.mdx | 205 ++++++
src/pages/community-report/_reports/_TEMPLATE.mdx | 35 +
src/pages/community-report/index.tsx | 33 +
.../materialized-view/intro-link.mdx | 13 +
versioned_sidebars/version-4.x-sidebars.json | 55 +-
15 files changed, 1938 insertions(+), 50 deletions(-)
diff --git a/docs/query-acceleration/materialized-view/intro-link.mdx
b/docs/query-acceleration/materialized-view/intro-link.mdx
new file mode 100644
index 00000000000..0c7f678d3d8
--- /dev/null
+++ b/docs/query-acceleration/materialized-view/intro-link.mdx
@@ -0,0 +1,13 @@
+---
+{
+ "title": "Materialized View",
+ "language": "en",
+ "description": "Apache Doris materialized view chapter navigation: covers
concepts, synchronous materialized views, asynchronous materialized views,
transparent rewriting, and FAQs."
+}
+---
+
+import GettingStartedCard from
'@site/src/components/getting-started-card/getting-started-card';
+
+A materialized view is an entity that contains both computation logic and
data. You can use it for query acceleration, lightweight ETL modeling, and
lakehouse federated query acceleration. Start by understanding the concepts,
then choose between synchronous and asynchronous materialized views based on
your freshness requirements.
+
+Please refer to the [materialized view](./intro) related documentation.
diff --git
a/i18n/zh-CN/docusaurus-plugin-content-docs/current/query-acceleration/materialized-view/intro-link.mdx
b/i18n/zh-CN/docusaurus-plugin-content-docs/current/query-acceleration/materialized-view/intro-link.mdx
new file mode 100644
index 00000000000..a87f3adecc7
--- /dev/null
+++
b/i18n/zh-CN/docusaurus-plugin-content-docs/current/query-acceleration/materialized-view/intro-link.mdx
@@ -0,0 +1,13 @@
+---
+{
+ "title": "物化视图",
+ "language": "zh-CN",
+ "description": "Apache Doris 物化视图章节导航:覆盖概念、同步物化视图、异步物化视图、透明改写与常见问题。"
+}
+---
+
+import GettingStartedCard from
'@site/src/components/getting-started-card/getting-started-card';
+
+物化视图是既包含计算逻辑、也包含数据的实体,可用于查询加速、轻量化 ETL 建模以及湖仓联邦查询加速。建议先理解概念,再根据时效性需求选择同步或异步物化视图。
+
+请参阅[物化视图](./intro)相关文档。
diff --git
a/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
b/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
new file mode 100644
index 00000000000..a87f3adecc7
--- /dev/null
+++
b/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
@@ -0,0 +1,13 @@
+---
+{
+ "title": "物化视图",
+ "language": "zh-CN",
+ "description": "Apache Doris 物化视图章节导航:覆盖概念、同步物化视图、异步物化视图、透明改写与常见问题。"
+}
+---
+
+import GettingStartedCard from
'@site/src/components/getting-started-card/getting-started-card';
+
+物化视图是既包含计算逻辑、也包含数据的实体,可用于查询加速、轻量化 ETL 建模以及湖仓联邦查询加速。建议先理解概念,再根据时效性需求选择同步或异步物化视图。
+
+请参阅[物化视图](./intro)相关文档。
diff --git a/scripts/check_move.js b/scripts/check_move.js
index 198e645ce7e..3e4520c043d 100644
--- a/scripts/check_move.js
+++ b/scripts/check_move.js
@@ -95,13 +95,110 @@ function removeCodeBlocks(content) {
return result;
}
+function findEsmStatementEnd(content, start) {
+ let depth = 0;
+ let quote = null;
+ let escaping = false;
+ let inLineComment = false;
+ let inBlockComment = false;
+
+ for (let i = start; i < content.length; i++) {
+ const ch = content[i];
+ const next = content[i + 1];
+
+ if (quote) {
+ if (escaping) {
+ escaping = false;
+ } else if (ch === "\\") {
+ escaping = true;
+ } else if (ch === quote) {
+ quote = null;
+ }
+ continue;
+ }
+
+ if (inLineComment) {
+ if (ch === "\n") {
+ inLineComment = false;
+ }
+ continue;
+ }
+
+ if (inBlockComment) {
+ if (ch === "*" && next === "/") {
+ inBlockComment = false;
+ i++;
+ }
+ continue;
+ }
+
+ if (ch === "/" && next === "/") {
+ inLineComment = true;
+ i++;
+ continue;
+ }
+
+ if (ch === "/" && next === "*") {
+ inBlockComment = true;
+ i++;
+ continue;
+ }
+
+ if (ch === "\"" || ch === "'" || ch === "`") {
+ quote = ch;
+ continue;
+ }
+
+ if (ch === "{" || ch === "[" || ch === "(") {
+ depth++;
+ continue;
+ }
+
+ if (ch === "}" || ch === "]" || ch === ")") {
+ depth = Math.max(0, depth - 1);
+ continue;
+ }
+
+ if (ch === ";" && depth === 0) {
+ return i + 1;
+ }
+ }
+
+ return content.length;
+}
+
+function removeMdxEsmBlocks(content) {
+ const esmStartRegex = /^[
\t]*(?:import(?:\s|["'{*])|export\s+(?:const|let|var|function|class|default|\{|\*))/;
+ let result = "";
+ let cursor = 0;
+ let index = 0;
+
+ while (index < content.length) {
+ const lineEnd = content.indexOf("\n", index);
+ const end = lineEnd === -1 ? content.length : lineEnd;
+ const line = content.slice(index, end);
+
+ if (esmStartRegex.test(line)) {
+ const statementEnd = findEsmStatementEnd(content, index);
+ result += content.slice(cursor, index);
+ result += content.slice(index, statementEnd).replace(/[^\n]/g, "");
+ cursor = statementEnd;
+ index = statementEnd;
+ continue;
+ }
+
+ index = lineEnd === -1 ? content.length : lineEnd + 1;
+ }
+
+ return result + content.slice(cursor);
+}
// Check links in files
function checkFileLinks(filePath) {
const content = fs.readFileSync(filePath, "utf-8");
const dir = path.dirname(filePath);
- const cleanedContent = removeCodeBlocks(content);
+ const cleanedContent = removeMdxEsmBlocks(removeCodeBlocks(content));
const matches = [...cleanedContent.matchAll(linkRegex)];
for (const match of matches) {
const rawLink = match[1].split("#")[0]; // Remove anchor point
diff --git a/scripts/check_move.test.js b/scripts/check_move.test.js
new file mode 100644
index 00000000000..ee6a9265221
--- /dev/null
+++ b/scripts/check_move.test.js
@@ -0,0 +1,62 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const test = require('node:test');
+const { execFileSync, spawnSync } = require('node:child_process');
+
+const scriptPath = path.join(__dirname, 'check_move.js');
+
+function createRepo(files) {
+ const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'check-move-'));
+ execFileSync('git', ['init'], { cwd: repoDir, stdio: 'ignore' });
+ execFileSync('git', ['config', 'user.email', '[email protected]'], { cwd:
repoDir });
+ execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: repoDir });
+
+ for (const [filePath, content] of Object.entries(files)) {
+ const absolutePath = path.join(repoDir, filePath);
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
+ fs.writeFileSync(absolutePath, content);
+ }
+
+ execFileSync('git', ['add', '.'], { cwd: repoDir });
+ execFileSync('git', ['commit', '-m', 'fixture'], { cwd: repoDir, stdio:
'ignore' });
+ return repoDir;
+}
+
+test('ignores markdown-like PR titles inside MDX ESM exports', () => {
+ const repoDir = createRepo({
+ 'src/pages/community-report/_reports/2026-06-29.mdx': `
+export const report = {
+ "prs": [
+ {
+ "title": "[feature](be) Add file scanner v2 readers",
+ "url": "https://github.com/apache/doris/pull/65046"
+ }
+ ]
+};
+`,
+ });
+
+ const result = spawnSync(process.execPath, [scriptPath, 'HEAD'], {
+ cwd: repoDir,
+ encoding: 'utf8',
+ });
+
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+ assert.match(result.stdout, /All links are OK/);
+});
+
+test('reports broken local links in markdown prose', () => {
+ const repoDir = createRepo({
+ 'docs/example.mdx': 'See [missing page](missing-page).\n',
+ });
+
+ const result = spawnSync(process.execPath, [scriptPath, 'HEAD'], {
+ cwd: repoDir,
+ encoding: 'utf8',
+ });
+
+ assert.equal(result.status, 1, result.stdout);
+ assert.match(result.stderr, /Broken link -> missing-page/);
+});
diff --git a/sidebars.ts b/sidebars.ts
index 80034d7c609..d784a1e332e 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -505,6 +505,27 @@ const sidebars: SidebarsConfig = {
'query-data/lateral-view',
],
},
+ {
+ type: 'category',
+ label: 'Materialized View',
+ link: {type: 'doc', id:
'query-acceleration/materialized-view/intro'},
+ items: [
+ 'query-acceleration/materialized-view/overview',
+ 'query-acceleration/materialized-view/sync-materialized-view',
+
'query-acceleration/tuning/tuning-plan/transparent-rewriting-with-sync-mv',
+ {
+ type: 'category',
+ label: 'Async Materialized View',
+ link: {type: 'doc', id:
'query-acceleration/materialized-view/async-materialized-view/overview'},
+ items: [
+
'query-acceleration/materialized-view/async-materialized-view/functions-and-demands',
+
'query-acceleration/materialized-view/async-materialized-view/use-guide',
+
'query-acceleration/tuning/tuning-plan/transparent-rewriting-with-async-mv',
+
'query-acceleration/materialized-view/async-materialized-view/faq',
+ ],
+ },
+ ],
+ },
{
type: 'category',
label: 'Performance & Tuning',
@@ -535,27 +556,7 @@ const sidebars: SidebarsConfig = {
'query-acceleration/tuning/tuning-plan/optimizing-table-scanning',
],
},
- {
- type: 'category',
- label: 'Materialized View',
- link: {type: 'doc', id:
'query-acceleration/materialized-view/intro'},
- items: [
-
'query-acceleration/materialized-view/overview',
-
'query-acceleration/materialized-view/sync-materialized-view',
-
'query-acceleration/tuning/tuning-plan/transparent-rewriting-with-sync-mv',
- {
- type: 'category',
- label: 'Async Materialized View',
- link: {type: 'doc', id:
'query-acceleration/materialized-view/async-materialized-view/overview'},
- items: [
-
'query-acceleration/materialized-view/async-materialized-view/functions-and-demands',
-
'query-acceleration/materialized-view/async-materialized-view/use-guide',
-
'query-acceleration/tuning/tuning-plan/transparent-rewriting-with-async-mv',
-
'query-acceleration/materialized-view/async-materialized-view/faq',
- ],
- },
- ],
- },
+ 'query-acceleration/materialized-view/intro-link',
{
type: 'category',
label: 'Join Optimization',
diff --git a/src/components/community-report-next/CommunityReportNext.scss
b/src/components/community-report-next/CommunityReportNext.scss
new file mode 100644
index 00000000000..ea10173ba47
--- /dev/null
+++ b/src/components/community-report-next/CommunityReportNext.scss
@@ -0,0 +1,176 @@
+// Doc-style layout for the Community Report page: a sticky left sidebar
+// (Weekly Report section) plus a content area that renders the selected
report.
+
+.community-report {
+ --cr-green: #06805F;
+ --cr-green-dark: #054C39;
+ --cr-ink: #0F1A14;
+ --cr-muted: #5A6472;
+ --cr-paper: #FFFCF5;
+ --cr-cream: #FAF6EE;
+ --cr-line: rgba(15, 26, 20, 0.10);
+ --cr-yellow: #FFD23F;
+ --cr-sidebar-w: 280px;
+ --cr-navbar-h: 64px;
+
+ min-height: 100vh;
+ background: var(--cr-paper);
+ color: var(--cr-ink);
+ font-family: var(--font-family-base);
+
+ * {
+ box-sizing: border-box;
+ }
+}
+
+.community-report__layout {
+ max-width: 1280px;
+ margin: 0 auto;
+ display: flex;
+ align-items: flex-start;
+ gap: clamp(28px, 4vw, 56px);
+ padding: clamp(96px, 8vw, 120px) clamp(20px, 4vw, 56px) 96px;
+}
+
+/* ---------------- Sidebar ---------------- */
+
+.community-report__sidebar {
+ width: var(--cr-sidebar-w);
+ flex: 0 0 var(--cr-sidebar-w);
+ position: sticky;
+ top: calc(var(--cr-navbar-h) + 24px);
+ align-self: flex-start;
+}
+
+.community-report__section {
+ padding: 20px;
+ background: #fff;
+ border: 1px solid var(--cr-line);
+ border-radius: 14px;
+ box-shadow: 0 1px 4px rgba(0, 89, 68, 0.06);
+}
+
+.community-report__section-title {
+ margin: 0 0 14px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--cr-green);
+}
+
+.community-report__entry {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ text-align: left;
+ cursor: pointer;
+ padding: 12px 14px;
+ border: 1px solid transparent;
+ border-radius: 10px;
+ background: var(--cr-cream);
+ color: var(--cr-ink);
+ transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s
ease;
+}
+
+.community-report__entry:hover {
+ background: #F3ECDD;
+}
+
+.community-report__entry--active {
+ background: rgba(6, 128, 95, 0.08);
+ border-color: rgba(6, 128, 95, 0.35);
+ box-shadow: inset 3px 0 0 var(--cr-green);
+}
+
+.community-report__entry-badge {
+ align-self: flex-start;
+ padding: 1px 8px;
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--cr-green-dark);
+ background: var(--cr-yellow);
+}
+
+.community-report__entry-label {
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+/* ---------------- Past-reports dropdown ---------------- */
+
+.community-report__select-wrap {
+ position: relative;
+ margin-top: 12px;
+}
+
+.community-report__select-wrap::after {
+ // Custom chevron (native arrow is hidden via appearance:none).
+ content: '';
+ position: absolute;
+ right: 14px;
+ top: 50%;
+ width: 10px;
+ height: 10px;
+ transform: translateY(-60%) rotate(45deg);
+ border-right: 2px solid var(--cr-muted);
+ border-bottom: 2px solid var(--cr-muted);
+ pointer-events: none;
+}
+
+.community-report__select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ width: 100%;
+ cursor: pointer;
+ padding: 11px 36px 11px 14px;
+ font-family: var(--font-family-base);
+ font-size: 14px;
+ color: var(--cr-ink);
+ background: #fff;
+ border: 1px solid var(--cr-line);
+ border-radius: 10px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.community-report__select:hover {
+ border-color: rgba(6, 128, 95, 0.4);
+}
+
+.community-report__select:focus-visible {
+ outline: none;
+ border-color: var(--cr-green);
+ box-shadow: 0 0 0 3px rgba(6, 128, 95, 0.15);
+}
+
+/* ---------------- Content ---------------- */
+
+.community-report__content {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding-top: 4px;
+}
+
+/* ---------------- Responsive ---------------- */
+
+@media (max-width: 900px) {
+ .community-report__layout {
+ flex-direction: column;
+ gap: 28px;
+ padding-top: clamp(84px, 16vw, 100px);
+ }
+
+ .community-report__sidebar {
+ position: static;
+ width: 100%;
+ flex-basis: auto;
+ }
+}
diff --git a/src/components/community-report-next/CommunityReportNext.tsx
b/src/components/community-report-next/CommunityReportNext.tsx
new file mode 100644
index 00000000000..f913a2610ad
--- /dev/null
+++ b/src/components/community-report-next/CommunityReportNext.tsx
@@ -0,0 +1,489 @@
+import React, { JSX, useEffect, useState } from 'react';
+import { LayoutNext } from '../home-next/LayoutNext';
+import './CommunityReportNext.scss';
+import './WeeklyReport.scss';
+
+export interface WeeklyReportStat {
+ num: string;
+ label: string;
+}
+
+/** One PR / issue reference, rendered as a linked `#<num>`. */
+export interface WeeklyReportRef {
+ num: number;
+ title: string;
+ url: string;
+ /** Open PR opened within the report week (in-progress only). */
+ isNew?: boolean;
+}
+
+/** One scenario line inside the new community-radar website export. */
+export interface WeeklyReportScenario {
+ name: string;
+ mergedNarrative?: string;
+ inProgressNarrative?: string;
+ merged?: WeeklyReportRef[];
+ inProgress?: WeeklyReportRef[];
+}
+
+/** Legacy section shape from the first structured MDX export. */
+export interface WeeklyReportLegacyScenario {
+ name: string;
+ narrative?: string;
+ prs?: WeeklyReportRef[];
+ refs?: WeeklyReportRef[];
+}
+
+export interface WeeklyReportHighlight {
+ title: string;
+ narrative: string;
+ prs?: WeeklyReportRef[];
+}
+
+export interface WeeklyReportDemand {
+ name: string;
+ narrative?: string;
+ refs?: WeeklyReportRef[];
+}
+
+/** One repository's chapter: the three scenario sections. */
+export interface WeeklyReportRepo {
+ repo: string;
+ scenarios?: WeeklyReportScenario[];
+ demand?: WeeklyReportDemand[];
+ /** Backward-compatible fields for already-published structured reports. */
+ merged?: WeeklyReportLegacyScenario[];
+ inProgress?: WeeklyReportLegacyScenario[];
+}
+
+/**
+ * Structured report body exported by community-radar (`export const report`).
+ * When present it is laid out by this component; otherwise the compiled
Markdown
+ * body is rendered instead (the legacy / hand-authored path).
+ */
+export interface WeeklyReportData {
+ summary: {
+ lead: string;
+ highlights: Array<string | WeeklyReportHighlight>;
+ numbers?: { mergedPrs: number; newIssues: number; contributors: number
};
+ };
+ repos: WeeklyReportRepo[];
+}
+
+export interface WeeklyReportEntry {
+ /** Stable id (the markdown filename without extension); used as the URL
hash. */
+ id: string;
+ /** Date range shown in the sidebar and page title, e.g. "Jun 29 – Jul 5,
2026". */
+ label: string;
+ /** Optional subtitle, e.g. "Week 27, 2026". */
+ week?: string;
+ /** Optional summary cards rendered above the report body. */
+ stats?: WeeklyReportStat[];
+ /** Structured body (from community-radar's export); takes precedence over
Component. */
+ report?: WeeklyReportData;
+ /** The compiled markdown body as a React component (fallback when no
`report`). */
+ Component: React.ComponentType;
+}
+
+interface CommunityReportNextProps {
+ /** All reports, ordered newest-first (see the page's loadReports()). */
+ reports: WeeklyReportEntry[];
+}
+
+export default function CommunityReportNext({ reports }:
CommunityReportNextProps): JSX.Element {
+ const latest = reports[0];
+ const historical = reports.slice(1);
+ const [selectedId, setSelectedId] = useState<string>(latest?.id ?? '');
+
+ // Allow deep-linking to a specific report via URL hash, e.g.
+ // /community-report#2026-06-22. Read on mount (client-only; window is
+ // undefined during SSR) and fall back to the latest report.
+ useEffect(() => {
+ const hash = window.location.hash.replace(/^#/, '');
+ if (hash && reports.some(r => r.id === hash)) {
+ setSelectedId(hash);
+ }
+ }, [reports]);
+
+ const select = (id: string) => {
+ setSelectedId(id);
+ if (typeof window !== 'undefined') {
+ window.history.replaceState(null, '', `#${id}`);
+ }
+ };
+
+ const selected = reports.find(r => r.id === selectedId) ?? latest;
+ const isHistoricalSelected = historical.some(r => r.id === selectedId);
+ const SelectedReport = selected?.Component;
+
+ return (
+ <LayoutNext
+ title="Community Report — Apache Doris"
+ description="Weekly reports from the Apache Doris community:
merged features, fixes, new contributors, and notable discussions."
+ >
+ <div className="community-report">
+ <div className="community-report__layout">
+ <aside className="community-report__sidebar"
aria-label="Reports">
+ <div className="community-report__section">
+ <h2
className="community-report__section-title">Weekly Report</h2>
+
+ {latest && (
+ <button
+ type="button"
+ className={
+ 'community-report__entry' +
+ (selectedId === latest.id ? '
community-report__entry--active' : '')
+ }
+ onClick={() => select(latest.id)}
+ >
+ <span
className="community-report__entry-badge">Latest</span>
+ <span
className="community-report__entry-label">{latest.label}</span>
+ </button>
+ )}
+
+ {historical.length > 0 && (
+ <div className="community-report__select-wrap">
+ <select
+ className="community-report__select"
+ aria-label="Browse past weekly reports"
+ value={isHistoricalSelected ?
selectedId : ''}
+ onChange={e => select(e.target.value)}
+ >
+ <option value="" disabled>
+ Past reports…
+ </option>
+ {historical.map(r => (
+ <option key={r.id} value={r.id}>
+ {r.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ )}
+ </div>
+ </aside>
+
+ <main className="community-report__content">
+ {selected && (
+ <article className="weekly-report">
+ <header className="weekly-report__header">
+ <p
className="weekly-report__eyebrow">Community Weekly Report</p>
+ <h1
className="weekly-report__title">{selected.label}</h1>
+ {selected.week && <p
className="weekly-report__meta">{selected.week}</p>}
+ </header>
+
+ {selected.stats && selected.stats.length > 0
&& !selected.report?.summary.numbers && (
+ <div className="weekly-report__stats">
+ {selected.stats.map((stat, i) => (
+ <div
className="weekly-report__stat" key={`${stat.label}-${i}`}>
+ <span
className="weekly-report__stat-num">{stat.num}</span>
+ <span
className="weekly-report__stat-label">{stat.label}</span>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {selected.report ? (
+ <StructuredBody key={selected.id}
report={selected.report} />
+ ) : (
+ <div className="weekly-report__body">
+ {SelectedReport && <SelectedReport />}
+ </div>
+ )}
+ </article>
+ )}
+ </main>
+ </div>
+ </div>
+ </LayoutNext>
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Structured body — the community-radar website layout:
+// overview -> highlights -> by scenario -> community pulse -> demand signals.
+// It is rendered under this site's visual language. Hand-authored Markdown
+// reports fall back to __body above.
+// ---------------------------------------------------------------------------
+
+function StructuredBody({ report }: { report: WeeklyReportData }): JSX.Element
{
+ const { summary, repos } = report;
+ const [activeRepo, setActiveRepo] = useState(0);
+ const [activeHighlight, setActiveHighlight] = useState<number |
null>(null);
+ const [activeScenario, setActiveScenario] = useState(0);
+ const repo = repos[Math.min(activeRepo, repos.length - 1)];
+ const highlights = summary.highlights ?? [];
+ const scenarios = repo ? normalizeScenarios(repo) : [];
+ const activeScenarioIndex = scenarios.length > 0 ?
Math.min(activeScenario, scenarios.length - 1) : 0;
+ const selectedScenario = scenarios[activeScenarioIndex];
+
+ return (
+ <div className="wr-structured">
+ {summary.lead && (
+ <section className="wr-section">
+ <SectionTitle index="00" title="Overview" />
+ <p className="wr-pulse__lead">{summary.lead}</p>
+ </section>
+ )}
+
+ {highlights.length > 0 && (
+ <section className="wr-section">
+ <SectionTitle index="01" title="Highlights" />
+ <div className="wr-highlights">
+ {highlights.map((h, i) => (
+ <HighlightCard
+ key={i}
+ highlight={h}
+ index={i + 1}
+ expanded={activeHighlight === i}
+ onToggle={() =>
setActiveHighlight(activeHighlight === i ? null : i)}
+ />
+ ))}
+ </div>
+ </section>
+ )}
+
+ {repos.length > 1 && (
+ <nav className="wr-tabs" aria-label="Repositories">
+ {repos.map((r, i) => (
+ <button
+ key={r.repo}
+ type="button"
+ className={'wr-tab' + (i === activeRepo ? '
wr-tab--active' : '')}
+ onClick={() => {
+ setActiveRepo(i);
+ setActiveScenario(0);
+ }}
+ >
+ {r.repo}
+ </button>
+ ))}
+ </nav>
+ )}
+
+ {repo && (
+ <>
+ <section className="wr-section">
+ <SectionTitle index="02" title="By Scenario" />
+ {scenarios.length === 0 ? (
+ <p className="wr-empty">Nothing to report this
week.</p>
+ ) : (
+ <div className="wr-scenarios-tabs">
+ <div className="wr-scenario-tabs"
role="tablist" aria-label={`${repo.repo} scenarios`}>
+ {scenarios.map((s, i) => (
+ <button
+ key={s.name + i}
+ type="button"
+ id={`wr-scenario-tab-${i}`}
+ role="tab"
+ aria-selected={i ===
activeScenarioIndex}
+
aria-controls={`wr-scenario-panel-${i}`}
+ className={
+ 'wr-scenario-tab' +
+ (i === activeScenarioIndex ? '
wr-scenario-tab--active' : '')
+ }
+ onClick={() =>
setActiveScenario(i)}
+ >
+ <span
className="wr-scenario-tab__idx">
+ {String(i + 1).padStart(2,
'0')}
+ </span>
+ <span
className="wr-scenario-tab__label">{s.name}</span>
+ </button>
+ ))}
+ </div>
+ {selectedScenario && (
+ <div
+
id={`wr-scenario-panel-${activeScenarioIndex}`}
+ role="tabpanel"
+
aria-labelledby={`wr-scenario-tab-${activeScenarioIndex}`}
+ className="wr-scenario-panel"
+ >
+ <ScenarioCard
scenario={selectedScenario} index={activeScenarioIndex + 1} />
+ </div>
+ )}
+ </div>
+ )}
+ </section>
+
+ {summary.numbers && (
+ <section className="wr-section">
+ <SectionTitle index="03" title="Community Pulse" />
+ <div className="wr-pulse">
+ <PulseFigure num={summary.numbers.mergedPrs}
label="Merged PRs" />
+ <PulseFigure num={summary.numbers.newIssues}
label="New issues" />
+ <PulseFigure
num={summary.numbers.contributors} label="Contributors" />
+ </div>
+ </section>
+ )}
+ </>
+ )}
+ </div>
+ );
+}
+
+function SectionTitle({ index, title }: { index: string; title: string }):
JSX.Element {
+ return (
+ <h2 className="wr-section__title">
+ <span className="wr-section__index">{index}</span>
+ {title}
+ </h2>
+ );
+}
+
+function HighlightCard({
+ highlight,
+ index,
+ expanded,
+ onToggle,
+}: {
+ highlight: string | WeeklyReportHighlight;
+ index: number;
+ expanded: boolean;
+ onToggle: () => void;
+}): JSX.Element {
+ const title = typeof highlight === 'string' ? highlight : highlight.title;
+ const narrative = typeof highlight === 'string' ? undefined :
highlight.narrative;
+ const prs = typeof highlight === 'string' ? [] : highlight.prs ?? [];
+ const panelId = `wr-highlight-panel-${index}`;
+
+ if (typeof highlight === 'string') {
+ return (
+ <article className="wr-highlight">
+ <button type="button" className="wr-highlight__trigger"
disabled>
+ <span
className="wr-highlight__idx">{String(index).padStart(2, '0')}</span>
+ <span className="wr-highlight__title">{title}</span>
+ </button>
+ </article>
+ );
+ }
+
+ return (
+ <article className={'wr-highlight' + (expanded ? ' wr-highlight--open'
: '')}>
+ <h3 className="wr-highlight__heading">
+ <button
+ type="button"
+ className="wr-highlight__trigger"
+ aria-expanded={expanded}
+ aria-controls={panelId}
+ onClick={onToggle}
+ >
+ <span
className="wr-highlight__idx">{String(index).padStart(2, '0')}</span>
+ <span className="wr-highlight__title">{title}</span>
+ <span className="wr-highlight__chevron" aria-hidden="true"
/>
+ </button>
+ </h3>
+ {expanded && (
+ <div className="wr-highlight__panel" id={panelId}>
+ {narrative && <p
className="wr-highlight__text">{narrative}</p>}
+ <RefList items={prs} />
+ </div>
+ )}
+ </article>
+ );
+}
+
+function PulseFigure({ num, label }: { num: number; label: string }):
JSX.Element {
+ return (
+ <div className="wr-pulse__figure">
+ <span className="wr-pulse__num">{num}</span>
+ <span className="wr-pulse__label">{label}</span>
+ </div>
+ );
+}
+
+function normalizeScenarios(repo: WeeklyReportRepo): WeeklyReportScenario[] {
+ if (repo.scenarios) {
+ return repo.scenarios;
+ }
+ const byName = new Map<string, WeeklyReportScenario>();
+
+ const ensure = (name: string): WeeklyReportScenario => {
+ const existing = byName.get(name);
+ if (existing) {
+ return existing;
+ }
+ const next = { name };
+ byName.set(name, next);
+ return next;
+ };
+
+ (repo.merged ?? []).forEach(s => {
+ const target = ensure(s.name);
+ target.mergedNarrative = s.narrative;
+ target.merged = s.prs ?? [];
+ });
+ (repo.inProgress ?? []).forEach(s => {
+ const target = ensure(s.name);
+ target.inProgressNarrative = s.narrative;
+ target.inProgress = s.prs ?? [];
+ });
+
+ return Array.from(byName.values());
+}
+
+function ScenarioCard({
+ scenario,
+ index,
+}: {
+ scenario: WeeklyReportScenario;
+ index: number;
+}): JSX.Element {
+ return (
+ <article className="wr-scn">
+ <div className="wr-scn__head">
+ <h3 className="wr-scn__name">{scenario.name}</h3>
+ <span className="wr-scn__idx">{String(index).padStart(2,
'0')}</span>
+ </div>
+ {scenario.mergedNarrative && (
+ <ScenarioBlock label="Merged"
narrative={scenario.mergedNarrative} items={scenario.merged ?? []} />
+ )}
+ {scenario.inProgressNarrative && (
+ <ScenarioBlock
+ label="In progress"
+ narrative={scenario.inProgressNarrative}
+ items={scenario.inProgress ?? []}
+ />
+ )}
+ </article>
+ );
+}
+
+function ScenarioBlock({
+ label,
+ narrative,
+ items,
+}: {
+ label: string;
+ narrative: string;
+ items: WeeklyReportRef[];
+}): JSX.Element {
+ return (
+ <div className="wr-scn__block">
+ <div className="wr-scn__label">{label}</div>
+ <p className="wr-scn__narr">{narrative}</p>
+ <RefList items={items} />
+ </div>
+ );
+}
+
+function RefList({ items }: { items: WeeklyReportRef[] }): JSX.Element | null {
+ if (items.length === 0) {
+ return null;
+ }
+
+ return (
+ <ul className="wr-scn__prs">
+ {items.map(it => (
+ <li className="wr-scn__pr" key={it.num}>
+ <span className="wr-scn__pr-title">{it.title}</span>
+ <span className="wr-scn__pr-meta">
+ {it.isNew && <span className="wr-scn__new">Newly
opened</span>}
+ <a className="wr-scn__pr-num" href={it.url}
target="_blank" rel="noopener noreferrer">
+ #{it.num}
+ </a>
+ </span>
+ </li>
+ ))}
+ </ul>
+ );
+}
diff --git a/src/components/community-report-next/WeeklyReport.scss
b/src/components/community-report-next/WeeklyReport.scss
new file mode 100644
index 00000000000..44222f17901
--- /dev/null
+++ b/src/components/community-report-next/WeeklyReport.scss
@@ -0,0 +1,726 @@
+// Unified styles for every weekly report.
+//
+// The header and stat cards are rendered by CommunityReportNext.tsx from each
+// report's frontmatter (label / week / stats). The `__body` block wraps the
+// Markdown compiled by Docusaurus's MDX loader, so its rules target the plain
+// semantic tags (h2, ul, p, code, table…) that Markdown produces.
+
+.weekly-report {
+ --wr-green: #06805F;
+ --wr-green-dark: #054C39;
+ --wr-ink: #0F1A14;
+ --wr-muted: #5A6472;
+ --wr-paper: #FFFCF5;
+ --wr-cream: #FAF6EE;
+ --wr-line: rgba(15, 26, 20, 0.10);
+ --wr-yellow: #FFD23F;
+
+ color: var(--wr-ink);
+ font-family: var(--font-family-base);
+ max-width: 820px;
+
+ * {
+ box-sizing: border-box;
+ }
+}
+
+/* ---------------- Header (from frontmatter) ---------------- */
+
+.weekly-report__header {
+ padding-bottom: 28px;
+ border-bottom: 1px solid var(--wr-line);
+ margin-bottom: 32px;
+}
+
+.weekly-report__eyebrow {
+ margin: 0 0 10px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--wr-green);
+}
+
+.weekly-report__title {
+ margin: 0;
+ font-size: clamp(28px, 2rem + 1vw, 40px);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ line-height: 1.1;
+ color: var(--wr-ink);
+}
+
+.weekly-report__meta {
+ margin: 10px 0 0;
+ font-size: 14px;
+ color: var(--wr-muted);
+}
+
+/* ---------------- Stats (from frontmatter) ---------------- */
+
+.weekly-report__stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ margin-bottom: 40px;
+}
+
+.weekly-report__stat {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 18px 20px;
+ background: var(--wr-cream);
+ border: 1px solid var(--wr-line);
+ border-radius: 12px;
+}
+
+.weekly-report__stat-num {
+ font-family: var(--font-mono);
+ font-size: 30px;
+ font-weight: 700;
+ line-height: 1;
+ color: var(--wr-green);
+}
+
+.weekly-report__stat-label {
+ font-size: 13px;
+ color: var(--wr-muted);
+}
+
+/* ---------------- Markdown body ---------------- */
+
+.weekly-report__body {
+ font-size: 15px;
+ line-height: 1.7;
+ color: #2A3441;
+}
+
+.weekly-report__body > :first-child {
+ margin-top: 0;
+}
+
+.weekly-report__body h2 {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 34px 0 16px;
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ color: var(--wr-ink);
+ scroll-margin-top: 88px;
+}
+
+.weekly-report__body h2::before {
+ content: '';
+ flex: 0 0 auto;
+ width: 8px;
+ height: 20px;
+ border-radius: 3px;
+ background: var(--wr-yellow);
+}
+
+.weekly-report__body h3 {
+ margin: 26px 0 12px;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--wr-ink);
+ scroll-margin-top: 88px;
+}
+
+.weekly-report__body p {
+ margin: 0 0 16px;
+}
+
+.weekly-report__body ul,
+.weekly-report__body ol {
+ margin: 0 0 16px;
+ padding-left: 22px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.weekly-report__body li {
+ line-height: 1.6;
+}
+
+.weekly-report__body li::marker {
+ color: var(--wr-green);
+}
+
+.weekly-report__body a {
+ color: var(--wr-green);
+ text-decoration: none;
+ border-bottom: 1px solid rgba(6, 128, 95, 0.3);
+ transition: border-color 0.15s ease;
+}
+
+.weekly-report__body a:hover {
+ border-bottom-color: var(--wr-green);
+}
+
+.weekly-report__body strong {
+ font-weight: 700;
+ color: var(--wr-ink);
+}
+
+.weekly-report__body em {
+ font-style: italic;
+ color: #B08800;
+}
+
+// Inline code. Fenced code blocks are rendered by Docusaurus's own CodeBlock
+// theme component, so they keep the site-wide code styling untouched here.
+.weekly-report__body :not(pre) > code {
+ font-family: var(--font-mono);
+ font-size: 0.88em;
+ padding: 1px 6px;
+ border-radius: 4px;
+ background: rgba(15, 26, 20, 0.06);
+ color: var(--wr-green-dark);
+}
+
+.weekly-report__body blockquote {
+ margin: 0 0 18px;
+ padding: 4px 16px;
+ border-left: 3px solid var(--wr-green);
+ background: var(--wr-cream);
+ border-radius: 0 8px 8px 0;
+ color: var(--wr-muted);
+}
+
+.weekly-report__body table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 0 0 18px;
+ font-size: 14px;
+ display: block;
+ overflow-x: auto;
+}
+
+.weekly-report__body th,
+.weekly-report__body td {
+ text-align: left;
+ padding: 8px 12px;
+ border: 1px solid var(--wr-line);
+}
+
+.weekly-report__body th {
+ background: var(--wr-cream);
+ font-weight: 700;
+}
+
+.weekly-report__body img {
+ max-width: 100%;
+ border-radius: 8px;
+}
+
+.weekly-report__body hr {
+ border: 0;
+ border-top: 1px solid var(--wr-line);
+ margin: 28px 0;
+}
+
+/* ---------------- Structured body ---------------- */
+//
+// Rendered by StructuredBody (CommunityReportNext.tsx) from `export const
report`.
+// The information architecture mirrors community-radar's report flow while the
+// colors, typography scale, and card treatment stay aligned with
doris.apache.org.
+
+.wr-section {
+ margin: 0;
+}
+
+.wr-section__title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 48px 0 20px;
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: 0;
+ color: var(--wr-ink);
+}
+
+.wr-section__title::before {
+ content: '';
+ flex: 0 0 auto;
+ width: 22px;
+ height: 3px;
+ border-radius: 999px;
+ background: var(--wr-yellow);
+}
+
+.wr-section__index {
+ font-family: var(--font-mono);
+ font-size: 1em;
+ line-height: 1;
+ letter-spacing: 0.08em;
+ color: var(--wr-green);
+}
+
+.wr-structured > .wr-section:first-child .wr-section__title {
+ margin-top: 0;
+}
+
+.wr-empty {
+ margin: 0;
+ font-size: 14px;
+ font-style: italic;
+ color: var(--wr-muted);
+}
+
+/* Overview */
+
+.wr-pulse__lead {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 1.65;
+ color: var(--wr-ink);
+}
+
+/* Highlights */
+
+.wr-highlights {
+ display: grid;
+ gap: 10px;
+}
+
+.wr-highlight {
+ background: var(--wr-green);
+ border: 1px solid rgba(5, 76, 57, 0.22);
+ border-radius: 10px;
+ box-shadow: 0 8px 24px rgba(0, 89, 68, 0.12);
+ overflow: hidden;
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, background
0.18s ease;
+}
+
+.wr-highlight--open {
+ border-color: rgba(5, 76, 57, 0.34);
+ box-shadow: 0 12px 28px rgba(0, 89, 68, 0.16);
+}
+
+.wr-highlight__heading {
+ margin: 0;
+}
+
+.wr-highlight__trigger {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 14px;
+ width: 100%;
+ padding: 17px 20px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.wr-highlight__trigger:disabled {
+ cursor: default;
+}
+
+.wr-highlight__trigger:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 3px rgba(255, 210, 63, 0.36);
+}
+
+.wr-highlight__idx {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--wr-yellow);
+}
+
+.wr-highlight__title {
+ margin: 0;
+ font-size: 16px;
+ line-height: 1.35;
+ color: var(--wr-paper);
+}
+
+.wr-highlight__chevron {
+ width: 9px;
+ height: 9px;
+ border-right: 2px solid var(--wr-paper);
+ border-bottom: 2px solid var(--wr-paper);
+ transform: rotate(45deg);
+ transition: transform 0.18s ease;
+}
+
+.wr-highlight--open .wr-highlight__chevron {
+ transform: rotate(225deg);
+}
+
+.wr-highlight__panel {
+ padding: 0 20px 20px 47px;
+}
+
+.wr-highlight__text {
+ margin: 0;
+ font-size: 15px;
+ line-height: 1.68;
+ color: rgba(255, 252, 245, 0.88);
+}
+
+.wr-highlight__panel .wr-scn__prs {
+ margin-top: 14px;
+ border-top-color: rgba(255, 252, 245, 0.18);
+}
+
+.wr-highlight__panel .wr-scn__pr {
+ border-top-color: rgba(255, 252, 245, 0.18);
+}
+
+.wr-highlight__panel .wr-scn__pr-title {
+ color: rgba(255, 252, 245, 0.92);
+}
+
+.wr-highlight__panel .wr-scn__pr-num {
+ color: var(--wr-yellow);
+ border-bottom-color: rgba(255, 210, 63, 0.5);
+}
+
+.wr-highlight__panel .wr-scn__pr-num:hover {
+ border-bottom-color: var(--wr-yellow);
+}
+
+.wr-highlight__panel .wr-scn__new {
+ color: var(--wr-paper);
+ background: rgba(255, 252, 245, 0.16);
+}
+
+/* Repo tabs (only shown when a report covers more than one repository) */
+
+.wr-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ margin: 8px 0 28px;
+ border-bottom: 1px solid var(--wr-line);
+}
+
+.wr-tab {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ letter-spacing: 0.02em;
+ border: 0;
+ background: transparent;
+ color: var(--wr-muted);
+ padding: 11px 16px;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ transition: color 0.15s ease, border-color 0.15s ease;
+}
+
+.wr-tab:hover {
+ color: var(--wr-green);
+}
+
+.wr-tab--active {
+ color: var(--wr-ink);
+ border-bottom-color: var(--wr-green);
+ font-weight: 700;
+}
+
+/* Scenario tabs */
+
+.wr-scenarios-tabs {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.wr-scenario-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 6px;
+ background: rgba(250, 246, 238, 0.72);
+ border: 1px solid var(--wr-line);
+ border-radius: 12px;
+}
+
+.wr-scenario-tab {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 38px;
+ max-width: 100%;
+ padding: 8px 12px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--wr-muted);
+ font-family: var(--font-family-base);
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1.2;
+ text-align: left;
+ cursor: pointer;
+ transition: background 0.16s ease, border-color 0.16s ease, color 0.16s
ease, box-shadow 0.16s ease;
+}
+
+.wr-scenario-tab:hover {
+ color: var(--wr-green-dark);
+ background: rgba(255, 255, 255, 0.72);
+}
+
+.wr-scenario-tab:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(6, 128, 95, 0.16);
+}
+
+.wr-scenario-tab--active {
+ color: var(--wr-paper);
+ background: var(--wr-green);
+ border-color: rgba(5, 76, 57, 0.28);
+ box-shadow: 0 8px 18px rgba(0, 89, 68, 0.16);
+}
+
+.wr-scenario-tab__idx {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--wr-green);
+}
+
+.wr-scenario-tab--active .wr-scenario-tab__idx {
+ color: var(--wr-yellow);
+}
+
+.wr-scenario-tab__label {
+ min-width: 0;
+}
+
+.wr-scenario-panel {
+ min-width: 0;
+}
+
+/* Scenario cards */
+
+.wr-scn {
+ background: #fff;
+ border: 1px solid var(--wr-line);
+ border-top: 3px solid var(--wr-green);
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: 0 1px 4px rgba(0, 89, 68, 0.06);
+}
+
+.wr-scn__head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.wr-scn__name {
+ margin: 0;
+ font-size: 17px;
+ font-weight: 700;
+ line-height: 1.25;
+ color: var(--wr-ink);
+}
+
+.wr-scn__idx {
+ font-family: var(--font-mono);
+ font-size: 22px;
+ font-weight: 700;
+ line-height: 1;
+ color: var(--wr-green);
+ opacity: 0.35;
+}
+
+.wr-scn__block {
+ margin-top: 18px;
+ padding-top: 18px;
+ border-top: 1px solid var(--wr-line);
+}
+
+.wr-scn__block:first-of-type {
+ margin-top: 16px;
+}
+
+.wr-scn__label {
+ display: inline-flex;
+ align-items: center;
+ min-height: 22px;
+ padding: 2px 10px;
+ border-radius: 999px;
+ background: var(--wr-cream);
+ color: var(--wr-green-dark);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.wr-scn__narr {
+ margin: 12px 0 0;
+ font-size: 15px;
+ line-height: 1.7;
+ color: #2A3441;
+}
+
+.wr-scn__prs {
+ list-style: none;
+ margin: 16px 0 0;
+ padding: 16px 0 0;
+ border-top: 1px solid var(--wr-line);
+ display: flex;
+ flex-direction: column;
+}
+
+.wr-scn__pr {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 9px 0;
+ border-top: 1px dashed var(--wr-line);
+}
+
+.wr-scn__pr:first-child {
+ border-top: 0;
+ padding-top: 0;
+}
+
+.wr-scn__pr-title {
+ flex: 1;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--wr-ink);
+}
+
+.wr-scn__pr-meta {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+}
+
+.wr-scn__pr-num {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ font-weight: 700;
+ color: var(--wr-green);
+ text-decoration: none;
+ border-bottom: 1px solid rgba(6, 128, 95, 0.3);
+ transition: border-color 0.15s ease;
+}
+
+.wr-scn__pr-num:hover {
+ border-bottom-color: var(--wr-green);
+}
+
+.wr-scn__new {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--wr-green-dark);
+ background: rgba(6, 128, 95, 0.12);
+ border-radius: 999px;
+ padding: 2px 8px;
+ white-space: nowrap;
+}
+
+/* Community Pulse */
+
+.wr-pulse {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ padding: 26px 28px;
+ background: var(--wr-green-dark);
+ border-radius: 14px;
+}
+
+.wr-pulse__figure {
+ border-left: 1px solid rgba(255, 252, 245, 0.18);
+ padding-left: 20px;
+}
+
+.wr-pulse__figure:first-child {
+ border-left: 0;
+ padding-left: 0;
+}
+
+.wr-pulse__num {
+ display: block;
+ font-family: var(--font-mono);
+ font-size: 36px;
+ font-weight: 700;
+ line-height: 1;
+ color: var(--wr-yellow);
+}
+
+.wr-pulse__label {
+ display: block;
+ margin-top: 10px;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: rgba(255, 252, 245, 0.72);
+}
+
+/* ---------------- Responsive ---------------- */
+
+@media (max-width: 640px) {
+ .weekly-report__stats {
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+
+ .wr-scn {
+ padding: 18px 18px;
+ }
+
+ .wr-scn__pr {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 6px;
+ }
+
+ .wr-highlight {
+ border-radius: 10px;
+ }
+
+ .wr-highlight__trigger {
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 10px;
+ padding: 15px 16px;
+ }
+
+ .wr-highlight__panel {
+ padding: 0 16px 17px 42px;
+ }
+
+ .wr-scenario-tabs {
+ gap: 6px;
+ }
+
+ .wr-scenario-tab {
+ width: 100%;
+ }
+
+ .wr-pulse {
+ grid-template-columns: 1fr;
+ }
+
+ .wr-pulse__figure,
+ .wr-pulse__figure:first-child {
+ border-left: 0;
+ padding-left: 0;
+ }
+}
diff --git a/src/components/home-next/NavbarNext.tsx
b/src/components/home-next/NavbarNext.tsx
index b56d49cab30..95dd70cf37d 100644
--- a/src/components/home-next/NavbarNext.tsx
+++ b/src/components/home-next/NavbarNext.tsx
@@ -27,6 +27,7 @@ function buildNavItems(
v21DocsHref: string,
releasesHref: string,
joinCommunityHref: string,
+ communityReportHref: string,
): NavItem[] {
return [
{
@@ -67,6 +68,7 @@ function buildNavItems(
{
label: 'Community',
items: [
+ { label: 'Community Report', href: communityReportHref },
{ label: 'Roadmap', href:
'https://github.com/apache/doris/issues/60036', external: true },
{ label: 'Build with Us', href: joinCommunityHref },
{ label: 'Join GitHub Discussions', href:
'https://github.com/apache/doris/discussions', external: true },
@@ -124,7 +126,16 @@ export function NavbarNext(): JSX.Element {
const v21DocsHref =
`${localePrefix}/docs/2.1/gettingStarted/what-is-apache-doris`;
const releasesHref = `${localePrefix}/releases/all-release`;
const joinCommunityHref = `${localePrefix}/community/join-community`;
- const navItems = buildNavItems(devDocsHref, stableDocsHref, v3xDocsHref,
v21DocsHref, releasesHref, joinCommunityHref);
+ const communityReportHref = `${localePrefix}/community-report`;
+ const navItems = buildNavItems(
+ devDocsHref,
+ stableDocsHref,
+ v3xDocsHref,
+ v21DocsHref,
+ releasesHref,
+ joinCommunityHref,
+ communityReportHref,
+ );
const [expandedMobileItem, setExpandedMobileItem] =
useState(navItems[0]?.label ?? '');
const homeHref = `${getLocalePrefix(currentLocale, defaultLocale)}/`;
diff --git a/src/pages/community-report/_reports/2026-06-29.mdx
b/src/pages/community-report/_reports/2026-06-29.mdx
new file mode 100644
index 00000000000..af9553c6d87
--- /dev/null
+++ b/src/pages/community-report/_reports/2026-06-29.mdx
@@ -0,0 +1,205 @@
+{/* Auto-generated by community-radar `report export-mdx` — do not edit by
hand.
+ `report` is English-only structured data laid out by doris-website. */}
+
+export const label = "Jun 29 – Jul 5, 2026";
+export const week = "Week 27, 2026";
+export const stats = [
+ {
+ "num": "113",
+ "label": "Merged PRs"
+ },
+ {
+ "num": "5",
+ "label": "New issues"
+ },
+ {
+ "num": "42",
+ "label": "Contributors"
+ }
+];
+
+export const report = {
+ "summary": {
+ "lead": "This week Apache Doris merged 113 PRs with 42 contributors
active, with the community concentrating on real-time data warehousing and
kernel engineering while advancing the multi-modal lakehouse and cloud-native
compute-storage separation tracks. Core capabilities deepened around Catalog
SPI decoupling, scan execution optimization, inverted index evolution, and peer
routing with cache tiering, alongside reinforced security/governance and
observability—signaling a clear direc [...]
+ "highlights": [
+ {
+ "title": "Catalog SPI decoupling and external data source expansion",
+ "narrative": "FE connectors move to a loadable Catalog SPI, easing new
data-source onboarding and upgrades, while BE gains a unified File Scanner v2
for Parquet, CSV, JSON and similar external files. Paimon write support and
OIDC session credentials for Iceberg REST close the lakehouse read-write and
secure-access loop, making Doris easier",
+ "prs": [
+ {
+ "num": 65185,
+ "title": "[Tracking][Catalog] Catalog SPI migration — decouple
built-in connectors from FE core into loadable plugins",
+ "url": "https://github.com/apache/doris/issues/65185"
+ },
+ {
+ "num": 65086,
+ "title": "Support writing to Apache Paimon tables",
+ "url": "https://github.com/apache/doris/issues/65086"
+ },
+ {
+ "num": 65046,
+ "title": "[feature](be) Add file scanner v2 readers ",
+ "url": "https://github.com/apache/doris/pull/65046"
+ },
+ {
+ "num": 63068,
+ "title": "[feature](fe) Support OIDC session credentials for
Iceberg REST catalog",
+ "url": "https://github.com/apache/doris/pull/63068"
+ }
+ ]
+ },
+ {
+ "title": "Cloud-native compute-storage separation: peer routing and
cache tiering",
+ "narrative": "Cross-compute-group peer read routing enables data
sharing across groups, while a new write-index-only cache config curbs IO
amplification under hot writes. Tenant-level colocation trims replica
redundancy in multi-tenant deployments, together pushing the
separation-of-storage-and-compute architecture toward higher elasticity and
finer resource control.",
+ "prs": [
+ {
+ "num": 63818,
+ "title": "[feat](cloud) cross compute group peer read routing with
peer cache I/O optimization",
+ "url": "https://github.com/apache/doris/pull/63818"
+ },
+ {
+ "num": 64995,
+ "title": "[feature](cloud) Support file cache write index only",
+ "url": "https://github.com/apache/doris/pull/64995"
+ },
+ {
+ "num": 64167,
+ "title": "[feat](colocate) support tenant-level colocation",
+ "url": "https://github.com/apache/doris/pull/64167"
+ }
+ ]
+ },
+ {
+ "title": "Scan and query execution: pruning, lazy read, and runtime
filters",
+ "narrative": "Lazy reads for pruned complex-type subcolumns and
expression-based ZoneMap pruning cut IO across internal and Parquet scans.
Global runtime filters now publish through an adaptive tree, removing duplicate
broadcasts of large filters in big clusters, while dynamic table snapshot reads
keep extending real-time warehouse capabilities.",
+ "prs": [
+ {
+ "num": 59263,
+ "title": "[feat](olap) Support lazy reading mode for pruned
complex columns",
+ "url": "https://github.com/apache/doris/pull/59263"
+ },
+ {
+ "num": 63389,
+ "title": "[feature](be) Support expression zonemap pruning",
+ "url": "https://github.com/apache/doris/pull/63389"
+ },
+ {
+ "num": 64851,
+ "title": "[feature](runtime-filter) Add adaptive global runtime
filter tree publish",
+ "url": "https://github.com/apache/doris/pull/64851"
+ },
+ {
+ "num": 64891,
+ "title": "[enhancement](Multi-stage lm) Multi-Stage Predicate Lazy
Materialization",
+ "url": "https://github.com/apache/doris/pull/64891"
+ },
+ {
+ "num": 64776,
+ "title": " [feat](dynamic table) support table stream part 4:
support snapshot read",
+ "url": "https://github.com/apache/doris/pull/64776"
+ }
+ ]
+ },
+ {
+ "title": "Inverted index evolution: Japanese tokenizer and SNII
format",
+ "narrative": "A new Kuromoji morphological analyzer closes Doris's gap
on space-less Japanese text, while the SNII inverted-index storage format lands
as an alternative path for better compression and extensibility. Together they
strengthen full-text search and observability for multi-language corpora and
AI-agent workloads.",
+ "prs": [
+ {
+ "num": 64667,
+ "title": "[feature](inverted-index) Add Japanese (Kuromoji)
morphological analyzer",
+ "url": "https://github.com/apache/doris/pull/64667"
+ },
+ {
+ "num": 64909,
+ "title": "[feature](be) Add SNII inverted index storage format",
+ "url": "https://github.com/apache/doris/pull/64909"
+ }
+ ]
+ },
+ {
+ "title": "Security and governance hardening across FE and TDE",
+ "narrative": "Intra-FE RPC now switches to HTTPS when TLS is enabled,
removing the plaintext fallback in hardened deployments. TDE gains Aliyun KMS
and Ranger KMS provider metadata, letting transparent encryption plug into
enterprise key management. The combined effect raises Doris's default security
posture for compliance-sensitive workloads.",
+ "prs": [
+ {
+ "num": 60921,
+ "title": "[Enhancement] (FE) Convert intra FE-to-FE calls to HTTPS
when enabled",
+ "url": "https://github.com/apache/doris/pull/60921"
+ },
+ {
+ "num": 64561,
+ "title": "[feat](tde) Add Aliyun and Ranger KMS provider metadata",
+ "url": "https://github.com/apache/doris/pull/64561"
+ }
+ ]
+ }
+ ],
+ "numbers": {
+ "mergedPrs": 113,
+ "newIssues": 5,
+ "contributors": 42
+ }
+ },
+ "repos": [
+ {
+ "repo": "apache/doris",
+ "scenarios": [
+ {
+ "name": "Multi-modal Lakehouse",
+ "mergedNarrative": "The multi-modal lakehouse scenario stabilized
Iceberg/Hive/external-write paths. Iceberg v3 row-lineage columns are rejected
to avoid hidden conflicts; COUNT(*) pushdown tolerates missing snapshot summary
counters; LZ4 compression now ships to Iceberg/Hive Parquet/ORC writers; binary
columns are emitted with correct Arrow types; nested decimal precision
promotion is supported; external writers clean up",
+ "merged": [],
+ "inProgressNarrative": "Multi-modal Lakehouse work strengthens
external read/write paths: Paimon write P0, Iceberg position-deletes system
table, ORC format v2 reader, Parquet dict bounds checks, Arrow Flight
coordinator lifecycle, OSS/HTTP FS SPI normalization, MaxCompute
partition-by-name parsing, Iceberg REST OAuth re-auth on 401, S3 load complex
types for Parquet/ORC, and Hive-ORC column-name default —",
+ "inProgress": []
+ },
+ {
+ "name": "Real-time Data Warehouse",
+ "mergedNarrative": "Real-time data warehouse work targeted P0
stability and correctness. Group commit recovers from missing block queues and
load_id reuse that dropped rows; SimplifyAggGroupBy now verifies injectivity;
local shuffle eliminates the bucket-shuffle serial bottleneck and upgrades
parallelism; local runtime filter merge deadlocks, pipeline-task
wake-after-finalize crashes, and variable-length column overflows across
sorted-run",
+ "merged": [],
+ "inProgressNarrative": "Real-time DWH advances the optimizer, load
pipeline, and ops visibility: Nereids adds expression defaults, named
BloomFilter, regexp rewrite, FD-redundant group-by elimination, stats-driven
distinct/eager-agg pushdown, anti/outer-join conversion, bucket shuffle for set
ops; loads gain routine-load target swap, stream-load progress observability,
UTF-8 validation toggle, Arrow case-sensitive columns, label-aware abort,
binlog hidden keys,",
+ "inProgress": []
+ },
+ {
+ "name": "Compute-Storage Separation & Cloud-Native",
+ "mergedNarrative": "Compute-storage separation and cloud-native
reliability advanced this week. Queries hitting E-230 now force a fresh version
cache; start.sh accepts multiple config files; version reads use snapshot
transactions to avoid write conflicts with update_table_version; S3FileWriter
now recovers from async submit failures; file-cache evict metrics are aligned
with the FileCacheType enum; Azure",
+ "merged": [],
+ "inProgressNarrative": "Compute-storage separation and cloud-native
work keeps hardening meta service, filesystem, and cache reliability and
observability: capability-gated MS_TOO_BUSY, warm-up job count bvar, recycler
S3 timeout fix, LRU restore speedup, file-cache skip-after-threshold,
post-alter tablet meta sync, label-aware transaction abort, Azure glob
pushdown, HDFS property migration, HTTP FS SPI scaffolding, segment id list,",
+ "inProgress": []
+ },
+ {
+ "name": "Agent Observability",
+ "mergedNarrative": "This scenario hardens observability across
Variant, inverted-index, and Delete paths. Variant compaction now distinguishes
empty keys from the root to prevent misrouting into sparse columns;
tokenization rejects invalid char_filter replacements; DELETE supports
generated-column partial updates; inverted-index predicates no longer crash on
null literals; and regression-side pinning plus bool-output normalization
reduce",
+ "merged": [],
+ "inProgressNarrative": "Agent Observability work is hardening legacy
index paths by blocking creation of inverted index V1 in FE, preventing the
small-file merge and packed-file path bypass that could amplify issues during
agent monitoring and export.",
+ "inProgress": []
+ },
+ {
+ "name": "Ecosystem Integration",
+ "mergedNarrative": "Ecosystem-integration work reconciled
client-side protocol differences. The regression suite now skips
Arrow-incompatible map cast cases with null keys and asserts bitmaps via
bitmap_to_string so Arrow Flight SQL and JDBC return semantically consistent
results without spurious failures.",
+ "merged": [],
+ "inProgressNarrative": "Ecosystem Integration is restoring the
dbt-doris adapter dev loop by fixing long-broken test dependencies so
contributors can run adapter tests from a clean checkout (new this month).",
+ "inProgress": []
+ },
+ {
+ "name": "Security & Governance",
+ "mergedNarrative": "Security governance closed loopholes across
credentials, gateway, and REST APIs. Kafka routine load properties are now
masked in SHOW/metadata; the _stream_load_forward endpoint is gated by a new BE
config and requires auth to block SSRF; brpc disables an SSL BIO buffer to
avoid mTLS write issues on large responses; and",
+ "merged": [],
+ "inProgressNarrative": "Security & Governance tightens TLS and
supply-chain hygiene: enabling cert verification by default in the Go SDK,
pinning Python deps with known CVEs across doris-compose/dbt-doris/cost_model,
and normalizing FE-internal HTTP calls to https when enable_https is on (all
new this month).",
+ "inProgress": []
+ },
+ {
+ "name": "Kernel & Engineering",
+ "mergedNarrative": "Kernel and engineering foundations received
broad hardening. Key fixes address bvar SIGSEGV under high EPS, NaN propagation
in PERCENTILE, SIGFPE on BIGINT_MIN/-1, sliced FixedSizeBinary reads, redundant
partition keys in PartitionTopN sort, AuditLoader concurrent assembly, and
AsyncIO workers without ThreadContext. Refactors moved array functions to
ColumnArrayView and split const vs. mutable",
+ "merged": [],
+ "inProgressNarrative": "Kernel & Engineering spans core correctness
and infra: the key change is adaptive random-bucket load routing for cloud
mode, alongside bitmap/Prometheus/TabletStatMgr/Broker-Load fixes, stable
`now()` folding, DECIMAL-returning round functions, rollback handling, Arrow
buffer validation, ORC SerDe decoding, UDF rollback, SetPreAggStatus stack
refactor, bucketed hash aggregate plan refactor, and many regression/Docker/CI
stabilizations",
+ "inProgress": [
+ {
+ "num": 62661,
+ "title": "[feature](load) introduce adaptive random bucket load
routing",
+ "url": "https://github.com/apache/doris/pull/62661"
+ }
+ ]
+ }
+ ],
+ "demand": []
+ }
+ ]
+};
diff --git a/src/pages/community-report/_reports/_TEMPLATE.mdx
b/src/pages/community-report/_reports/_TEMPLATE.mdx
new file mode 100644
index 00000000000..a0603fa8ca6
--- /dev/null
+++ b/src/pages/community-report/_reports/_TEMPLATE.mdx
@@ -0,0 +1,35 @@
+{/*
+ Standard weekly-report template.
+
+ To publish a new report:
+ 1. Copy this file and rename it to the report's start date: YYYY-MM-DD.mdx
+ (the filename sets the order — the newest date shows first).
+ 2. Edit the exports below and write the body in Markdown.
+
+ Files whose name starts with "_" (like this one) are ignored by the build.
+ Metadata is carried as `export const` (not YAML frontmatter) because these
+ files are compiled as MDX partials, which may not contain front matter.
+*/}
+
+export const label = 'Mon DD – Mon DD, YYYY'; // sidebar entry + page title
+export const week = 'Week NN, YYYY'; // optional subtitle — remove
if unused
+export const stats = [ // optional summary cards —
remove if unused
+ { num: '0', label: 'PRs merged' },
+ { num: '0', label: 'New contributors' },
+ { num: '0', label: 'Issues closed' },
+];
+
+One or two sentences introducing the week.
+
+## Highlights
+
+- First highlight.
+- Second highlight.
+
+## Development
+
+- Something that was built or fixed.
+
+## Notable Discussions
+
+- A discussion worth reading.
diff --git a/src/pages/community-report/index.tsx
b/src/pages/community-report/index.tsx
new file mode 100644
index 00000000000..614e6850745
--- /dev/null
+++ b/src/pages/community-report/index.tsx
@@ -0,0 +1,33 @@
+import React, { JSX } from 'react';
+import CommunityReportNext, { WeeklyReportEntry } from
'@site/src/components/community-report-next/CommunityReportNext';
+
+// Build-time glob of every report in ./_reports. The files live under
+// src/pages inside an underscore folder, so Docusaurus compiles each one as an
+// MDX partial: a React component (`default`) plus any `export const` metadata
+// (label / week / stats). The regex skips files whose name starts with "_"
+// (e.g. _TEMPLATE.mdx).
+function loadReports(): WeeklyReportEntry[] {
+ // require.context is a webpack build-time API and is not typed on
`require`.
+ const ctx = (require as any).context('./_reports', false,
/^\.\/[^_].*\.mdx?$/);
+ const entries = ctx.keys().map((key: string): WeeklyReportEntry => {
+ const mod = ctx(key);
+ const id = key.replace(/^\.\//, '').replace(/\.mdx?$/, '');
+ return {
+ id,
+ label: mod.label ?? id,
+ week: mod.week,
+ stats: Array.isArray(mod.stats) ? mod.stats : undefined,
+ report: mod.report,
+ Component: mod.default,
+ };
+ });
+ // Filenames follow YYYY-MM-DD, so a descending sort puts the latest first.
+ entries.sort((a, b) => (a.id < b.id ? 1 : -1));
+ return entries;
+}
+
+const REPORTS = loadReports();
+
+export default function CommunityReportPage(): JSX.Element {
+ return <CommunityReportNext reports={REPORTS} />;
+}
diff --git
a/versioned_docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
b/versioned_docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
new file mode 100644
index 00000000000..0c7f678d3d8
--- /dev/null
+++
b/versioned_docs/version-4.x/query-acceleration/materialized-view/intro-link.mdx
@@ -0,0 +1,13 @@
+---
+{
+ "title": "Materialized View",
+ "language": "en",
+ "description": "Apache Doris materialized view chapter navigation: covers
concepts, synchronous materialized views, asynchronous materialized views,
transparent rewriting, and FAQs."
+}
+---
+
+import GettingStartedCard from
'@site/src/components/getting-started-card/getting-started-card';
+
+A materialized view is an entity that contains both computation logic and
data. You can use it for query acceleration, lightweight ETL modeling, and
lakehouse federated query acceleration. Start by understanding the concepts,
then choose between synchronous and asynchronous materialized views based on
your freshness requirements.
+
+Please refer to the [materialized view](./intro) related documentation.
diff --git a/versioned_sidebars/version-4.x-sidebars.json
b/versioned_sidebars/version-4.x-sidebars.json
index 99b2ca4935b..fc882dfbab8 100644
--- a/versioned_sidebars/version-4.x-sidebars.json
+++ b/versioned_sidebars/version-4.x-sidebars.json
@@ -580,6 +580,33 @@
"query-data/lateral-view"
]
},
+ {
+ "type": "category",
+ "label": "Materialized View",
+ "link": {
+ "type": "doc",
+ "id": "query-acceleration/materialized-view/intro"
+ },
+ "items": [
+ "query-acceleration/materialized-view/overview",
+ "query-acceleration/materialized-view/sync-materialized-view",
+
"query-acceleration/tuning/tuning-plan/transparent-rewriting-with-sync-mv",
+ {
+ "type": "category",
+ "label": "Async Materialized View",
+ "link": {
+ "type": "doc",
+ "id":
"query-acceleration/materialized-view/async-materialized-view/overview"
+ },
+ "items": [
+
"query-acceleration/materialized-view/async-materialized-view/functions-and-demands",
+
"query-acceleration/materialized-view/async-materialized-view/use-guide",
+
"query-acceleration/tuning/tuning-plan/transparent-rewriting-with-async-mv",
+
"query-acceleration/materialized-view/async-materialized-view/faq"
+ ]
+ }
+ ]
+ },
{
"type": "category",
"label": "Performance & Tuning",
@@ -619,33 +646,7 @@
"query-acceleration/tuning/tuning-plan/optimizing-table-scanning"
]
},
- {
- "type": "category",
- "label": "Materialized View",
- "link": {
- "type": "doc",
- "id": "query-acceleration/materialized-view/intro"
- },
- "items": [
- "query-acceleration/materialized-view/overview",
-
"query-acceleration/materialized-view/sync-materialized-view",
-
"query-acceleration/tuning/tuning-plan/transparent-rewriting-with-sync-mv",
- {
- "type": "category",
- "label": "Async Materialized View",
- "link": {
- "type": "doc",
- "id":
"query-acceleration/materialized-view/async-materialized-view/overview"
- },
- "items": [
-
"query-acceleration/materialized-view/async-materialized-view/functions-and-demands",
-
"query-acceleration/materialized-view/async-materialized-view/use-guide",
-
"query-acceleration/tuning/tuning-plan/transparent-rewriting-with-async-mv",
-
"query-acceleration/materialized-view/async-materialized-view/faq"
- ]
- }
- ]
- },
+ "query-acceleration/materialized-view/intro-link",
{
"type": "category",
"label": "Join Optimization",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]