Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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({
Expand All @@ -43,6 +45,7 @@ export default function CategoryArticles({
category,
renderPageSize,
isPremium,
isNewsCategory,
}) {
const fetchPageSize = renderPageSize * 2

Expand All @@ -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
Expand Down
13 changes: 12 additions & 1 deletion packages/mirror-media-next/config/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ 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

Expand Down Expand Up @@ -97,6 +98,8 @@ 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
STORY_GQL_ENDPOINT =
Expand Down Expand Up @@ -165,6 +168,8 @@ 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 ||
'https://go-story-staging-983956931553.asia-east1.run.app/api/graphql'
Expand Down Expand Up @@ -230,6 +235,8 @@ 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 ||
'https://go-story-dev-983956931553.asia-east1.run.app/api/graphql'
Expand Down Expand Up @@ -301,6 +308,8 @@ 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 ||
'https://go-story-dev-983956931553.asia-east1.run.app/api/graphql'
Expand Down Expand Up @@ -385,4 +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,
}
60 changes: 50 additions & 10 deletions packages/mirror-media-next/pages/category/[slug].js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
fetchCategoryByCategorySlug,
fetchPostsByCategorySlug,
fetchPremiumPostsByCategorySlug,
fetchNewsCategoryInfo,
fetchNewsCategoryPostsJSON,
} from '../../utils/api/category'
import { useDisplayAd } from '../../hooks/useDisplayAd'
import { getCategoryOfWineSlug, getLogTraceObject } from '../../utils'
Expand Down Expand Up @@ -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({
Expand All @@ -196,6 +199,7 @@ export default function Category({
category,
isPremium,
headerData,
isNewsCategory,
}) {
const categoryName = category.name || ''
const { shouldShowAd, isLogInProcessFinished } = useDisplayAd()
Expand Down Expand Up @@ -263,6 +267,7 @@ export default function Category({
category={category}
renderPageSize={RENDER_PAGE_SIZE}
isPremium={isPremium}
isNewsCategory={isNewsCategory}
/>

{shouldShowAd && isNotWineCategory ? (
Expand Down Expand Up @@ -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?')
Comment on lines +307 to +308

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

判斷是否為「新聞」分類的邏輯 categorySlug.startsWith('news') || categorySlug.startsWith('news?') 可能不夠穩健。categorySlug 是從 URL 路徑中解析而來,不包含查詢參數(query string)。因此,對於 /category/news?page=2 這樣的 URL,categorySlug 的值會是 newsstartsWith('news?') 這個條件可能永遠不會為真,或者會意外匹配到名為 news-something 的分類。建議將判斷條件修改為更精確的 categorySlug === 'news',以符合預期行為。

Suggested change
const isNewsCategory =
categorySlug.startsWith('news') || categorySlug.startsWith('news?')
const isNewsCategory = categorySlug === '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
)
}
}
Comment on lines +310 to 328

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

這段程式碼透過 if/else 處理兩種不同來源的分類資料,但錯誤處理方式不一致(console.error vs logGqlError),且程式碼結構可以更簡潔。建議將其重構,使用 Promise.allSettledprocessSettledResult 來統一處理資料獲取與錯誤記錄,這也符合此檔案中其他資料獲取邏輯的風格。

  const fetchCategoryPromise = isNewsCategory
    ? fetchNewsCategoryInfo()
    : fetchCategoryByCategorySlug(categorySlug)

  const categoryResponses = await Promise.allSettled([fetchCategoryPromise])
  const categoryResult = processSettledResult(
    categoryResponses[0],
    (res) => res?.data?.category,
    `Error occurs while getting category data in category page (${categorySlug})`,
    globalLogFields
  )
  category = categoryResult || category


// handle category state, if `inactive` -> redirect to 404
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
Comment on lines +412 to +429

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

此處直接呼叫 fetchNewsCategoryPostsJSON 並解構其結果,但沒有任何錯誤處理。如果 API 請求失敗,getServerSideProps 將會拋出錯誤,導致伺服器回傳 500 錯誤頁面。建議參考此檔案中其他資料請求的作法,使用 Promise.allSettled 搭配 processSettledResult 來處理,以確保在 API 請求失敗時能有優雅的降級處理(graceful degradation)。

  if (isNewsCategory && !isPremium) {
    const responses = await Promise.allSettled([
      fetchHeaderDataInDefaultPageLayout(),
      fetchNewsCategoryPostsJSON(),
    ])

    // 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 postsWrapper = processSettledResult(
      responses[1],
      (res) => res?.data?.posts,
      `Error occurs while getting news category posts in category page (categorySlug: ${categorySlug})`,
      globalLogFields
    )

    posts = postsWrapper?.items || []
    postsCount = postsWrapper?.counts || 0
  }


// handle fetch post data
if (posts.length === 0) {
// fetchPost return empty array -> wrong authorId -> 404
Expand All @@ -408,6 +447,7 @@ export async function getServerSideProps({ query, req, res }) {
category,
isPremium,
headerData: { sectionsData, topicsData },
isNewsCategory,
}
return { props }
}
55 changes: 55 additions & 0 deletions packages/mirror-media-next/utils/api/category.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
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_INFO,
URL_STATIC_NEWS_CATEGORY_POSTS,
} from '../../config/index.mjs'

export function fetchPostsByCategorySlug(categorySlug, take, skip) {
return client.query({
Expand All @@ -17,6 +23,55 @@ 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))
}
Comment on lines +34 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

catch 區塊中僅將錯誤印出至 console,但未將錯誤向上拋出。這會導致函式在發生錯誤時返回 undefined,而呼叫此函式的地方 (例如 pages/category/[slug].js 中的 getServerSideProps) 在試圖對 undefined 進行解構時 (如 const { data } = ...) 會觸發 TypeError,導致頁面崩潰。建議在 catch 區塊中重新拋出錯誤,讓上層的錯誤處理機制(如 Promise.allSettledtry/catch)能夠捕捉到。

  } catch (err) {
    console.error('Error fetching news category info: ', JSON.stringify(err))
    throw err
  }

}

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)
)
Comment on lines +68 to +71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 JSON.stringify(err) 來記錄錯誤可能會遺失 Error 物件中的重要資訊(例如 stack trace),尤其對於 axios 的錯誤物件。建議直接記錄整個 err 物件,或使用專案中既有的錯誤記錄工具(如 logAxiosError),以便於日後除錯。這個建議同樣適用於 fetchNewsCategoryInfo 函式。

}
Comment on lines +67 to +72

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

fetchNewsCategoryInfo 函式有相同的問題。catch 區塊補捉到錯誤後沒有重新拋出,會導致函式在發生錯誤時返回 undefined,進而使呼叫端在解構時發生 TypeError 並崩潰。請在 catch 區塊中重新拋出錯誤。

  } catch (err) {
    console.error(
      'Failed to fetch JSON of URL_STATIC_NEWS_CATEGORY_POSTS: ',
      JSON.stringify(err)
    )
    throw err
  }

}

export function fetchPremiumPostsByCategorySlug(categorySlug, take, skip) {
return client.query({
query: fetchPosts,
Expand Down
6 changes: 4 additions & 2 deletions packages/mirror-media-next/utils/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down