diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..ff7ff438 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "bun", + "runtimeArgs": ["dev"], + "port": 3456, + "autoPort": true + } + ] +} diff --git a/bun.lock b/bun.lock index 7140bad9..e1ca9ea1 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "website", "dependencies": { + "@gsap/react": "^2.1.2", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^16.0.10", @@ -14,6 +14,8 @@ "@vimeo/player": "^2.26.0", "clsx": "^2.1.1", "framer-motion": "^12.23.26", + "geist": "^1.7.0", + "gsap": "^3.14.2", "hls.js": "^1.5.20", "next": "^16.0.10", "posthog-js": "1.335.3", @@ -62,6 +64,8 @@ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@gsap/react": ["@gsap/react@2.1.2", "", { "peerDependencies": { "gsap": "^3.12.5", "react": ">=17" } }, "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -360,8 +364,12 @@ "framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="], + "geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="], + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 242712d7..7d383765 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@gsap/react": "^2.1.2", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^16.0.10", @@ -9,6 +10,8 @@ "@vimeo/player": "^2.26.0", "clsx": "^2.1.1", "framer-motion": "^12.23.26", + "geist": "^1.7.0", + "gsap": "^3.14.2", "hls.js": "^1.5.20", "next": "^16.0.10", "posthog-js": "1.335.3", diff --git a/public/fonts/matter-regular.woff b/public/fonts/matter-regular.woff deleted file mode 100644 index c6d4c779..00000000 Binary files a/public/fonts/matter-regular.woff and /dev/null differ diff --git a/src/app/(rest)/blog/[slug]/opengraph-image/route.ts b/src/app/(rest)/blog/[slug]/opengraph-image/route.ts deleted file mode 100644 index 6e184973..00000000 --- a/src/app/(rest)/blog/[slug]/opengraph-image/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getBlogSocialImageResponse } from '../social-image' - -export const runtime = 'nodejs' -export const revalidate = 86400 - -type Params = { - params: Promise<{ slug: string }> -} - -export async function GET(_request: Request, { params }: Params) { - const { slug } = await params - return getBlogSocialImageResponse(slug) -} diff --git a/src/app/(rest)/blog/[slug]/page.tsx b/src/app/(rest)/blog/[slug]/page.tsx deleted file mode 100644 index ce1edbca..00000000 --- a/src/app/(rest)/blog/[slug]/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { Metadata } from 'next' -import Link from 'next/link' -import { META } from '~/lib/constants/metadata' -import { createMetadata } from '~/lib/utils/create-metadata' -import { formatDate } from '~/lib/utils/date' -import { getPost, getPostSlugs } from '~/lib/utils/posts' -import { CTA } from '~/ui/cta' -import { CustomImage } from '~/ui/custom-image' -import { NextPost } from '~/ui/next-post' -import { TableOfContents } from '~/ui/table-of-contents' - -export const dynamicParams = false - -type Props = { - params: Promise<{ slug: string }> -} - -export async function generateStaticParams() { - const slugs = await getPostSlugs() - return slugs.map(slug => ({ slug })) -} - -export async function generateMetadata({ params }: Props): Promise { - const { slug } = await params - - const { metadata } = await getPost(slug) - const socialImageAlt = metadata.title - const socialMetadataPath = `/blog/${slug}` - const baseMetadata = createMetadata({ - description: metadata.description, - pathname: socialMetadataPath, - title: `${metadata.title} | ${META.title}` - }) - - return { - ...baseMetadata, - openGraph: { - ...(baseMetadata.openGraph ?? {}), - images: [{ alt: socialImageAlt, url: `${socialMetadataPath}/opengraph-image` }] - }, - twitter: { - ...(baseMetadata.twitter ?? {}), - images: [{ alt: socialImageAlt, url: `${socialMetadataPath}/twitter-image` }] - } - } -} - -export default async function Page({ params }: Props) { - const { slug } = await params - const { Post, metadata, toc } = await getPost(slug) - - if (!Post || !metadata) return
Post not found
- - return ( -
-
-
- -
-
-

{formatDate(metadata.date)}

-

- by{' '} - - {metadata.author.name} - -

-

{metadata.category}

-
-
-
-
-

{metadata.title}

- {metadata.subtitle ?

{metadata.subtitle}

: null} -
- - -
-
- - -
-
-
-
- ) -} diff --git a/src/app/(rest)/blog/[slug]/social-image.tsx b/src/app/(rest)/blog/[slug]/social-image.tsx deleted file mode 100644 index bdeaa1d6..00000000 --- a/src/app/(rest)/blog/[slug]/social-image.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { readFile } from 'node:fs/promises' -import path from 'node:path' -import { ImageResponse } from 'next/og' -import { toJpegImageResponse } from '~/lib/utils/og-image' -import { getPost } from '~/lib/utils/posts' -import { Rubric } from '~/ui/logos/rubric' - -export const BLOG_SOCIAL_IMAGE_ALT = - 'Applied AI lab helping companies build intelligent applications' -export const BLOG_SOCIAL_IMAGE_SIZE = { - height: 630, - width: 1200 -} - -const fontDataPromise = readFile(path.join(process.cwd(), 'src/app/fonts/matter-regular.woff')) -const bannerSrcCache = new Map() - -const BlogSocialImage = ({ - title, - backgroundImageUrl -}: { - title: string - backgroundImageUrl: string -}) => { - return ( -
- {backgroundImageUrl ? ( -
- {/** biome-ignore lint/performance/noImgElement: techdebt */} - {title} -
- ) : null} -
- -
Rubric Labs Blog
-
-
-
{title}
-
-
- ) -} - -// Builds a cached data URI for banner images; input '/images/primitives.png' -> output 'data:image/png;base64,...'. -const getBannerSrc = async (bannerImageUrl: string) => { - const cached = bannerSrcCache.get(bannerImageUrl) - if (cached) return cached - - const bannerPath = bannerImageUrl.replace(/^\//, '') - const extension = path.extname(bannerPath).toLowerCase() - const mimeType = - extension === '.png' - ? 'image/png' - : extension === '.jpg' || extension === '.jpeg' - ? 'image/jpeg' - : extension === '.webp' - ? 'image/webp' - : 'image/png' - const bannerData = await readFile(path.join(process.cwd(), 'public', bannerPath), 'base64') - const bannerSrc = `data:${mimeType};base64,${bannerData}` - bannerSrcCache.set(bannerImageUrl, bannerSrc) - return bannerSrc -} - -export const getBlogSocialImageResponse = async (slug: string) => { - const [{ metadata }, localFont] = await Promise.all([getPost(slug), fontDataPromise]) - const bannerSrc = await getBannerSrc(metadata.bannerImageUrl) - const pngResponse = new ImageResponse( - , - { - ...BLOG_SOCIAL_IMAGE_SIZE, - fonts: [ - { - data: localFont, - name: 'Matter', - style: 'normal', - weight: 400 - } - ] - } - ) - - return toJpegImageResponse(pngResponse) -} diff --git a/src/app/(rest)/blog/[slug]/twitter-image/route.ts b/src/app/(rest)/blog/[slug]/twitter-image/route.ts deleted file mode 100644 index 6e184973..00000000 --- a/src/app/(rest)/blog/[slug]/twitter-image/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getBlogSocialImageResponse } from '../social-image' - -export const runtime = 'nodejs' -export const revalidate = 86400 - -type Params = { - params: Promise<{ slug: string }> -} - -export async function GET(_request: Request, { params }: Params) { - const { slug } = await params - return getBlogSocialImageResponse(slug) -} diff --git a/src/app/(rest)/blog/page.tsx b/src/app/(rest)/blog/page.tsx deleted file mode 100644 index 319f58c8..00000000 --- a/src/app/(rest)/blog/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { META } from '~/lib/constants/metadata' -import { createMetadata } from '~/lib/utils/create-metadata' -import { getPostMetadata } from '~/lib/utils/posts' -import { Card } from '~/ui/card' - -export const metadata = createMetadata({ - description: `The latest from our team. ${META.description}`, - pathname: '/blog', - title: `Blog | ${META.title}` -}) - -export default async function Page() { - const posts = await getPostMetadata() - - return ( -
-
-

Blog

-

The latest from our team

-
-
- {posts.map((post, index) => ( - - ))} -
-
- ) -} diff --git a/src/app/(rest)/contact/contact-form.tsx b/src/app/(rest)/contact/contact-form.tsx deleted file mode 100644 index 86ef3507..00000000 --- a/src/app/(rest)/contact/contact-form.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' - -import { useRef } from 'react' -import { createContactRequest } from '~/lib/actions/create-contact-request' -import { useShortcut } from '~/lib/hooks/use-shortcut' -import { Button } from '~/ui/button' -import { Form } from '~/ui/form' - -export const ContactForm = () => { - const formRef = useRef(null) - - useShortcut('cmd+enter', () => formRef.current?.requestSubmit(), { fireInForm: true }) - return ( -
- {({ pending, state }) => ( - <> -
-
- {/* biome-ignore lint/a11y/noAutofocus: techdebt */} - - - -