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 && (
+
+ )}
{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:
-
+
)}
{tag && (
@@ -382,6 +390,25 @@ const TagPage = ({
)}
+
+ ,
+ }}
+ emptyScreen={<>>}
+ />
+