From f8a05ac2848fdad41295e2733c9c2c15e4e5605e Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:06:19 +0200 Subject: [PATCH 1/3] fix(webapp): improve tag page SSR content and linking Pre-render top posts and related tags in tag-page static props, and keep an H1 visible during fallback so crawlers always see meaningful HTML content and links. Made-with: Cursor --- packages/webapp/__tests__/TagPage.tsx | 7 +- packages/webapp/pages/tags/[tag].tsx | 138 +++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 17 deletions(-) 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..4130bcd444a 100644 --- a/packages/webapp/pages/tags/[tag].tsx +++ b/packages/webapp/pages/tags/[tag].tsx @@ -59,6 +59,7 @@ import { RelatedSources } from '@dailydotdev/shared/src/components/RelatedSource import { ActiveFeedNameContext } from '@dailydotdev/shared/src/contexts'; import HorizontalFeed from '@dailydotdev/shared/src/components/feeds/HorizontalFeed'; import { PostType } from '@dailydotdev/shared/src/graphql/posts'; +import { gql } from 'graphql-request'; import { useFeature } from '@dailydotdev/shared/src/components/GrowthBookProvider'; import { feature } from '@dailydotdev/shared/src/lib/featureManagement'; import { cloudinarySourceRoadmap } from '@dailydotdev/shared/src/lib/image'; @@ -75,9 +76,49 @@ import { defaultOpenGraph, defaultSeo } from '../../next-seo'; interface TagPageProps extends DynamicSeoProps { tag: string; initialData: Keyword; + topPosts: TagTopPost[]; + recommendedTags: string[]; } -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 TAG_TOP_POSTS_QUERY = gql` + query TagTopPosts($tag: String!, $first: Int) { + page: tagFeed(tag: $tag, first: $first, ranking: POPULARITY) { + edges { + node { + id + title + slug + } + } + } + } +`; + +const TagRecommendedTags = ({ + tag, + blockedTags, + initialTags = [], +}: TagRecommendedTagsProps): ReactElement => { const { data: recommendedTags, isPending } = useQuery({ queryKey: [RequestKey.RecommendedTags, null, tag], @@ -92,10 +133,12 @@ const TagRecommendedTags = ({ tag, blockedTags }): ReactElement => { staleTime: StaleTime.OneHour, }); + const tags = recommendedTags?.recommendedTags?.tags ?? initialTags; + return ( ); }; @@ -133,8 +176,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( @@ -187,7 +235,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 +342,25 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => { {initialData?.flags?.description && (

