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 && (
+
+ )}
{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;
}
}