diff --git a/packages/shared/src/graphql/feed.ts b/packages/shared/src/graphql/feed.ts index 89735461824..da2a358b4b3 100644 --- a/packages/shared/src/graphql/feed.ts +++ b/packages/shared/src/graphql/feed.ts @@ -190,6 +190,20 @@ export const TAG_FEED_QUERY = gql` ${FEED_POST_CONNECTION_FRAGMENT} `; +export const TAG_TOP_POSTS_QUERY = gql` + query TagTopPosts($tag: String!, $first: Int) { + page: tagFeed(tag: $tag, first: $first, ranking: POPULARITY) { + edges { + node { + id + title + slug + } + } + } + } +`; + export const SOURCE_FEED_QUERY = gql` query SourceFeed( $source: ID! diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index b4192602e6b..17f381dccc1 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -54,6 +54,7 @@ export enum OtherFeedPage { SourcePage = 'sources[source]', SourceMostUpvoted = 'sources[source]/most-upvoted', SourceBestDiscussed = 'sources[source]/best-discussed', + TagsTopPosts = 'tags[tag]/top-posts', TagsMostUpvoted = 'tags[tag]/most-upvoted', TagsBestDiscussed = 'tags[tag]/best-discussed', Explore = 'posts', diff --git a/packages/webapp/__tests__/TagPage.tsx b/packages/webapp/__tests__/TagPage.tsx index 5fc53dc6814..8c3d2fb9d91 100644 --- a/packages/webapp/__tests__/TagPage.tsx +++ b/packages/webapp/__tests__/TagPage.tsx @@ -139,7 +139,12 @@ const renderComponent = ( > - + diff --git a/packages/webapp/pages/tags/[tag].tsx b/packages/webapp/pages/tags/[tag].tsx index 7a70229e344..56c99badcc5 100644 --- a/packages/webapp/pages/tags/[tag].tsx +++ b/packages/webapp/pages/tags/[tag].tsx @@ -23,6 +23,7 @@ import { MOST_DISCUSSED_FEED_QUERY, MOST_UPVOTED_FEED_QUERY, TAG_FEED_QUERY, + TAG_TOP_POSTS_QUERY, } from '@dailydotdev/shared/src/graphql/feed'; import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext'; import type { ButtonProps } from '@dailydotdev/shared/src/components/buttons/Button'; @@ -74,10 +75,36 @@ import { defaultOpenGraph, defaultSeo } from '../../next-seo'; interface TagPageProps extends DynamicSeoProps { tag: string; - initialData: Keyword; + initialData: Keyword | null; + topPosts: TagTopPost[]; + recommendedTags: TagsData['tags']; } -const TagRecommendedTags = ({ tag, blockedTags }): ReactElement => { +interface TagTopPost { + id: string; + title?: string; + slug?: string; +} + +interface TagTopPostsData { + page?: { + edges?: { + node: TagTopPost; + }[]; + }; +} + +interface TagRecommendedTagsProps { + tag: string; + blockedTags?: string[]; + initialTags?: TagsData['tags']; +} + +const TagRecommendedTags = ({ + tag, + blockedTags, + initialTags = [], +}: TagRecommendedTagsProps): ReactElement => { const { data: recommendedTags, isPending } = useQuery({ queryKey: [RequestKey.RecommendedTags, null, tag], @@ -92,10 +119,12 @@ const TagRecommendedTags = ({ tag, blockedTags }): ReactElement => { staleTime: StaleTime.OneHour, }); + const tags = recommendedTags?.recommendedTags?.tags ?? initialTags; + return ( ); }; @@ -133,8 +162,13 @@ const TagTopSources = ({ tag }: { tag: string }) => { ); }; -const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { - const { isFallback, push } = useRouter(); +const TagPage = ({ + tag, + initialData, + topPosts, + recommendedTags, +}: TagPageProps): ReactElement => { + const { isFallback, push, query } = useRouter(); const showRoadmap = useFeature(feature.showRoadmap); const { user, showLogin } = useContext(AuthContext); const mostUpvotedQueryVariables = useMemo( @@ -149,6 +183,18 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { }), [tag], ); + const topPostsQueryVariables = useMemo( + () => ({ + tag, + ranking: 'POPULARITY', + supportedTypes: [ + PostType.Article, + PostType.VideoYouTube, + PostType.Collection, + ], + }), + [tag], + ); const bestDiscussedQueryVariables = useMemo( () => ({ tag, @@ -187,7 +233,17 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { }, [feedSettings, tag]); if (isFallback) { - return <>; + const fallbackTag = typeof query.tag === 'string' ? query.tag : tag; + return ( + + +
+ +

{fallbackTag}

+
+
+
+ ); } const followButtonProps: ButtonProps<'button'> = { @@ -284,10 +340,24 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { {initialData?.flags?.description && (

{initialData?.flags?.description}

)} + {topPosts.length > 0 && ( +
+ {topPosts.map((post) => ( + + {post.title} + + ))} +
+ )} {tag && ( )} {showRoadmap && initialData?.flags?.roadmap && ( @@ -320,6 +390,25 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { )} + + , + }} + emptyScreen={<>} + /> + @@ -405,28 +494,54 @@ export async function getStaticProps({ }: GetStaticPropsContext): Promise< GetStaticPropsResult > { + const tag = params?.tag; + if (!tag) { + return { notFound: true, revalidate: 3600 }; + } + const notFoundResponse = { revalidate: 3600, props: { - tag: params.tag, + tag, initialData: null, - seo: getSeoData(params.tag), + topPosts: [], + recommendedTags: [], + seo: getSeoData(tag), }, }; try { - const result = await gqlClient.request<{ keyword: Keyword }>( - KEYWORD_QUERY, - { value: params.tag }, - ); + const [keywordResult, topPostsResult, recommendedTagsResult] = + await Promise.all([ + gqlClient.request<{ keyword: Keyword }>(KEYWORD_QUERY, { + value: tag, + }), + gqlClient + .request(TAG_TOP_POSTS_QUERY, { + tag, + first: 10, + }) + .catch(() => null), + gqlClient + .request<{ recommendedTags: TagsData }>(GET_RECOMMENDED_TAGS_QUERY, { + tags: [tag], + excludedTags: [], + }) + .catch(() => null), + ]); - if (!result?.keyword) { + if (!keywordResult?.keyword) { return notFoundResponse; } - const initialData = result.keyword; + const initialData = keywordResult.keyword; + const topPosts = + topPostsResult?.page?.edges + ?.map((edge) => edge.node) + .filter((post) => !!post.title) ?? []; + const recommendedTags = recommendedTagsResult?.recommendedTags?.tags ?? []; const seo = getSeoData( - initialData.flags?.title || params.tag, + initialData.flags?.title || tag, initialData.flags?.description, ); @@ -434,12 +549,14 @@ export async function getStaticProps({ props: { seo, initialData, - tag: params.tag, + tag, + topPosts, + recommendedTags, }, revalidate: 3600, }; } catch (error) { - // keyword not found, ignoring for now + // Return fallback props for any request failure in getStaticProps. return notFoundResponse; } }