From d7a8a65d0e14ba633c74567da41575b692423e11 Mon Sep 17 00:00:00 2001 From: idoshamun <1993245+idoshamun@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:26:02 +0000 Subject: [PATCH] fix(webapp): return a real 404 for squads that don't exist The squad page caught NOT_FOUND alongside FORBIDDEN and rendered a 200 noindex shell, so /squads/ looked like a live page to crawlers and agents. Private squads (FORBIDDEN under unauthenticated SSR) keep the existing noindex fallback. --- .../webapp/__tests__/SquadPageStaticProps.spec.ts | 13 ++++++++++++- packages/webapp/pages/squads/[handle]/index.tsx | 12 ++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/webapp/__tests__/SquadPageStaticProps.spec.ts b/packages/webapp/__tests__/SquadPageStaticProps.spec.ts index 9da08719251..1a5ae006be7 100644 --- a/packages/webapp/__tests__/SquadPageStaticProps.spec.ts +++ b/packages/webapp/__tests__/SquadPageStaticProps.spec.ts @@ -150,7 +150,7 @@ describe('squad page getServerSideProps seo', () => { // SSR is always unauthenticated, so the API rejects every private squad with // FORBIDDEN before we ever see `public`. This fallback used to ship no seo at // all, which left private squads advertised as index,follow. - it.each([ApiError.Forbidden, ApiError.NotFound, ApiError.RateLimited])( + it.each([ApiError.Forbidden, ApiError.RateLimited])( 'marks the %s fallback as noindex and nofollow', async (code) => { mockRequest.mockRejectedValue({ @@ -170,4 +170,15 @@ describe('squad page getServerSideProps seo', () => { }); }, ); + + it('returns a real 404 for a squad that does not exist', async () => { + mockRequest.mockRejectedValue({ + response: { errors: [{ extensions: { code: ApiError.NotFound } }] }, + }); + + const result = await runGssp(); + + expect(result).toEqual({ notFound: true }); + expect(mockStaticFields).not.toHaveBeenCalled(); + }); }); diff --git a/packages/webapp/pages/squads/[handle]/index.tsx b/packages/webapp/pages/squads/[handle]/index.tsx index c22782787fa..fe94d9923d7 100644 --- a/packages/webapp/pages/squads/[handle]/index.tsx +++ b/packages/webapp/pages/squads/[handle]/index.tsx @@ -501,12 +501,20 @@ export async function getServerSideProps({ const errors = Object.values(ApiError); const errorCode = clientError?.response?.errors?.[0]?.extensions?.code; + // A squad that doesn't exist is a real 404, not a soft one, so crawlers + // and agents don't index or cite a page that isn't there. + if (errorCode === ApiError.NotFound) { + setCacheHeader(); + + return { notFound: true }; + } + if (errors.includes(errorCode)) { setCacheHeader(); // SSR always runs unauthenticated, so every private squad resolves as - // FORBIDDEN here. This branch also serves the soft-404/rate-limited - // cases, none of which should ever be advertised as indexable. + // FORBIDDEN here. This branch also serves the rate-limited and other + // API error cases, none of which should ever be advertised as indexable. return { props: { handle, seo: { ...noindexSeoProps } }, };