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

Cache loaders #40

Merged
merged 11 commits into from
Jan 2, 2025
1 change: 1 addition & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Use Remix V2 syntax
* Use TSDoc specification for docstrings
* Use TailwindCSS for styling
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ pre-commit 3.7.0
nodejs 21.7.3
pnpm 9.9.0
yarn 1.22.22
supabase-cli 2.1.1
supabase-cli 2.2.1
terraform 1.5.4
4 changes: 2 additions & 2 deletions app/components/molecules/TagSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const TagItem = ({ name, onClick, checked, isFocused }: TagItemProps) => {
);
};

interface TagOption {
export interface TagOption {
name: string;
selected: boolean;
}
Expand Down Expand Up @@ -220,7 +220,7 @@ export const TagSearchBar = ({

return (
<div
className="font-gill-sans relative mx-auto mt-4 w-full max-w-xs font-light"
className="font-gill-sans relative mx-auto mb-4 mt-4 w-full max-w-xs font-light"
ref={TagSearchBarRef}
onKeyDown={handleKeyDown}
role="combobox"
Expand Down
6 changes: 3 additions & 3 deletions app/components/organisms/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Tables } from "~/integrations/supabase/database.types";
*/
export const Carousel = ({ posts }: { posts: Tables<"posts">[] }) => {
return (
<div className="mb-2 mt-2">
<div>
{posts.length > 0 ? (
<div className="carousel carousel-vertical h-full w-full">
{posts.map((post) => (
Expand All @@ -25,10 +25,10 @@ export const Carousel = ({ posts }: { posts: Tables<"posts">[] }) => {
{post.title}
</h2>
<p className="font-gill-sans mb-1 text-sm">
Created at{" "}
Published on{" "}
{new Date(post.created_at).toLocaleDateString("en-US", {
year: "numeric",
month: "numeric",
month: "long",
day: "numeric",
})}
</p>
Expand Down
2 changes: 1 addition & 1 deletion app/components/organisms/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const Pagination = ({
const endPage = Math.min(startPage + MAX_PAGES_PER_PAGE, pagesInTotal);

return (
<div className="font-gill-sans join mt-auto flex justify-center space-x-0">
<div className="font-gill-sans join mt-4 flex justify-center space-x-0">
<button
className="btn join-item btn-sm"
onClick={() => onPageChange(currentPage - 1)}
Expand Down
3 changes: 3 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Gleb Khaykin" />
<Meta />
<Links />
</head>
Expand Down
9 changes: 0 additions & 9 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,6 @@ import { Header } from "~/components/organisms/Header";
*/
export const meta: MetaFunction = () => {
return [
{ charset: "utf-8" },
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
name: "author",
content: "Gleb Khaykin",
},
{
property: "og:title",
content: "About",
Expand Down
142 changes: 96 additions & 46 deletions app/routes/blog.$slug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {
LoaderFunctionArgs,
MetaFunction,
} from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import {
Await,
ClientLoaderFunction,
ClientLoaderFunctionArgs,
useLoaderData,
} from "@remix-run/react";
import { createClient } from "@supabase/supabase-js";
import { NotionAPI } from "notion-client";
import React, { Suspense } from "react";
Expand All @@ -19,7 +24,6 @@ import {
import { ExtendedRecordMap } from "vendor/react-notion-x/packages/notion-types/src/maps";
import { NotionRenderer } from "vendor/react-notion-x/packages/react-notion-x";

import { LoadingSpinner } from "~/components/atoms/LoadingSpinner";
import { Footer } from "~/components/organisms/Footer";
import { Header } from "~/components/organisms/Header";
import { Tables } from "~/integrations/supabase/database.types";
Expand Down Expand Up @@ -104,30 +108,74 @@ const NotionPage = ({ recordMap }: { recordMap: ExtendedRecordMap }) => {
export const loader: LoaderFunction = async ({
params,
}: LoaderFunctionArgs) => {
const slug = params.slug;
if (!slug) {
throw new Response("Slug is required", { status: 400 });
}

const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const { data: post, error } = await supabase
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("slug", slug)
.eq("slug", params.slug)
.returns<Tables<"posts">[]>()
.single();

if (error) {
throw new Response("Post not found", { status: 404 });
}
if (error) throw new Response("Failed to load post", { status: 500 });
if (!data) throw new Response("Post not found", { status: 404 });

const notion = new NotionAPI();
const recordMapPromise = notion.getPage(post.notion_page_id);
return defer({ post, recordMap: recordMapPromise });
const recordMapPromise = notion.getPage(data.notion_page_id);

return defer({ post: data, recordMap: recordMapPromise });
};

const CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day in milliseconds

export const clientLoader: ClientLoaderFunction = async ({
params,
serverLoader,
}: ClientLoaderFunctionArgs) => {
const cachedData = sessionStorage.getItem(`blogPosts-${params.slug}`);
const cachedTimestamp = sessionStorage.getItem(
`blogPostsTimestamp-${params.slug}`,
);

// Use cached data if it's valid
if (cachedData && cachedTimestamp) {
const isExpired = Date.now() - Number(cachedTimestamp) > CACHE_DURATION;
const parsedData = JSON.parse(cachedData);
if (!isExpired && parsedData.post && parsedData.recordMap) {
return {
post: parsedData.post,
recordMap: Promise.resolve(parsedData.recordMap),
};
}
}

// Get fresh data from server
const serverData = (await serverLoader()) as {
post: Tables<"posts">;
recordMap: Promise<ExtendedRecordMap>;
};

// Cache the data
Promise.resolve(serverData.recordMap).then((recordMap) => {
sessionStorage.setItem(
`blogPosts-${params.slug}`,
JSON.stringify({
post: serverData.post,
recordMap,
}),
);
sessionStorage.setItem(
`blogPostsTimestamp-${params.slug}`,
Date.now().toString(),
);
});

return serverData;
};
// Tell Remix to use the client loader during hydration
clientLoader.hydrate = true;

export const handle: SEOHandle = {
/**
Expand Down Expand Up @@ -159,54 +207,52 @@ export const handle: SEOHandle = {
* @param post - The post object
* @returns The meta tags
*/
// @ts-expect-error: Expect not assignable type (otherwise, it would be a server timeout)
export const meta: MetaFunction = ({
data,
}: {
data: { post: Tables<"posts"> };
}) => {
const { post } = data;

const dateStr = new Date(post.created_at).toLocaleDateString("en-US", {
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) {
return [
{ title: "Blog Post Not Found" },
{ description: "The requested blog post could not be found." },
];
}

const createdDate = new Date(data.post.created_at);
const formattedDate = createdDate.toLocaleDateString("en-US", {
year: "numeric",
month: "numeric",
month: "long",
day: "numeric",
});
const description = `Created at ${dateStr}`;

return [
{ charset: "utf-8" },
{
name: "author",
content: "Gleb Khaykin",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
property: "og:image",
content: post.image_url || "/img/van_gogh_wheatfield_with_crows.webp",
},
{ title: data.post.title },
{ description: `Published on ${formattedDate}` },
{
property: "og:title",
content: post.title,
content: data.post.title,
},
{
property: "og:description",
content: description,
content: `Published on ${formattedDate}`,
},
{
property: "og:type",
content: "article",
},
{
property: "og:url",
content: `https://khaykingleb.com/blog/${data.post.slug}`,
},
{
property: "og:image",
content:
data.post.image_url || "/img/van_gogh_wheatfield_with_crows.webp",
},
{
property: "article:published_time",
content: post.created_at,
content: data.post.created_at,
},
{
property: "og:url",
content: `https://khaykingleb.com/blog/${post.slug}`,
property: "article:tag",
content: Array.isArray(data.post.tags) ? data.post.tags.join(", ") : "",
},
];
};
Expand All @@ -222,9 +268,13 @@ export default function BlogPostRoute() {
return (
<div className="flex min-h-screen flex-col">
<Header backgroundImageUrl="/img/van_gogh_wheatfield_with_crows.webp" />
<main className="flex-grow px-4 sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-[750px] flex-col">
<Suspense fallback={<LoadingSpinner />}>
<main className="flex flex-grow px-4 sm:px-6 lg:px-8">
<div className="mx-auto flex w-full max-w-[750px] flex-col">
<Suspense
fallback={
<div className="mb-2 mt-4 w-full flex-grow animate-pulse rounded-lg bg-gray-200" />
}
>
<Await resolve={recordMap}>
{(resolvedRecordMap: ExtendedRecordMap) => {
return <NotionPage recordMap={resolvedRecordMap} />;
Expand Down
Loading
Loading