fix(ssr): restore shared user profiles

This commit is contained in:
DIYgod 2026-07-25 15:06:20 +08:00
parent cf224d63f5
commit 110960a6f0
7 changed files with 117 additions and 30 deletions

View File

@ -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)

View File

@ -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 <LoadingCircle size="large" className="center fixed inset-0" />
}
if (!user.data) {
if (user.error instanceof FollowAPIError && user.error.status === 404) {
return <NotFoundContent />
}
return <ProfileLoadError onRetry={() => void user.refetch()} />
}
return (
<>
{user.isLoading ? (
<LoadingCircle size="large" className="center fixed inset-0" />
) : (
<Fragment>
<UserHero user={user.data!} />
<Lists userId={user.data?.id} />
{/* Subscriptions Section */}
<Subscriptions userId={user.data?.id} />
</Fragment>
)}
</>
<Fragment>
<UserHero user={user.data} />
<Lists userId={user.data.id} />
{/* Subscriptions Section */}
<Subscriptions userId={user.data.id} />
</Fragment>
)
}
const ProfileLoadError = ({ onRetry }: { onRetry: () => void }) => (
<div className="mx-auto flex min-h-[60vh] max-w-xl flex-col items-center justify-center px-6 text-center">
<i className="i-mgc-warning-fill mb-6 size-12 text-orange-500" />
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
Unable to load this profile
</h1>
<p className="mt-3 text-zinc-500 dark:text-zinc-400">
This may be a temporary problem. Please try again.
</p>
<Button buttonClassName="mt-8" onClick={onRetry}>
Try again
</Button>
</div>
)
const UserHero = ({ user }: { user: UserProfile }) => {
const subscriptions = useUserSubscriptionsQuery(user.id)

View File

@ -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
}

View File

@ -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",
})
})
})

View File

@ -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))

View File

@ -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

View File

@ -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,