From 110960a6f0d06cd27a05730930c0c16ce0bb4bc1 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sat, 25 Jul 2026 15:06:20 +0800 Subject: [PATCH] fix(ssr): restore shared user profiles --- apps/ssr/client/components/common/404.tsx | 2 +- .../pages/(main)/share/users/[id]/index.tsx | 47 ++++++++++++----- apps/ssr/client/query/users.ts | 12 ++--- apps/ssr/src/lib/user-profile-params.test.ts | 50 +++++++++++++++++++ apps/ssr/src/lib/user-profile-params.ts | 19 +++++++ apps/ssr/src/router/og/user.tsx | 11 ++-- apps/ssr/wrangler.jsonc | 6 ++- 7 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 apps/ssr/src/lib/user-profile-params.test.ts create mode 100644 apps/ssr/src/lib/user-profile-params.ts diff --git a/apps/ssr/client/components/common/404.tsx b/apps/ssr/client/components/common/404.tsx index 607916320..1b356ceec 100644 --- a/apps/ssr/client/components/common/404.tsx +++ b/apps/ssr/client/components/common/404.tsx @@ -7,7 +7,7 @@ import { m, useAnimationControls } from "motion/react" import { Fragment, useEffect, useState } from "react" import * as React from "react" -const NotFoundContent = () => { +export const NotFoundContent = () => { const [glitchText, setGlitchText] = useState("404") const [isGlitching, setIsGlitching] = useState(false) diff --git a/apps/ssr/client/pages/(main)/share/users/[id]/index.tsx b/apps/ssr/client/pages/(main)/share/users/[id]/index.tsx index cbb3ef9fc..2698ed3cf 100644 --- a/apps/ssr/client/pages/(main)/share/users/[id]/index.tsx +++ b/apps/ssr/client/pages/(main)/share/users/[id]/index.tsx @@ -1,3 +1,4 @@ +import { NotFoundContent } from "@client/components/common/404" import { FeedIcon } from "@client/components/ui/feed-icon" import { openInFollowApp } from "@client/lib/helper" import { UrlBuilder } from "@client/lib/url-builder" @@ -10,6 +11,7 @@ import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { useTitle } from "@follow/hooks" import { cn } from "@follow/utils/utils" import type { SubscriptionWithFeed, UserProfile } from "@follow-app/client-sdk" +import { FollowAPIError } from "@follow-app/client-sdk" import * as React from "react" import { Fragment, memo, useState } from "react" import { useParams } from "react-router" @@ -89,22 +91,43 @@ export const Component = () => { useTitle(user.data?.name) + if (user.isLoading) { + return + } + + if (!user.data) { + if (user.error instanceof FollowAPIError && user.error.status === 404) { + return + } + + return void user.refetch()} /> + } + return ( - <> - {user.isLoading ? ( - - ) : ( - - - - {/* Subscriptions Section */} - - - )} - + + + + {/* Subscriptions Section */} + + ) } +const ProfileLoadError = ({ onRetry }: { onRetry: () => void }) => ( +
+ +

+ Unable to load this profile +

+

+ This may be a temporary problem. Please try again. +

+ +
+) + const UserHero = ({ user }: { user: UserProfile }) => { const subscriptions = useUserSubscriptionsQuery(user.id) diff --git a/apps/ssr/client/query/users.ts b/apps/ssr/client/query/users.ts index f351e4cb8..a3f0318cb 100644 --- a/apps/ssr/client/query/users.ts +++ b/apps/ssr/client/query/users.ts @@ -2,7 +2,7 @@ import { followClient } from "@client/lib/api-fetch" import { getProviders } from "@client/lib/auth" import { getHydrateData } from "@client/lib/helper" import type { LoginHydrateData } from "@client/pages/(login)/login/metadata" -import { isBizId, sortByAlphabet } from "@follow/utils/utils" +import { sortByAlphabet } from "@follow/utils/utils" import type { InboxSubscriptionResponse, ListSubscriptionResponse, @@ -10,6 +10,8 @@ import type { } from "@follow-app/client-sdk" import { useQuery } from "@tanstack/react-query" +import { getUserProfile } from "../../src/lib/user-profile-params" + type GetUserSubscriptionsResponse = ( SubscriptionWithFeed | ListSubscriptionResponse | InboxSubscriptionResponse )[] @@ -69,13 +71,7 @@ export const useUserSubscriptionsQuery = (userId: string | undefined) => { } export const fetchUser = async (handleOrId: string | undefined) => { - const handle = isBizId(handleOrId || "") - ? handleOrId - : `${handleOrId}`.startsWith("@") - ? `${handleOrId}`.slice(1) - : handleOrId - - const res = await followClient.api.profiles.getProfile({ id: handleOrId, handle }) + const res = await getUserProfile(followClient, handleOrId) return res.data } diff --git a/apps/ssr/src/lib/user-profile-params.test.ts b/apps/ssr/src/lib/user-profile-params.test.ts new file mode 100644 index 000000000..1cbef864b --- /dev/null +++ b/apps/ssr/src/lib/user-profile-params.test.ts @@ -0,0 +1,50 @@ +import type { FollowClient } from "@follow-app/client-sdk" +import { describe, expect, it, vi } from "vitest" + +import { getUserProfile, resolveUserProfileParams } from "./user-profile-params" + +vi.mock("@follow/utils/utils", () => ({ + isBizId: (value: string | undefined) => value === "41125409313095680", +})) + +describe("resolveUserProfileParams", () => { + it("uses a handle without also sending it as an id", () => { + expect(resolveUserProfileParams("DIYgod")).toEqual({ + id: undefined, + handle: "DIYgod", + }) + }) + + it("removes a leading at sign from handles", () => { + expect(resolveUserProfileParams("@DIYgod")).toEqual({ + id: undefined, + handle: "DIYgod", + }) + }) + + it("uses a business id without also sending it as a handle", () => { + expect(resolveUserProfileParams("41125409313095680")).toEqual({ + id: "41125409313095680", + handle: undefined, + }) + }) + + it("uses the resolved parameters for the profile request", async () => { + const getProfile = vi.fn().mockResolvedValue({ data: { id: "profile-id" } }) + const apiClient = { + api: { + profiles: { + getProfile, + }, + }, + } as unknown as FollowClient + + await getUserProfile(apiClient, "DIYgod") + + expect(getProfile).toHaveBeenCalledOnce() + expect(getProfile).toHaveBeenCalledWith({ + id: undefined, + handle: "DIYgod", + }) + }) +}) diff --git a/apps/ssr/src/lib/user-profile-params.ts b/apps/ssr/src/lib/user-profile-params.ts new file mode 100644 index 000000000..6c8598be9 --- /dev/null +++ b/apps/ssr/src/lib/user-profile-params.ts @@ -0,0 +1,19 @@ +import { isBizId } from "@follow/utils/utils" +import type { FollowClient } from "@follow-app/client-sdk" + +export const resolveUserProfileParams = (handleOrId: string | undefined) => { + if (isBizId(handleOrId || "")) { + return { + id: handleOrId, + handle: undefined, + } + } + + return { + id: undefined, + handle: handleOrId?.startsWith("@") ? handleOrId.slice(1) : handleOrId, + } +} + +export const getUserProfile = (apiClient: FollowClient, handleOrId: string | undefined) => + apiClient.api.profiles.getProfile(resolveUserProfileParams(handleOrId)) diff --git a/apps/ssr/src/router/og/user.tsx b/apps/ssr/src/router/og/user.tsx index d2d65ec9b..047dab559 100644 --- a/apps/ssr/src/router/og/user.tsx +++ b/apps/ssr/src/router/og/user.tsx @@ -1,17 +1,12 @@ -import { isBizId } from "@follow/utils/utils" import type { FollowClient } from "@follow-app/client-sdk" import * as React from "react" import { renderToImage } from "../../lib/og/render-to-image" +import { getUserProfile } from "../../lib/user-profile-params" import { getImageBase64, OGAvatar, OGCanvas } from "./__base" -export const renderUserOG = async (apiClient: FollowClient, id: string) => { - const handle = isBizId(id || "") ? id : `${id}`.startsWith("@") ? `${id}`.slice(1) : id - - const user = await apiClient.api.profiles.getProfile({ - id, - handle, - }) +export const renderUserOG = async (apiClient: FollowClient, handleOrId: string) => { + const user = await getUserProfile(apiClient, handleOrId) if (!user) { throw 404 diff --git a/apps/ssr/wrangler.jsonc b/apps/ssr/wrangler.jsonc index 97a7676b9..1e3a59f77 100644 --- a/apps/ssr/wrangler.jsonc +++ b/apps/ssr/wrangler.jsonc @@ -3,7 +3,11 @@ "name": "folo-ssr", "main": "dist/worker/worker-entry.mjs", "compatibility_date": "2026-02-01", - "compatibility_flags": ["nodejs_compat"], + "compatibility_flags": [ + "nodejs_compat", + // The SSR Worker fetches api.folo.is, another Worker Route in the same zone. + "global_fetch_strictly_public", + ], "observability": { "logs": { "enabled": true,