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>