Skip to content

Commit f0dbe79

Browse files
devin-ai-integration[bot]sbawabevercel[bot]
authored
feat(dashboard): move homepage image fetching to server-side with caching (#4778)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Sarah Bawabe <[email protected]> Co-authored-by: sarah bawabe <[email protected]> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
1 parent de102af commit f0dbe79

File tree

9 files changed

+145
-195
lines changed

9 files changed

+145
-195
lines changed

packages/fern-dashboard/src/app/api/homepage-images/get/handler.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

packages/fern-dashboard/src/app/api/homepage-images/get/route.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import "server-only";
2+
3+
import { unstable_cache } from "next/cache";
4+
5+
import { doesObjectExist, getPresignedUrlForS3Object } from "@/app/services/s3";
6+
7+
import { getHomepageImagesS3BucketName } from "../../../api/homepage-images/constants";
8+
import generateHomepageImages from "../../../api/homepage-images/generate/handler";
9+
import { getS3KeyForHomepageScreenshot } from "../../../api/homepage-images/getS3KeyForHomepageScreenshot";
10+
import type { Theme } from "../../../api/homepage-images/types";
11+
12+
async function getHomepageImageUrlUncached({ url, theme }: { url: string; theme: Theme }) {
13+
console.debug(`Getting homepage image for ${url}`);
14+
const bucketName = getHomepageImagesS3BucketName();
15+
const objectKey = getS3KeyForHomepageScreenshot({ url, theme });
16+
17+
let screenshotExists = await doesObjectExist({
18+
bucketName: bucketName,
19+
objectKey
20+
});
21+
22+
if (!screenshotExists) {
23+
await generateHomepageImages({ url });
24+
// Re-check if the object exists after generation
25+
screenshotExists = await doesObjectExist({
26+
bucketName: bucketName,
27+
objectKey
28+
});
29+
}
30+
31+
if (screenshotExists) {
32+
return {
33+
imageUrl: await getPresignedUrlForS3Object({ bucketName, objectKey })
34+
};
35+
}
36+
37+
return null;
38+
}
39+
40+
export function getHomepageImageUrl({ url, theme }: { url: string; theme: Theme }) {
41+
return unstable_cache(() => getHomepageImageUrlUncached({ url, theme }), ["homepage-image-url", url, theme], {
42+
revalidate: 3600 * 3 // 3 hours
43+
})();
44+
}

packages/fern-dashboard/src/app/services/dashboard-api/client.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { getMyOrganizations } from "@/app/api/get-my-organizations/route";
44
import type { getOrgInvitations } from "@/app/api/get-org-invitations/route";
55
import type { getOrgMembers } from "@/app/api/get-org-members/route";
66
import type { validateGithubBranch } from "@/app/api/get-validate-github-branch/route";
7-
import type { getHomepageImageUrl } from "@/app/api/homepage-images/get/route";
87
import type { postDocsGithubSource } from "@/app/api/post-docs-github-source/route";
98
import type { postCreatePr } from "@/app/api/post-git-create-pr/route";
109
import type { generateSignedUploadUrl } from "@/app/api/signed-image-url/generate/route";
@@ -18,8 +17,6 @@ export const DashboardApiClient = {
1817
typedFetch<getOrgInvitations.Response>("/api/get-org-invitations", request),
1918
getOrgMembers: (request: getOrgMembers.Request): Promise<getOrgMembers.Response> =>
2019
typedFetch<getOrgMembers.Response>("/api/get-org-members", request),
21-
getHomepageImages: (request: getHomepageImageUrl.Request) =>
22-
typedFetch<getHomepageImageUrl.Response>("/api/homepage-images/get", request),
2320
getDocsUrlOwner: (request: getDocsUrlOwner.Request) =>
2421
typedFetch<getDocsUrlOwner.Response>("/api/get-docs-url-owner", request),
2522
postCreatePr: (request: postCreatePr.Request) =>

packages/fern-dashboard/src/components/docs-page/DocsSiteOverviewCard.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { Skeleton } from "../ui/skeleton";
99
import { DocsSiteAttribute } from "./DocsSiteAttribute";
1010
import { DocsSiteLink } from "./DocsSiteLink";
1111
import { DownloadFernDocsButton } from "./DownloadFernDocsButton";
12-
import { DocsSiteImage } from "./docs-site-image/DocsSiteImage";
12+
import { DocsSiteImageServer } from "./docs-site-image/DocsSiteImageServer";
13+
import { SkeletonDocsSiteImage } from "./docs-site-image/SkeletonDocsSiteImage";
1314
import { FernCliVersion } from "./FernCliVersion";
1415
import { GithubSource } from "./GithubSource";
1516
import { PublishToGitHubButton } from "./PublishToGitHubButton";
@@ -26,7 +27,9 @@ export async function DocsSiteOverviewCard({
2627
return (
2728
<div className="flex w-full flex-col gap-4">
2829
<Card className="flex flex-col md:flex-row">
29-
<DocsSiteImage docsSite={docsSite} />
30+
<Suspense fallback={<SkeletonDocsSiteImage />}>
31+
<DocsSiteImageServer docsSite={docsSite} />
32+
</Suspense>
3033
<div className="flex min-w-0 flex-col gap-4 text-gray-900">
3134
<div className="flex flex-col gap-2">
3235
<p>Domains</p>

packages/fern-dashboard/src/components/docs-page/docs-site-image/DocsSiteImage.tsx

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import "server-only";
2+
3+
import type { FdrAPI } from "@fern-api/fdr-sdk/client/types";
4+
import ExclamationCircleIcon from "@heroicons/react/24/outline/ExclamationCircleIcon";
5+
6+
import { getHomepageImageUrl } from "@/app/services/dal/homepage-images/getHomepageImageUrl";
7+
import { convertFdrDocsSiteUrlToDocsUrl } from "@/utils/getDocsSiteUrl";
8+
9+
import { DocsSiteImageLayout } from "./DocsSiteImageLayout";
10+
import { SkeletonDocsSiteImage } from "./SkeletonDocsSiteImage";
11+
12+
export declare namespace DocsSiteImageServer {
13+
export interface Props {
14+
docsSite: FdrAPI.dashboard.DocsSite;
15+
}
16+
}
17+
18+
export async function DocsSiteImageServer({ docsSite }: DocsSiteImageServer.Props) {
19+
const docsUrls = docsSite.urls.map(convertFdrDocsSiteUrlToDocsUrl);
20+
21+
let lightImageUrl: string | null = null;
22+
let darkImageUrl: string | null = null;
23+
let hasError = false;
24+
25+
for (const url of docsUrls) {
26+
const [lightResult, darkResult] = await Promise.allSettled([
27+
getHomepageImageUrl({ url, theme: "light" }),
28+
getHomepageImageUrl({ url, theme: "dark" })
29+
]);
30+
31+
if (lightResult.status === "fulfilled" && lightResult.value?.imageUrl) {
32+
lightImageUrl = lightResult.value.imageUrl;
33+
} else if (lightResult.status === "rejected") {
34+
console.warn(`Failed to get light theme homepage image for ${url}`, lightResult.reason);
35+
hasError = true;
36+
}
37+
38+
if (darkResult.status === "fulfilled" && darkResult.value?.imageUrl) {
39+
darkImageUrl = darkResult.value.imageUrl;
40+
} else if (darkResult.status === "rejected") {
41+
console.warn(`Failed to get dark theme homepage image for ${url}`, darkResult.reason);
42+
hasError = true;
43+
}
44+
45+
if (lightImageUrl || darkImageUrl) {
46+
break;
47+
}
48+
}
49+
50+
if (hasError && !lightImageUrl && !darkImageUrl) {
51+
return (
52+
<DocsSiteImageLayout>
53+
<div className="flex flex-1 flex-col items-center justify-center gap-2 bg-white text-gray-900 dark:bg-black">
54+
<ExclamationCircleIcon className="size-10" />
55+
<div>Failed to load</div>
56+
</div>
57+
</DocsSiteImageLayout>
58+
);
59+
}
60+
61+
if (!lightImageUrl && !darkImageUrl) {
62+
return <SkeletonDocsSiteImage />;
63+
}
64+
65+
return (
66+
<DocsSiteImageLayout docsUrl={docsSite.urls[0]}>
67+
<>
68+
{lightImageUrl && darkImageUrl ? (
69+
<>
70+
{/*eslint-disable-next-line @next/next/no-img-element */}
71+
<img
72+
src={lightImageUrl}
73+
alt="docs homepage"
74+
className="flex-1 object-cover object-top block dark:hidden"
75+
/>
76+
{/*eslint-disable-next-line @next/next/no-img-element */}
77+
<img
78+
src={darkImageUrl}
79+
alt="docs homepage"
80+
className="flex-1 object-cover object-top hidden dark:block"
81+
/>
82+
</>
83+
) : (
84+
<>
85+
{/*eslint-disable-next-line @next/next/no-img-element */}
86+
<img
87+
src={lightImageUrl || darkImageUrl || ""}
88+
alt="docs homepage"
89+
className="flex-1 object-cover object-top"
90+
/>
91+
</>
92+
)}
93+
</>
94+
</DocsSiteImageLayout>
95+
);
96+
}

packages/fern-dashboard/src/state/queryKeys.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { getDocsUrlOwner } from "@/app/api/get-docs-url-owner/route";
22
import type { getMyOrganizations } from "@/app/api/get-my-organizations/route";
33
import type { getOrgMembers } from "@/app/api/get-org-members/route";
4-
import type { getHomepageImageUrl } from "@/app/api/homepage-images/get/route";
5-
import type { Theme } from "@/app/api/homepage-images/types";
64
import type { Auth0OrgName } from "@/app/services/auth0/types";
75
import type { GithubSourceRepo } from "@/app/services/github/types";
86
import type { DocsUrl } from "@/utils/types";
@@ -15,8 +13,6 @@ export const ReactQueryKey = {
1513
orgInvitations: (orgName: Auth0OrgName) => queryKey<OrgInvitation[]>("org-invitations", orgName),
1614
orgMembers: (orgName: Auth0OrgName) => queryKey<getOrgMembers.Response>("org-members", orgName),
1715
myOrganizations: () => queryKey<getMyOrganizations.Response>("my-orgs"),
18-
homepageImageUrl: ({ orgName, docsUrls, theme }: { orgName: Auth0OrgName; docsUrls: DocsUrl[]; theme: Theme }) =>
19-
queryKey<getHomepageImageUrl.Response>("homepage-image-url", orgName, ...docsUrls, theme),
2016
docsUrlOwner: (docsUrl: DocsUrl) => queryKey<getDocsUrlOwner.Response>("docs-url-owner", docsUrl),
2117
orgSvgLogo: (svgUrl: string) => queryKey<string>("org-svg", svgUrl),
2218
githubSourceRepo: (githubUrl: string) => queryKey<GithubSourceRepo>("github-source-repo", githubUrl)

packages/fern-dashboard/src/state/useHomepageImageUrl.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)