{initialData?.flags?.description}

)} + {topPosts.length > 0 && ( +
+

Top posts:

+ +
+ )} {tag && ( ({ + name: recommendedTag, + }))} /> )} {showRoadmap && initialData?.flags?.roadmap && ( @@ -405,28 +478,59 @@ 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 + ?.map((recommendedTag) => recommendedTag.name) + .filter( + (recommendedTag): recommendedTag is string => !!recommendedTag, + ) ?? []; const seo = getSeoData( - initialData.flags?.title || params.tag, + initialData.flags?.title || tag, initialData.flags?.description, ); @@ -434,7 +538,9 @@ export async function getStaticProps({ props: { seo, initialData, - tag: params.tag, + tag, + topPosts, + recommendedTags, }, revalidate: 3600, }; From a770abc8bc6e668480ec517b8083e60cb4fcc0b3 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:15:00 +0200 Subject: [PATCH 2/3] refactor(webapp): align tag-page SSR implementation with review feedback Move the top-post query to shared GraphQL, fix tag page prop typing for null initial data, remove recommended-tags remapping, and use Link components for top-post navigation while keeping current UI sections unchanged. Made-with: Cursor --- packages/shared/src/graphql/feed.ts | 14 ++++++++++ packages/webapp/pages/tags/[tag].tsx | 40 +++++++++------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/shared/src/graphql/feed.ts b/packages/shared/src/graphql/feed.ts index 55902969616..1e5bbdb7135 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/webapp/pages/tags/[tag].tsx b/packages/webapp/pages/tags/[tag].tsx index 4130bcd444a..7ca234bd8d4 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'; @@ -59,7 +60,6 @@ import { RelatedSources } from '@dailydotdev/shared/src/components/RelatedSource import { ActiveFeedNameContext } from '@dailydotdev/shared/src/contexts'; import HorizontalFeed from '@dailydotdev/shared/src/components/feeds/HorizontalFeed'; import { PostType } from '@dailydotdev/shared/src/graphql/posts'; -import { gql } from 'graphql-request'; import { useFeature } from '@dailydotdev/shared/src/components/GrowthBookProvider'; import { feature } from '@dailydotdev/shared/src/lib/featureManagement'; import { cloudinarySourceRoadmap } from '@dailydotdev/shared/src/lib/image'; @@ -75,9 +75,9 @@ import { defaultOpenGraph, defaultSeo } from '../../next-seo'; interface TagPageProps extends DynamicSeoProps { tag: string; - initialData: Keyword; + initialData: Keyword | null; topPosts: TagTopPost[]; - recommendedTags: string[]; + recommendedTags: TagsData['tags']; } interface TagTopPost { @@ -100,20 +100,6 @@ interface TagRecommendedTagsProps { initialTags?: TagsData['tags']; } -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 - } - } - } - } -`; - const TagRecommendedTags = ({ tag, blockedTags, @@ -348,7 +334,12 @@ const TagPage = ({ @@ -358,9 +349,7 @@ const TagPage = ({ ({ - name: recommendedTag, - }))} + initialTags={recommendedTags} /> )} {showRoadmap && initialData?.flags?.roadmap && ( @@ -523,12 +512,7 @@ export async function getStaticProps({ topPostsResult?.page?.edges ?.map((edge) => edge.node) .filter((post) => !!post.title) ?? []; - const recommendedTags = - recommendedTagsResult?.recommendedTags?.tags - ?.map((recommendedTag) => recommendedTag.name) - .filter( - (recommendedTag): recommendedTag is string => !!recommendedTag, - ) ?? []; + const recommendedTags = recommendedTagsResult?.recommendedTags?.tags ?? []; const seo = getSeoData( initialData.flags?.title || tag, initialData.flags?.description, @@ -545,7 +529,7 @@ export async function getStaticProps({ revalidate: 3600, }; } catch (error) { - // keyword not found, ignoring for now + // Return fallback props for any request failure in getStaticProps. return notFoundResponse; } } From b9d9341ea578adc66546509d28cd7813bea14357 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:12:23 +0200 Subject: [PATCH 3/3] refactor(webapp): render tag top posts with horizontal feed cards Replace the visible top-post bullet list with a horizontal feed section above Most upvoted posts while keeping SSR link coverage through a screen-reader-only link block. Made-with: Cursor --- packages/shared/src/lib/query.ts | 1 + packages/webapp/pages/tags/[tag].tsx | 55 +++++++++++++++++++++------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index e14bf44dfbc..c3ef6dcb032 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/pages/tags/[tag].tsx b/packages/webapp/pages/tags/[tag].tsx index 7ca234bd8d4..56c99badcc5 100644 --- a/packages/webapp/pages/tags/[tag].tsx +++ b/packages/webapp/pages/tags/[tag].tsx @@ -183,6 +183,18 @@ const TagPage = ({ }), [tag], ); + const topPostsQueryVariables = useMemo( + () => ({ + tag, + ranking: 'POPULARITY', + supportedTypes: [ + PostType.Article, + PostType.VideoYouTube, + PostType.Collection, + ], + }), + [tag], + ); const bestDiscussedQueryVariables = useMemo( () => ({ tag, @@ -329,20 +341,16 @@ const TagPage = ({

{initialData?.flags?.description}

)} {topPosts.length > 0 && ( -
-

Top posts:

-
    - {topPosts.map((post) => ( -
  • - - {post.title} - -
  • - ))} -
+
+ {topPosts.map((post) => ( + + {post.title} + + ))}
)} {tag && ( @@ -382,6 +390,25 @@ const TagPage = ({ )} + + , + }} + emptyScreen={<>} + /> +