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

400Ping pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git


The following commit(s) were added to refs/heads/main by this push:
     new e53b879a8 [Website] Split blog and meeting minutes listings (#1423)
e53b879a8 is described below

commit e53b879a87e6ce42a7e3daf16856a128b74b517e
Author: Jie-Kai Chang <[email protected]>
AuthorDate: Mon Jun 29 03:40:16 2026 +0800

    [Website] Split blog and meeting minutes listings (#1423)
    
    Signed-off-by: 400Ping <[email protected]>
---
 docs/blog/2026-02-02-Qumat-0.5.0-Release.md       |   2 +-
 docs/blog/2026-06-01-Qumat-0.6.0-Release/index.md |   2 +-
 website/src/global.d.ts                           |  12 ++
 website/src/pages/blog/minutes.tsx                | 192 ++++++++++++++++++++++
 website/src/theme/BlogListPage/index.tsx          | 187 +++++++++++++++++----
 website/src/theme/BlogListPage/styles.module.css  |  74 +++++++++
 website/src/theme/BlogPostItem/index.tsx          |  12 +-
 7 files changed, 446 insertions(+), 35 deletions(-)

diff --git a/docs/blog/2026-02-02-Qumat-0.5.0-Release.md 
b/docs/blog/2026-02-02-Qumat-0.5.0-Release.md
index 722217de1..bb511013c 100644
--- a/docs/blog/2026-02-02-Qumat-0.5.0-Release.md
+++ b/docs/blog/2026-02-02-Qumat-0.5.0-Release.md
@@ -9,7 +9,7 @@ We are excited to announce **QuMat v0.5.0**, the next release 
of Mahout's quantu
 
 The main theme of this release is moving QuMat from a circuit abstraction into 
a more complete quantum ML development platform. The release introduces the 
first public proof-of-concept of **QDP (Quantum Data Plane)**, adds 
GPU-accelerated data-to-state encoding, improves QuMat's backend behavior, and 
tightens the development workflow around testing, packaging, and documentation.
 
-Special thanks to everyone who contributed to this release. We would like to 
thank PMC members Jie-Kai Chang, Guan-Ming Chiu, Andrew Musselman, Shannon 
Quinn (PMC Chair), and Trevor Grant. We also thank Committers Hsien-Cheng 
Huang, Kuan-Hao Huang and Krishna Dave, along with contributors Nai-Jui Yeh 
(Union.ai), Che-Yu Wu (Synology), Vic Wen, ChenChen Lai, and the broader Apache 
Mahout community.
+Special thanks to everyone who contributed to this release. We would like to 
thank PMC members Jie-Kai Chang, Guan-Ming Chiu, Andrew Musselman, Shannon 
Quinn (PMC Chair), and Trevor Grant. We also thank Committers Hsien-Cheng 
Huang, Kuan-Hao Huang and Krishna Dave, along with contributors Nai-Jui Yeh 
(Union.ai), Che-Yu Wu (Synology), Jia-Wei Jiang (TSMC), Guan-Hua Wen, Yu-Chen 
Lai, and the broader Apache Mahout community.
 
 <!-- truncate -->
 
diff --git a/docs/blog/2026-06-01-Qumat-0.6.0-Release/index.md 
b/docs/blog/2026-06-01-Qumat-0.6.0-Release/index.md
index db4a72a68..dbd0671b2 100644
--- a/docs/blog/2026-06-01-Qumat-0.6.0-Release/index.md
+++ b/docs/blog/2026-06-01-Qumat-0.6.0-Release/index.md
@@ -9,7 +9,7 @@ We are excited to announce **QuMat v0.6.0**, the next release 
of Mahout's quantu
 
 The main theme of this release is moving QDP from an NVIDIA-focused proof of 
concept into a broader GPU data plane for quantum ML. QuMat v0.6.0 brings AMD 
ROCm support to first-class status, closes encoder parity gaps between CUDA and 
ROCm, adds realistic benchmark workloads, and refreshes the documentation site 
for users and contributors.
 
-Special thanks to everyone who contributed to this release. We would like to 
thank PMC members Jie-Kai Chang, Guan-Ming Chiu, Andrew Musselman, Shannon 
Quinn (PMC Chair) and Trevor Grant. We also thank Committers Hsien-Cheng Huang, 
and Kuan-Hao Huang, along with contributors Tim Hsiung (Microsoft), Vic Wen 
(Microsoft), Eddie Tsai (Phind), ChenChen Lai and the broader Apache Mahout 
community.
+Special thanks to everyone who contributed to this release. We would like to 
thank PMC members Jie-Kai Chang, Guan-Ming Chiu, Andrew Musselman, Shannon 
Quinn (PMC Chair) and Trevor Grant. We also thank Committers Hsien-Cheng Huang, 
and Kuan-Hao Huang, along with contributors Yu-Ting Hsiung (Microsoft), 
Guan-Hua Wen (Microsoft), An-Te Tsai (Phind), Chao-Hong Yeh (Trend Micro), 
Han-Wen Tsao, Yu-Chen Lai and the broader Apache Mahout community.
 
 <!-- truncate -->
 
diff --git a/website/src/global.d.ts b/website/src/global.d.ts
index 5ee351c71..f7b18da9e 100644
--- a/website/src/global.d.ts
+++ b/website/src/global.d.ts
@@ -6,3 +6,15 @@ declare module '*.module.css' {
   const classes: { readonly [key: string]: string };
   export default classes;
 }
+
+declare module '~blog/default/blog-post-list-prop-default.json' {
+  const value: {
+    readonly items: readonly {
+      readonly date: string;
+      readonly permalink: string;
+      readonly title: string;
+      readonly unlisted?: boolean;
+    }[];
+  };
+  export default value;
+}
diff --git a/website/src/pages/blog/minutes.tsx 
b/website/src/pages/blog/minutes.tsx
new file mode 100644
index 000000000..51972dcfa
--- /dev/null
+++ b/website/src/pages/blog/minutes.tsx
@@ -0,0 +1,192 @@
+import React, {type ReactNode} from 'react';
+import clsx from 'clsx';
+
+import Link from '@docusaurus/Link';
+import {useLocation} from '@docusaurus/router';
+import {
+  HtmlClassNameProvider,
+  PageMetadata,
+  ThemeClassNames,
+} from '@docusaurus/theme-common';
+import Layout from '@theme/Layout';
+import SearchMetadata from '@theme/SearchMetadata';
+import blogPostList from '~blog/default/blog-post-list-prop-default.json';
+
+import styles from '../../theme/BlogListPage/styles.module.css';
+
+type BlogListEntry = (typeof blogPostList.items)[number];
+
+const MINUTES_PER_PAGE = 12;
+
+function getCurrentPage(search: string) {
+  const rawPage = Number(new URLSearchParams(search).get('page'));
+
+  return Number.isInteger(rawPage) && rawPage > 0 ? rawPage : 1;
+}
+
+function getPageLink(page: number) {
+  return page > 1 ? `/blog/minutes?page=${page}` : '/blog/minutes';
+}
+
+function paginateItems(items: BlogListEntry[], page: number) {
+  const totalPages = Math.max(1, Math.ceil(items.length / MINUTES_PER_PAGE));
+  const currentPage = Math.min(page, totalPages);
+  const start = (currentPage - 1) * MINUTES_PER_PAGE;
+
+  return {
+    currentPage,
+    pageItems: items.slice(start, start + MINUTES_PER_PAGE),
+    totalPages,
+  };
+}
+
+function formatDate(date: string) {
+  return new Intl.DateTimeFormat('en', {
+    day: 'numeric',
+    month: 'long',
+    timeZone: 'UTC',
+    year: 'numeric',
+  }).format(new Date(date));
+}
+
+function isMeetingMinutes(item: BlogListEntry) {
+  return item.title.toLowerCase().includes('meeting minutes');
+}
+
+function NewsHeader() {
+  return (
+    <header className={styles.header}>
+      <div>
+        <div className={styles.kicker}>Apache Mahout</div>
+        <h1 className={styles.title}>News</h1>
+      </div>
+      <p className={styles.description}>
+        Apache Mahout project news, releases, and community updates
+      </p>
+    </header>
+  );
+}
+
+function NewsNav() {
+  return (
+    <nav className={styles.sectionNav} aria-label="News sections">
+      <Link to="/blog">Blog</Link>
+      <Link className={styles.activeNavLink} to="/blog/minutes">
+        Meeting Minutes
+      </Link>
+    </nav>
+  );
+}
+
+function MinutesItem({item}: {item: BlogListEntry}) {
+  return (
+    <article className={styles.minutesItem}>
+      <div className={styles.eyebrow}>
+        <time dateTime={item.date}>{formatDate(item.date)}</time>
+      </div>
+      <h3 className={styles.minutesTitle}>
+        <Link to={item.permalink}>{item.title}</Link>
+      </h3>
+    </article>
+  );
+}
+
+function ListPagination({
+  currentPage,
+  totalPages,
+}: {
+  currentPage: number;
+  totalPages: number;
+}) {
+  if (totalPages <= 1) {
+    return null;
+  }
+
+  const pages = Array.from({length: totalPages}, (_, index) => index + 1);
+
+  return (
+    <nav className={styles.pagination} aria-label="Meeting minutes pagination">
+      <Link
+        className={clsx(
+          styles.pageButton,
+          currentPage === 1 && styles.disabledPageButton,
+        )}
+        aria-disabled={currentPage === 1}
+        tabIndex={currentPage === 1 ? -1 : undefined}
+        to={getPageLink(Math.max(1, currentPage - 1))}>
+        Previous
+      </Link>
+      <div className={styles.pageNumbers}>
+        {pages.map((page) => (
+          <Link
+            key={page}
+            className={clsx(
+              styles.pageNumber,
+              page === currentPage && styles.activePageNumber,
+            )}
+            aria-current={page === currentPage ? 'page' : undefined}
+            to={getPageLink(page)}>
+            {page}
+          </Link>
+        ))}
+      </div>
+      <Link
+        className={clsx(
+          styles.pageButton,
+          currentPage === totalPages && styles.disabledPageButton,
+        )}
+        aria-disabled={currentPage === totalPages}
+        tabIndex={currentPage === totalPages ? -1 : undefined}
+        to={getPageLink(Math.min(totalPages, currentPage + 1))}>
+        Next
+      </Link>
+    </nav>
+  );
+}
+
+function MeetingMinutesPageContent() {
+  const location = useLocation();
+  const minutes = blogPostList.items.filter(isMeetingMinutes);
+  const {currentPage, pageItems, totalPages} = paginateItems(
+    minutes,
+    getCurrentPage(location.search),
+  );
+
+  return (
+    <Layout>
+      <main className="container margin-vert--lg">
+        <NewsHeader />
+        <NewsNav />
+        <section className={styles.section}>
+          <div className={styles.sectionHeader}>
+            <span>Meeting Minutes</span>
+            <h2>Community meeting notes</h2>
+          </div>
+          <div className={styles.minutesList}>
+            {pageItems.map((item) => (
+              <MinutesItem key={item.permalink} item={item} />
+            ))}
+          </div>
+        </section>
+        <ListPagination currentPage={currentPage} totalPages={totalPages} />
+      </main>
+    </Layout>
+  );
+}
+
+export default function MeetingMinutesPage(): ReactNode {
+  return (
+    <HtmlClassNameProvider
+      className={clsx(
+        ThemeClassNames.wrapper.blogPages,
+        ThemeClassNames.page.blogListPage,
+      )}>
+      <PageMetadata
+        title="Meeting Minutes"
+        description="Apache Mahout community meeting notes"
+      />
+      <SearchMetadata tag="blog_posts_list" />
+      <MeetingMinutesPageContent />
+    </HtmlClassNameProvider>
+  );
+}
diff --git a/website/src/theme/BlogListPage/index.tsx 
b/website/src/theme/BlogListPage/index.tsx
index 61f47f36d..03305d35c 100644
--- a/website/src/theme/BlogListPage/index.tsx
+++ b/website/src/theme/BlogListPage/index.tsx
@@ -3,6 +3,7 @@ import clsx from 'clsx';
 
 import Link from '@docusaurus/Link';
 import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import {useLocation} from '@docusaurus/router';
 import {
   PageMetadata,
   HtmlClassNameProvider,
@@ -10,7 +11,6 @@ import {
 } from '@docusaurus/theme-common';
 import {useDateTimeFormat} from '@docusaurus/theme-common/internal';
 import Layout from '@theme/Layout';
-import BlogListPaginator from '@theme/BlogListPaginator';
 import SearchMetadata from '@theme/SearchMetadata';
 import type {Props} from '@theme/BlogListPage';
 import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData';
@@ -18,6 +18,9 @@ import BlogListPageStructuredData from 
'@theme/BlogListPage/StructuredData';
 import styles from './styles.module.css';
 
 type BlogListItem = Props['items'][number];
+type NewsView = 'blog' | 'minutes';
+
+const BLOG_POSTS_PER_PAGE = 9;
 
 function useFormattedDate() {
   const dateTimeFormat = useDateTimeFormat({
@@ -77,6 +80,49 @@ function isMeetingMinutes(item: BlogListItem) {
   );
 }
 
+function getView(search: string): NewsView {
+  const params = new URLSearchParams(search);
+
+  return params.get('view') === 'minutes' ? 'minutes' : 'blog';
+}
+
+function getCurrentPage(search: string) {
+  const rawPage = Number(new URLSearchParams(search).get('page'));
+
+  return Number.isInteger(rawPage) && rawPage > 0 ? rawPage : 1;
+}
+
+function getPageLink(view: NewsView, page: number) {
+  const params = new URLSearchParams();
+
+  if (view === 'minutes') {
+    if (page > 1) {
+      params.set('page', String(page));
+    }
+
+    const query = params.toString();
+    return query ? `/blog/minutes?${query}` : '/blog/minutes';
+  }
+  if (page > 1) {
+    params.set('page', String(page));
+  }
+
+  const query = params.toString();
+  return query ? `/blog?${query}` : '/blog';
+}
+
+function paginateItems(items: BlogListItem[], page: number, pageSize: number) {
+  const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
+  const currentPage = Math.min(page, totalPages);
+  const start = (currentPage - 1) * pageSize;
+
+  return {
+    currentPage,
+    pageItems: items.slice(start, start + pageSize),
+    totalPages,
+  };
+}
+
 function PostCard({item}: {item: BlogListItem}) {
   const {metadata} = item.content;
 
@@ -173,52 +219,133 @@ function BlogListHeader({metadata}: Pick<Props, 
'metadata'>) {
   );
 }
 
+function NewsNav({view}: {view: NewsView}) {
+  return (
+    <nav className={styles.sectionNav} aria-label="News sections">
+      <Link
+        className={clsx(view === 'blog' && styles.activeNavLink)}
+        to="/blog">
+        Blog
+      </Link>
+      <Link
+        className={clsx(view === 'minutes' && styles.activeNavLink)}
+        to="/blog/minutes">
+        Meeting Minutes
+      </Link>
+    </nav>
+  );
+}
+
+function ListPagination({
+  currentPage,
+  totalPages,
+  view,
+}: {
+  currentPage: number;
+  totalPages: number;
+  view: NewsView;
+}) {
+  if (totalPages <= 1) {
+    return null;
+  }
+
+  const pages = Array.from({length: totalPages}, (_, index) => index + 1);
+
+  return (
+    <nav className={styles.pagination} aria-label="News pagination">
+      <Link
+        className={clsx(
+          styles.pageButton,
+          currentPage === 1 && styles.disabledPageButton,
+        )}
+        aria-disabled={currentPage === 1}
+        tabIndex={currentPage === 1 ? -1 : undefined}
+        to={getPageLink(view, Math.max(1, currentPage - 1))}>
+        Previous
+      </Link>
+      <div className={styles.pageNumbers}>
+        {pages.map((page) => (
+          <Link
+            key={page}
+            className={clsx(
+              styles.pageNumber,
+              page === currentPage && styles.activePageNumber,
+            )}
+            aria-current={page === currentPage ? 'page' : undefined}
+            to={getPageLink(view, page)}>
+            {page}
+          </Link>
+        ))}
+      </div>
+      <Link
+        className={clsx(
+          styles.pageButton,
+          currentPage === totalPages && styles.disabledPageButton,
+        )}
+        aria-disabled={currentPage === totalPages}
+        tabIndex={currentPage === totalPages ? -1 : undefined}
+        to={getPageLink(view, Math.min(totalPages, currentPage + 1))}>
+        Next
+      </Link>
+    </nav>
+  );
+}
+
 function BlogListPageContent(props: Props): ReactNode {
   const {metadata, items} = props;
+  const location = useLocation();
+  const view = getView(location.search);
+  const requestedPage = getCurrentPage(location.search);
   const posts = items.filter((item) => !isMeetingMinutes(item));
-  const minutes = items.filter(isMeetingMinutes);
-  const [featured, ...blogPosts] = posts;
+  const activeItems = view === 'minutes' ? items.filter(isMeetingMinutes) : 
posts;
+  const pageSize = BLOG_POSTS_PER_PAGE;
+  const {currentPage, pageItems, totalPages} = paginateItems(
+    activeItems,
+    requestedPage,
+    pageSize,
+  );
+  const [featured, ...blogPosts] = currentPage === 1 ? pageItems : [];
+  const regularBlogPosts = currentPage === 1 ? blogPosts : pageItems;
 
   return (
     <Layout>
       <main className="container margin-vert--lg">
         <BlogListHeader metadata={metadata} />
-        <nav className={styles.sectionNav} aria-label="News sections">
-          <a href="#blog">Blog</a>
-          <a href="#meeting-minutes">Meeting Minutes</a>
-        </nav>
-        <section id="blog" className={styles.section}>
-          <div className={styles.sectionHeader}>
-            <span>Blog</span>
-            <h2>Project updates and release notes</h2>
-          </div>
-        {featured && <FeaturedPost item={featured} />}
-        <section className={styles.grid} aria-label="News posts">
-          {blogPosts.map((item) => (
-            <PostCard
-              key={item.content.metadata.permalink}
-              item={item}
-            />
-          ))}
-        </section>
-        </section>
-        {minutes.length > 0 && (
-          <section id="meeting-minutes" className={styles.minutesSection}>
+        <NewsNav view={view} />
+        {view === 'blog' ? (
+          <section className={styles.section}>
             <div className={styles.sectionHeader}>
-              <span>Meeting Minutes</span>
-              <h2>Community meeting notes</h2>
+              <span>Blog</span>
+              <h2>Project updates and release notes</h2>
             </div>
-            <div className={styles.minutesList}>
-              {minutes.map((item) => (
-                <MinutesItem
+            {featured && <FeaturedPost item={featured} />}
+            <section className={styles.grid} aria-label="News posts">
+              {regularBlogPosts.map((item) => (
+                <PostCard
                   key={item.content.metadata.permalink}
                   item={item}
                 />
               ))}
+            </section>
+          </section>
+        ) : (
+          <section className={styles.section}>
+            <div className={styles.sectionHeader}>
+              <span>Meeting Minutes</span>
+              <h2>Community meeting notes</h2>
+            </div>
+            <div className={styles.minutesList}>
+              {pageItems.map((item) => (
+                <MinutesItem key={item.content.metadata.permalink} item={item} 
/>
+              ))}
             </div>
           </section>
         )}
-        <BlogListPaginator metadata={metadata} />
+        <ListPagination
+          currentPage={currentPage}
+          totalPages={totalPages}
+          view={view}
+        />
       </main>
     </Layout>
   );
diff --git a/website/src/theme/BlogListPage/styles.module.css 
b/website/src/theme/BlogListPage/styles.module.css
index 4a9e20f73..569edafd3 100644
--- a/website/src/theme/BlogListPage/styles.module.css
+++ b/website/src/theme/BlogListPage/styles.module.css
@@ -41,9 +41,11 @@
 }
 
 .sectionNav a {
+  border-bottom: 3px solid transparent;
   color: var(--ifm-font-color-base);
   font-size: 0.9rem;
   font-weight: 800;
+  padding-bottom: 0.45rem;
 }
 
 .sectionNav a:hover {
@@ -51,6 +53,11 @@
   text-decoration: none;
 }
 
+.activeNavLink {
+  border-color: var(--ifm-color-primary-darkest) !important;
+  color: var(--ifm-color-primary-darkest) !important;
+}
+
 .section {
   scroll-margin-top: calc(var(--ifm-navbar-height) + 1.5rem);
 }
@@ -275,6 +282,64 @@
   margin: -0.55rem 0 0;
 }
 
+.pagination {
+  align-items: center;
+  border-top: 1px solid var(--ifm-color-emphasis-200);
+  display: flex;
+  gap: 1rem;
+  justify-content: space-between;
+  margin-top: 2rem;
+  padding-top: 1.4rem;
+}
+
+.pageNumbers {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.45rem;
+  justify-content: center;
+}
+
+.pageButton,
+.pageNumber {
+  align-items: center;
+  border: 1px solid var(--ifm-color-emphasis-300);
+  border-radius: 8px;
+  color: var(--ifm-font-color-base);
+  display: inline-flex;
+  font-size: 0.9rem;
+  font-weight: 800;
+  justify-content: center;
+  line-height: 1;
+  min-height: 2.45rem;
+  min-width: 2.45rem;
+  padding: 0.7rem 0.85rem;
+}
+
+.pageButton:hover,
+.pageNumber:hover {
+  border-color: var(--ifm-color-primary-darkest);
+  color: var(--ifm-color-primary-darkest);
+  text-decoration: none;
+}
+
+.activePageNumber {
+  background: var(--ifm-color-primary-darkest);
+  border-color: var(--ifm-color-primary-darkest);
+  color: var(--ifm-color-white);
+}
+
+.activePageNumber:hover {
+  color: var(--ifm-color-white);
+}
+
+.disabledPageButton,
+.disabledPageButton:hover {
+  border-color: var(--ifm-color-emphasis-200);
+  color: var(--ifm-color-emphasis-500);
+  cursor: not-allowed;
+  pointer-events: none;
+}
+
 @media (max-width: 996px) {
   .header,
   .featured {
@@ -324,4 +389,13 @@
     grid-column: auto;
     margin-top: 0;
   }
+
+  .pagination {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .pageButton {
+    width: 100%;
+  }
 }
diff --git a/website/src/theme/BlogPostItem/index.tsx 
b/website/src/theme/BlogPostItem/index.tsx
index 7fce9b774..29aad9cf6 100644
--- a/website/src/theme/BlogPostItem/index.tsx
+++ b/website/src/theme/BlogPostItem/index.tsx
@@ -87,14 +87,20 @@ function ListPostItem({children, className}: Props): 
ReactNode {
 
 function ArticlePostItem({children, className}: Props): ReactNode {
   const {
-    metadata: {title},
+    metadata: {tags, title},
   } = useBlogPost();
+  const isMeetingMinutes =
+    title.toLowerCase().includes('meeting minutes') ||
+    tags.some((tag) => tag.label.toLowerCase() === 'minutes');
+  const breadcrumb = isMeetingMinutes
+    ? {label: 'Meeting Minutes', to: '/blog/minutes'}
+    : {label: 'Blog', to: '/blog'};
 
   return (
     <article className={clsx(styles.article, className)}>
       <header className={styles.articleHeader}>
-        <Link className={styles.breadcrumb} to="/blog">
-          News
+        <Link className={styles.breadcrumb} to={breadcrumb.to}>
+          {breadcrumb.label}
         </Link>
         <PostMeta />
         <h1 className={styles.articleTitle}>{title}</h1>

Reply via email to