From 21eb58b2e3f1c21c30a557314c5e0d7809270f36 Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Thu, 12 Mar 2026 14:54:51 +0800 Subject: [PATCH 1/7] feat(config): add URL_STATIC_NEWS_CATEGORY_POSTS --- packages/mirror-media-next/config/index.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mirror-media-next/config/index.mjs b/packages/mirror-media-next/config/index.mjs index 464777ec..bb0a481b 100644 --- a/packages/mirror-media-next/config/index.mjs +++ b/packages/mirror-media-next/config/index.mjs @@ -59,7 +59,7 @@ let STORY_GQL_ENDPOINT = '' let ENABLE_NON_PREMIUM_OPEN_ARTICLE_MODE = true let GCS_FUSE_MOUNT_DIR = '' let GCS_FUSE_STATIC_BUCKET = '' - +let URL_STATIC_NEWS_CATEGORY_POSTS = '' /** @type {import("firebase/auth").ActionCodeSettings} */ let ACTION_CODE_SETTING @@ -97,6 +97,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://${STATIC_FILE_DOMAIN}/json/atest/latest_content_section_column_1` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` // Only use STORY_GQL_ENDPOINT if explicitly set via env var // Do not fallback to dev endpoint in prod to avoid connecting to dev service STORY_GQL_ENDPOINT = @@ -165,6 +166,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://${STATIC_FILE_DOMAIN}/json/atest/latest_content_section_column_1` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || 'https://go-story-staging-983956931553.asia-east1.run.app/api/graphql' @@ -230,6 +232,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://storage.googleapis.com/v3-statics-dev.mirrormedia.mg/json/latest/latest_content_section_column_1.json` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || 'https://go-story-dev-983956931553.asia-east1.run.app/api/graphql' @@ -301,6 +304,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://storage.googleapis.com/v3-statics-dev.mirrormedia.mg/json/latest/latest_content_section_column_1.json` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || 'https://go-story-dev-983956931553.asia-east1.run.app/api/graphql' @@ -385,4 +389,5 @@ export { ENABLE_NON_PREMIUM_OPEN_ARTICLE_MODE, GCS_FUSE_MOUNT_DIR, GCS_FUSE_STATIC_BUCKET, + URL_STATIC_NEWS_CATEGORY_POSTS, } From 18acadd5b7ad68137b8975d42c6d35a632f1e7bf Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Thu, 12 Mar 2026 17:33:27 +0800 Subject: [PATCH 2/7] feat(api): add fetchNewsCategoryPostsJSON --- .../mirror-media-next/utils/api/category.js | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/mirror-media-next/utils/api/category.js b/packages/mirror-media-next/utils/api/category.js index 03b96ae2..258720a5 100644 --- a/packages/mirror-media-next/utils/api/category.js +++ b/packages/mirror-media-next/utils/api/category.js @@ -1,6 +1,11 @@ import client from '../../apollo/apollo-client' import { fetchCategorySections } from '../../apollo/query/categroies' import { fetchPosts } from '../../apollo/query/posts' +import axios from 'axios' +import { + API_TIMEOUT, + URL_STATIC_NEWS_CATEGORY_POSTS, +} from '../../config/index.mjs' export function fetchPostsByCategorySlug(categorySlug, take, skip) { return client.query({ @@ -17,6 +22,42 @@ export function fetchPostsByCategorySlug(categorySlug, take, skip) { }) } +export async function fetchNewsCategoryPostsJSON(page = 1, take = 24) { + const POSTS_PER_JSON = 120 + const TAKE_PER_JSON = POSTS_PER_JSON / take + const jsonFileOrder = Math.ceil(page / TAKE_PER_JSON) + const jsonUrl = `${URL_STATIC_NEWS_CATEGORY_POSTS}_${jsonFileOrder}.json` + + try { + const response = await axios({ + method: 'get', + url: jsonUrl, + timeout: API_TIMEOUT, + }) + + const jsonIndex = (page - 1) % TAKE_PER_JSON + const startIndex = jsonIndex * take + const endIndex = startIndex + take + const postItems = response.data.posts.items.slice(startIndex, endIndex) + + return { + data: { + posts: { + items: jsonFileOrder <= 4 ? postItems : [], + counts: + response.data.posts.counts.posts + + response.data.posts.counts.externals, + }, + }, + } + } catch (err) { + console.error( + 'Failed to fetch JSON of URL_STATIC_NEWS_CATEGORY_POSTS', + JSON.stringify(err) + ) + } +} + export function fetchPremiumPostsByCategorySlug(categorySlug, take, skip) { return client.query({ query: fetchPosts, From bb52898f84fb45b1bfb6f3f704c069fc7467a0d4 Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Thu, 12 Mar 2026 17:39:15 +0800 Subject: [PATCH 3/7] feat(category-articles): call fetchNewsCategoryPostsJSON when isNewsCategory is true --- .../components/category/category-articles.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/mirror-media-next/components/category/category-articles.js b/packages/mirror-media-next/components/category/category-articles.js index 3f1b09e9..daebfe89 100644 --- a/packages/mirror-media-next/components/category/category-articles.js +++ b/packages/mirror-media-next/components/category/category-articles.js @@ -7,6 +7,7 @@ import PremiumArticleList from '../shared/premium-article-list' import { fetchPostsByCategorySlug, fetchPremiumPostsByCategorySlug, + fetchNewsCategoryPostsJSON, } from '../../utils/api/category' import LoadingPage from '../../public/images-next/loading_page.gif' @@ -35,6 +36,7 @@ const Loading = styled.div` * @param {Category} props.category * @param {Number} props.renderPageSize * @param {boolean} props.isPremium + * @param {boolean} props.isNewsCategory * @returns {React.ReactElement} */ export default function CategoryArticles({ @@ -43,6 +45,7 @@ export default function CategoryArticles({ category, renderPageSize, isPremium, + isNewsCategory, }) { const fetchPageSize = renderPageSize * 2 @@ -55,7 +58,13 @@ export default function CategoryArticles({ const skip = (page - 1) * take const response = isPremium ? await fetchPremiumPostsByCategorySlug(category.slug, take, skip) + : isNewsCategory + ? await fetchNewsCategoryPostsJSON(page, take) : await fetchPostsByCategorySlug(category.slug, take, skip) + + if (isNewsCategory) { + return response.data.posts.items + } return response.data.posts } catch (error) { // [to-do]: use beacon api to log error on gcs From 945111f5c3e03d241d9e5fd174d986ec88231d4d Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Thu, 12 Mar 2026 17:49:13 +0800 Subject: [PATCH 4/7] feat(config): add URL_STATIC_NEWS_CATEGORY_INFO --- packages/mirror-media-next/config/index.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/mirror-media-next/config/index.mjs b/packages/mirror-media-next/config/index.mjs index bb0a481b..95f1acd8 100644 --- a/packages/mirror-media-next/config/index.mjs +++ b/packages/mirror-media-next/config/index.mjs @@ -59,6 +59,7 @@ let STORY_GQL_ENDPOINT = '' let ENABLE_NON_PREMIUM_OPEN_ARTICLE_MODE = true let GCS_FUSE_MOUNT_DIR = '' let GCS_FUSE_STATIC_BUCKET = '' +let URL_STATIC_NEWS_CATEGORY_INFO = '' let URL_STATIC_NEWS_CATEGORY_POSTS = '' /** @type {import("firebase/auth").ActionCodeSettings} */ let ACTION_CODE_SETTING @@ -97,6 +98,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://${STATIC_FILE_DOMAIN}/json/atest/latest_content_section_column_1` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_INFO = `https://${STATIC_FILE_DOMAIN}/json/latest/category_news.json` URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` // Only use STORY_GQL_ENDPOINT if explicitly set via env var // Do not fallback to dev endpoint in prod to avoid connecting to dev service @@ -166,6 +168,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://${STATIC_FILE_DOMAIN}/json/atest/latest_content_section_column_1` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_INFO = `https://${STATIC_FILE_DOMAIN}/json/latest/category_news.json` URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || @@ -232,6 +235,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://storage.googleapis.com/v3-statics-dev.mirrormedia.mg/json/latest/latest_content_section_column_1.json` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_INFO = `https://${STATIC_FILE_DOMAIN}/json/latest/category_news.json` URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || @@ -304,6 +308,7 @@ switch (ENV) { URL_STATIC_PROMOTE_VIDEOS = `https://${STATIC_FILE_DOMAIN}/files/json/promoting-video.json` URL_STATIC_COLUMN_SECTION_POSTS = `https://storage.googleapis.com/v3-statics-dev.mirrormedia.mg/json/latest/latest_content_section_column_1.json` URL_STATIC_DAILY_COLUMN_HEADLINES = `https://${STATIC_FILE_DOMAIN}/files/json/daily-column.json` + URL_STATIC_NEWS_CATEGORY_INFO = `https://${STATIC_FILE_DOMAIN}/json/latest/category_news.json` URL_STATIC_NEWS_CATEGORY_POSTS = `https://${STATIC_FILE_DOMAIN}/json/latest/latest_content_category_news_public` STORY_GQL_ENDPOINT = process.env.STORY_GQL_ENDPOINT || @@ -389,5 +394,6 @@ export { ENABLE_NON_PREMIUM_OPEN_ARTICLE_MODE, GCS_FUSE_MOUNT_DIR, GCS_FUSE_STATIC_BUCKET, + URL_STATIC_NEWS_CATEGORY_INFO, URL_STATIC_NEWS_CATEGORY_POSTS, } From 0268a74d8478dfe4073cfe62567f99196c1d90b0 Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Fri, 13 Mar 2026 14:05:54 +0800 Subject: [PATCH 5/7] feat(api): add func fetchNewsCategoryInfo --- packages/mirror-media-next/utils/api/category.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/mirror-media-next/utils/api/category.js b/packages/mirror-media-next/utils/api/category.js index 258720a5..f43e2592 100644 --- a/packages/mirror-media-next/utils/api/category.js +++ b/packages/mirror-media-next/utils/api/category.js @@ -4,6 +4,7 @@ import { fetchPosts } from '../../apollo/query/posts' import axios from 'axios' import { API_TIMEOUT, + URL_STATIC_NEWS_CATEGORY_INFO, URL_STATIC_NEWS_CATEGORY_POSTS, } from '../../config/index.mjs' @@ -22,6 +23,19 @@ export function fetchPostsByCategorySlug(categorySlug, take, skip) { }) } +export async function fetchNewsCategoryInfo() { + try { + const response = await axios({ + method: 'get', + url: URL_STATIC_NEWS_CATEGORY_INFO, + timeout: API_TIMEOUT, + }) + return response + } catch (err) { + console.error('Error fetching news category info: ', JSON.stringify(err)) + } +} + export async function fetchNewsCategoryPostsJSON(page = 1, take = 24) { const POSTS_PER_JSON = 120 const TAKE_PER_JSON = POSTS_PER_JSON / take @@ -52,7 +66,7 @@ export async function fetchNewsCategoryPostsJSON(page = 1, take = 24) { } } catch (err) { console.error( - 'Failed to fetch JSON of URL_STATIC_NEWS_CATEGORY_POSTS', + 'Failed to fetch JSON of URL_STATIC_NEWS_CATEGORY_POSTS: ', JSON.stringify(err) ) } From 2278ff5666c3fdbae4dc9daecdb58c0a4f8f11be Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Fri, 13 Mar 2026 14:14:19 +0800 Subject: [PATCH 6/7] feat(category):fetch JSON URLs instead of GraphQL endpoints when the category is 'news' --- .../pages/category/[slug].js | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/mirror-media-next/pages/category/[slug].js b/packages/mirror-media-next/pages/category/[slug].js index d640e6e1..608c4aa5 100644 --- a/packages/mirror-media-next/pages/category/[slug].js +++ b/packages/mirror-media-next/pages/category/[slug].js @@ -19,6 +19,8 @@ import { fetchCategoryByCategorySlug, fetchPostsByCategorySlug, fetchPremiumPostsByCategorySlug, + fetchNewsCategoryInfo, + fetchNewsCategoryPostsJSON, } from '../../utils/api/category' import { useDisplayAd } from '../../hooks/useDisplayAd' import { getCategoryOfWineSlug, getLogTraceObject } from '../../utils' @@ -188,6 +190,7 @@ const RENDER_PAGE_SIZE = 12 * @param {number} props.postsCount * @param {boolean} props.isPremium * @param {Object} props.headerData + * @param {boolean} props.isNewsCategory * @returns {React.ReactElement} */ export default function Category({ @@ -196,6 +199,7 @@ export default function Category({ category, isPremium, headerData, + isNewsCategory, }) { const categoryName = category.name || '' const { shouldShowAd, isLogInProcessFinished } = useDisplayAd() @@ -263,6 +267,7 @@ export default function Category({ category={category} renderPageSize={RENDER_PAGE_SIZE} isPremium={isPremium} + isNewsCategory={isNewsCategory} /> {shouldShowAd && isNotWineCategory ? ( @@ -298,15 +303,28 @@ export async function getServerSideProps({ query, req, res }) { isMemberOnly: false, state: 'inactive', } - try { - const { data } = await fetchCategoryByCategorySlug(categorySlug) - category = data.category || category - } catch (error) { - logGqlError( - error, - `Error occurs while getting category data in category page (${categorySlug})`, - globalLogFields - ) + + const isNewsCategory = + categorySlug.startsWith('news') || categorySlug.startsWith('news?') + + if (isNewsCategory) { + try { + const { data } = await fetchNewsCategoryInfo() + category = data.category || category + } catch (error) { + console.error('Error fetching news category:', error) + } + } else { + try { + const { data } = await fetchCategoryByCategorySlug(categorySlug) + category = data.category || category + } catch (error) { + logGqlError( + error, + `Error occurs while getting category data in category page (${categorySlug})`, + globalLogFields + ) + } } // handle category state, if `inactive` -> redirect to 404 @@ -360,7 +378,9 @@ export async function getServerSideProps({ query, req, res }) { `Error occurs while getting premium post data in category page (categorySlug: ${categorySlug})`, globalLogFields ) - } else { + } + + if (!isNewsCategory && !isPremium) { const responses = await Promise.allSettled([ fetchHeaderDataInDefaultPageLayout(), fetchPostsByCategorySlug(categorySlug, RENDER_PAGE_SIZE * 2, 0), @@ -389,6 +409,25 @@ export async function getServerSideProps({ query, req, res }) { ) } + if (isNewsCategory && !isPremium) { + const responses = await Promise.allSettled([ + fetchHeaderDataInDefaultPageLayout(), + ]) + + // handle header data + ;[sectionsData, topicsData] = processSettledResult( + responses[0], + getSectionAndTopicFromDefaultHeaderData, + `Error occurs while getting header data in category page (categorySlug: ${categorySlug})`, + globalLogFields + ) + + // handle fetch post data + const { data } = await fetchNewsCategoryPostsJSON() + posts = data.posts.items || [] + postsCount = data.posts.counts || 0 + } + // handle fetch post data if (posts.length === 0) { // fetchPost return empty array -> wrong authorId -> 404 @@ -408,6 +447,7 @@ export async function getServerSideProps({ query, req, res }) { category, isPremium, headerData: { sectionsData, topicsData }, + isNewsCategory, } return { props } } From f2691dbf59bb8f97f1bf49fdfa3cecc58b9436b7 Mon Sep 17 00:00:00 2001 From: Wendy Hu Date: Fri, 13 Mar 2026 14:18:35 +0800 Subject: [PATCH 7/7] chore: auto format --- packages/mirror-media-next/utils/api/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mirror-media-next/utils/api/index.js b/packages/mirror-media-next/utils/api/index.js index 73afaa54..10d7f7f3 100644 --- a/packages/mirror-media-next/utils/api/index.js +++ b/packages/mirror-media-next/utils/api/index.js @@ -86,7 +86,7 @@ const createStaticJsonRequest = (requestUrl) => { return async () => { if (typeof window === 'undefined') { try { - const mod = await import('../server-side-only/fetch-static-json.js') + const mod = await import('../server-side-only/fetch-static-json.js') const res = await mod.fetchStaticJson(requestUrl) // Note: fetchStaticJson internally logs whether it's from GCS mount or HTTP // fetchStaticJson returns { data: ... } format @@ -128,7 +128,9 @@ const fetchNormalSections = createStaticJsonRequest(URL_STATIC_HEADER_HEADERS) /** @type {() => Promise<{ data: { topics: Topics } }>} */ const fetchTopics = createStaticJsonRequest(URL_STATIC_TOPICS) -const fetchPremiumSections = createStaticJsonRequest(URL_STATIC_PREMIUM_SECTIONS) +const fetchPremiumSections = createStaticJsonRequest( + URL_STATIC_PREMIUM_SECTIONS +) const fetchPodcastList = createStaticJsonRequest(URL_STATIC_PODCAST_LIST)