Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render Contentful assets #279

Merged
merged 35 commits into from
Aug 2, 2023

Conversation

Benaiah
Copy link
Contributor

@Benaiah Benaiah commented Jul 4, 2023

Jira ticket: LDAF-248

Proposed changes

  • Renders Contentful rich text images with the Image component
  • Adds an /api/v1/blurhash route that creates blurhashes on-demand from Contentful images and caches them in Redis

Screenshots

image

Acceptance criteria validation

  • Manually tested

Other details

Alternate solutions

We considered using Imgix for blurhashes, and we could still easily switch to that, but the API endpoint included here was a small lift and means we don't have to worry about the number of images we have or go thru procurement for anything.

@vercel
Copy link

vercel bot commented Jul 4, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ldaf ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 2, 2023 7:36pm

: createConnectedRedisClient({ url: KV_URL, useTLS: !KV_URL.startsWith("redis://localhost") });

// So we don't get unhandled promise rejection warnings if nothing awaits the promise
redisClientPromise.catch((err) => console.error(err));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If #274 is merged first, this will need to be updated to use the logger added in that PR.

if (!item?.sys?.id || !item?.url || !item?.contentType?.startsWith("image/")) return [];
const blurhashResponse = await fetch(`/api/v1/blurhash/${encodeURIComponent(item.url)}`);
if (!blurhashResponse.ok) return [];
return [item.sys.id, await blurhashResponse.text()];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return [item.sys.id, await blurhashResponse.text()];
return [[item.sys.id, await blurhashResponse.text()]];

Comment on lines 51 to 52
const errorMessage = getErrorMessage(body);
if (errorMessage) return errorMessage;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getErrorMessage always returns a value so getDataMessage is not being used right now.

Copy link
Contributor

@hinzed1127 hinzed1127 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I just have one small update around error messaging, but otherwise, LGTM

Copy link
Member

@LouisFettet LouisFettet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as well, just a few small notes and questions. Maybe just give us a heads-up after getting the latest updates from main so that we can take one more quick look before merging this.

@LouisFettet
Copy link
Member

@Benaiah the /test-contentful-content page looks good but unfortunately it looks like you'll need to make some updates to the Core Content pages in order for them to show up. Currently I'm getting a 500 on /animals/branding with error no context was provided for asset hyperlink

@Benaiah Benaiah force-pushed the benaiah/render-contentful-references-and-assets branch from e7e0039 to 2b38b88 Compare August 1, 2023 02:24
@LouisFettet
Copy link
Member

Giving this a look now; things seemed rather slow on first run-through but then I changed some redis-cli settings to increase memory and buffer limits and everything seemed much smoother (although it's possible that this performance increase was due to having already requested the images on prior run).

I'm still seeing the SocketClosedUnexpectedlyError after about 5 minutes, so I'm going to continue looking into that.

I think the only blocker might be that the image on this page seems to run into an issue where it fails to load, although I haven't looked into why yet: https://ldaf-git-benaiah-render-contentful-references-and-assets-ldaf.vercel.app/plants/pests

let redisClientError: undefined | unknown;
const redisClient = createRedisClient<None, None, None>({
url,
socket: { tls: useTLS },
Copy link
Member

@LouisFettet LouisFettet Aug 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may be able to fix the socket connection error with the pingInterval config, although I'm not sure if this could have side effects outside of local development (so we may only want to add this config for local dev). After making this change locally, I didn't see the error pop up after running dev for over 10 minutes (usually I saw the error pop up around the 5 minute mark).

Suggested change
socket: { tls: useTLS },
socket: { tls: useTLS },
// ping every 5 minutes to avoid a socket connection error in local development
pingInterval: 5 * 60 * 1000,
});

Found the suggestion in this issue comment, and the docs for the createClient configs are here.

@Benaiah @hinzed1127 let me know if this solves the issue for y'all locally!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seemed to make it last longer (maybe 10 minutes?), but I was still having things crash after a period of time

Copy link
Member

@LouisFettet LouisFettet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a question and a minor comment, but otherwise this LGTM! Only blocker is figuring out why we're getting an error on the image on the /plants/pests page.


export const handle = (async ({ event, resolve }) => {
const handleSetupRedisClient = (async ({ event, resolve }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about how this works. Server hooks get run when the app starts and then on every request right? Are we spinning up a new Redis client for every request? Is it possible for us to check whether event.locals.getKVClient has already been set so that we don't create a new client or reset it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running on Vercel (as opposed to local development) there's no real distinction between "when the app starts" and "when it handles a request" because we're using serverless functions, so the app starts and runs to completion for each request. We can't persist a client between different invocations of a serverless function.

Comment on lines 192 to 193
const pageMetadata = pageMetadataMap.get(metadataID);
if (!pageMetadata) break fetchData;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to break here then this should probably be run before we request the data right?

Copy link
Contributor

@hinzed1127 hinzed1127 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of small comments, but this looks ready to go

Comment on lines +134 to +168
type ServiceGroup = ExtractQueryType<
ServiceGroupCollectionQuery,
["serviceGroupCollection", "items", number]
>;

if (matchedServiceGroup) {
const serviceEntries = matchedServiceGroup.serviceEntriesCollection?.items.filter(
(item) => item?.__typename === "ServiceEntry"
);
let serviceGroups = matchedServiceGroup.serviceEntriesCollection?.items.filter(
(item) => item?.__typename === "ServiceGroup"
) as ServiceGroup[];
type ServiceGroupMetadata = ExtractQueryType<ServiceGroup, ["pageMetadata"]>;

serviceGroups = serviceGroups.map((serviceGroup) => {
const serviceGroupMetadata = pageMetadataMap.get(
serviceGroup?.pageMetadata?.sys.id || ""
);
return { ...serviceGroup, url: serviceGroupMetadata?.url };
});
type ChildServiceEntryOrGroup = ExtractQueryType<
ServiceGroup,
["serviceEntriesCollection", "items", number]
>;

return {
...matchedServiceGroup,
pageMetadata: matchedPageMetadata,
serviceEntries,
serviceGroups,
} as ServiceGroupPage;
}
} else {
console.warn(
`A Service Group entry with the slug "${slug}" could not be found. If this page was reached via a link, it is likely that the Page Metadata entry is published but the Service Group entry is not.`
);
}
type ChildServiceEntry = Extract<ChildServiceEntryOrGroup, { __typename: "ServiceEntry" }>;

type ChildServiceGroup = Extract<ChildServiceEntryOrGroup, { __typename: "ServiceGroup" }>;

export type ServiceGroupPage = {
serviceGroup: ServiceGroup & {
heroImage?: ServiceGroup["heroImage"] & {
imageSource?: NonNullable<ServiceGroup["heroImage"]>["imageSource"] & {
blurhash?: string | null | undefined;
};
};
description?: ServiceGroup["description"] & {
blurhashes?: Record<string, string> | null | undefined;
};
};
childServiceEntries: (ChildServiceEntry & {
description?: ChildServiceEntry["description"] & {
blurhashes?: Record<string, string> | null | undefined;
};
})[];
childServiceGroups: (ChildServiceGroup & { url?: string | null | undefined })[];
pageMetadata?: ServiceGroupMetadata;
};
Copy link
Contributor

@hinzed1127 hinzed1127 Aug 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to export all these types from a src/routes/(infoPages)/[topTierPage]/[...serviceGroupPage]/types.ts file and import them here?

}
serviceEntriesCollection(limit: 10) {
serviceEntriesCollection(limit: 5) {
Copy link
Contributor

@hinzed1127 hinzed1127 Aug 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have been hitting query complexity limits, but this'll be too small for some service entries. E.g. /animals/meat-poultry currently has 8 service entries, so we'll probably want at least that. Ideally figure out with UX what the ceiling on number of service entries might be (10? higher?)

@github-actions
Copy link

github-actions bot commented Aug 2, 2023

Coverage after merging benaiah/render-contentful-references-and-assets into main will be

96.32%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/lib/components/Accordion
   Accordion.svelte100%100%100%100%
   AccordionItem.svelte95.65%66.67%100%100%21, 35
   index.ts100%100%100%100%
src/lib/components/Alert
   Alert.svelte94.74%57.14%100%100%34, 41, 41
src/lib/components/AnnouncementBanner
   AnnouncementBanner.svelte100%100%100%100%
src/lib/components/Breadcrumbs
   Breadcrumbs.svelte100%100%100%100%
src/lib/components/Button
   Button.svelte98.04%50%100%100%41
   buttonOptions.ts100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Card
   Card.svelte97.50%0%100%100%7
   index.ts100%100%100%100%
src/lib/components/ContactCard
   ContactCard.svelte100%100%100%100%
   Section.svelte100%100%100%100%
src/lib/components/ContentfulRichText
   ContentfulRichText.svelte97.56%50%100%100%20
   context.ts96.67%75%100%100%7
   headings.ts93.18%50%100%95.12%37–39
   predicates.ts96.19%100%88.24%97.26%65, 73
src/lib/components/ContentfulRichText/nodes
   AssetHyperlink.svelte100%100%100%100%
   Blockquote.svelte100%100%100%100%
   EmbeddedAssetBlock.svelte87.72%0%100%100%13, 17, 21, 30, 37–39
   Heading1.svelte100%100%100%100%
   Heading2.svelte100%100%100%100%
   Heading3.svelte100%100%100%100%
   Heading4.svelte100%100%100%100%
   Heading5.svelte100%100%100%100%
   Heading6.svelte100%100%100%100%
   Hr.svelte100%100%100%100%
   Hyperlink.svelte100%100%100%100%
   ListItem.svelte100%100%100%100%
   Node.svelte100%100%100%100%
   OrderedList.svelte100%100%100%100%
   Paragraph.svelte100%100%100%100%
   Table.svelte100%100%100%100%
   TableCell.svelte100%100%100%100%
   TableHeaderCell.svelte100%100%100%100%
   TableRow.svelte100%100%100%100%
   Text.svelte100%100%100%100%
   UnorderedList.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/CopyToClipboard
   CopyToClipboard.svelte97.50%60%100%100%71, 71
   index.ts100%100%100%100%
src/lib/components/Header
   Header.svelte92.37%38.46%80%100%68, 68–69, 69, 72, 88, 88, 88
src/lib/components/Header/Logo
   Logo.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Header/Nav
   Nav.svelte97.06%83.33%100%100%19
   NavItem.svelte100%100%100%100%
   NavLink.svelte100%100%100%100%
   NavMenu.svelte98.25%83.33%100%100%59, 68
   index.ts100%100%100%100%
src/lib/components/Header/Title
   Title.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Icon
   Icon.svelte87.50%20%100%100%19, 19, 24, 24
   index.ts100%100%100%100%
src/lib/components/Image
   BlurhashRenderer.svelte100%100%100%100%
   Image.svelte91.54%72.55%66.67%96.33%117, 131, 133–134, 154, 158–163, 163, 183–185, 193, 193, 212–213, 28, 39, 86
   index.ts100%100%100%100%
   renderBlurhash.ts100%100%100%100%
src/lib/components/IntersectionObserver
   IntersectionObserver.svelte94.74%75%100%100%36–37, 39
   RootIntersectionObserver.svelte85.19%0%100%92%14–17
   index.ts100%100%100%100%
   key.ts100%100%100%100%
   observe.ts99.03%93.33%100%100%73
src/lib/components/Link
   Link.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Search
   Search.svelte91.43%20%100%96.88%39–41, 46, 46, 50
   index.ts100%100%100%100%
   options.ts100%100%100%100%
src/lib/components/SideNav
   SideNav.svelte100%100%100%100%
   SideNavItem.svelte100%100%100%100%
src/lib/components/VideoCard
   VideoCard.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/constants
   images.ts100%100%100%100%
   support.ts43.75%0%100%50%12–14, 4–9
src/lib/imageServices
   contentful.ts98.31%90.91%100%100%21
src/lib/services/contentful
   graphqlClient.ts90.11%66.67%100%91.46%65–72, 74
src/lib/services/youtube
   getYoutubeVideoData.ts100%100%100%100%
   index.ts100%100%100%100%
src/lib/util
   classNames.ts100%100%100%100%
   getErrorMessageFromResponse.ts11.84%100%0%12.68%12–19, 2, 20, 23–29, 3, 30–45, 48–59, 6, 60–69, 7, 70–71, 8–9
   warn.ts100%100%100%100%
src/stories
   AccordionView.svelte100%100%100%100%
   CardView.svelte99.09%50%100%100%74

@github-actions
Copy link

github-actions bot commented Aug 2, 2023

Copy link
Member

@LouisFettet LouisFettet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after a quick click-test.

I'm not seeing blurhashes when navigating between Core Content pages that both have hero images (the previous hero stays in place until the new one loads, instead of showing the blurhash first), but we can handle that in a follow-up.

@Benaiah Benaiah merged commit 334fe70 into main Aug 2, 2023
@Benaiah Benaiah deleted the benaiah/render-contentful-references-and-assets branch August 2, 2023 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants