Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@tanstack/react-router-ssr-query": "^1.132.31",
"@tanstack/react-start": "^1.132.32",
"@tanstack/router-plugin": "^1.132.31",
"@vercel/og": "^0.8.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand All @@ -61,7 +62,7 @@
"lucide-react": "^0.544.0",
"motion": "^12.23.22",
"next-themes": "^0.4.6",
"node-appwrite": "^20.2.1",
"node-appwrite": "^21.1.0",
"react": "^19.1.1",
"react-day-picker": "^9.11.0",
"react-dom": "^19.1.1",
Expand Down
Binary file added public/default-og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions src/lib/og-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export type OGImageConfig = {
isCustom: boolean
title?: string
description?: string
image?: string
icon?: string
width?: number
height?: number
backgroundColor?: string
titleColor?: string
descriptionColor?: string
fontSize?: {
title: number
description: number
}
borderRadius?: number
}

export type OGMetaTags = {
title: string
description: string
image: string
url: string
type?: 'website' | 'article' | 'profile'
twitterHandle?: string
}

export const defaultCustomOGConfig: OGImageConfig = {
isCustom: true,
title: 'Imagine App',
description: 'Build something real',
width: 1200,
height: 630,
backgroundColor: '#ffffff',
titleColor: '#000000',
descriptionColor: '#666666',
fontSize: {
title: 60,
description: 30,
},
borderRadius: 12,
}

export function generateOGImageUrl(
config: Partial<OGImageConfig>,
baseUrl: string,
): string {
const defaultOGUrl = `${baseUrl.replace(/\/$/, '')}/og`

if (!config.isCustom) {
return defaultOGUrl
}

const merged = { ...defaultCustomOGConfig, ...config }

const params = new URLSearchParams({
...(merged.title && { title: merged.title }),
...(merged.description && { description: merged.description }),
...(merged.image && { image: merged.image }),
...(merged.backgroundColor && { bgColor: merged.backgroundColor }),
...(merged.titleColor && { titleColor: merged.titleColor }),
...(merged.descriptionColor && {
descriptionColor: merged.descriptionColor,
}),
...(merged.fontSize?.title && {
titleSize: merged.fontSize.title.toString(),
}),
...(merged.fontSize?.description && {
descSize: merged.fontSize.description.toString(),
}),
})

return `${defaultOGUrl}?${params.toString()}`
}

export function createOGMetaTags(config: OGMetaTags) {
const {
title,
description,
image,
url,
type = 'website',
twitterHandle,
} = config

return {
meta: [
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
{ property: 'og:image', content: image },
{ property: 'og:url', content: url },
{ property: 'og:type', content: type },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: title },
{ name: 'twitter:description', content: description },
{ name: 'twitter:image', content: image },
...(twitterHandle
? [{ name: 'twitter:creator', content: twitterHandle }]
: []),
{ name: 'description', content: description },
],
}
}
21 changes: 21 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Route as AuthSignOutRouteImport } from './routes/_auth/sign-out'
import { Route as AuthSignInRouteImport } from './routes/_auth/sign-in'
import { Route as AuthResetPasswordRouteImport } from './routes/_auth/reset-password'
import { Route as AuthForgotPasswordRouteImport } from './routes/_auth/forgot-password'
import { Route as ApiOgRouteImport } from './routes/_api/og'
import { Route as ApiHelloRouteImport } from './routes/_api/hello'

const PublicRoute = PublicRouteImport.update({
Expand Down Expand Up @@ -69,6 +70,11 @@ const AuthForgotPasswordRoute = AuthForgotPasswordRouteImport.update({
path: '/forgot-password',
getParentRoute: () => AuthRoute,
} as any)
const ApiOgRoute = ApiOgRouteImport.update({
id: '/_api/og',
path: '/og',
getParentRoute: () => rootRouteImport,
} as any)
const ApiHelloRoute = ApiHelloRouteImport.update({
id: '/_api/hello',
path: '/hello',
Expand All @@ -77,6 +83,7 @@ const ApiHelloRoute = ApiHelloRouteImport.update({

export interface FileRoutesByFullPath {
'/hello': typeof ApiHelloRoute
'/og': typeof ApiOgRoute
'/forgot-password': typeof AuthForgotPasswordRoute
'/reset-password': typeof AuthResetPasswordRoute
'/sign-in': typeof AuthSignInRoute
Expand All @@ -87,6 +94,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/hello': typeof ApiHelloRoute
'/og': typeof ApiOgRoute
'/forgot-password': typeof AuthForgotPasswordRoute
'/reset-password': typeof AuthResetPasswordRoute
'/sign-in': typeof AuthSignInRoute
Expand All @@ -101,6 +109,7 @@ export interface FileRoutesById {
'/_protected': typeof ProtectedRouteWithChildren
'/_public': typeof PublicRouteWithChildren
'/_api/hello': typeof ApiHelloRoute
'/_api/og': typeof ApiOgRoute
'/_auth/forgot-password': typeof AuthForgotPasswordRoute
'/_auth/reset-password': typeof AuthResetPasswordRoute
'/_auth/sign-in': typeof AuthSignInRoute
Expand All @@ -113,6 +122,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/hello'
| '/og'
| '/forgot-password'
| '/reset-password'
| '/sign-in'
Expand All @@ -123,6 +133,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/hello'
| '/og'
| '/forgot-password'
| '/reset-password'
| '/sign-in'
Expand All @@ -136,6 +147,7 @@ export interface FileRouteTypes {
| '/_protected'
| '/_public'
| '/_api/hello'
| '/_api/og'
| '/_auth/forgot-password'
| '/_auth/reset-password'
| '/_auth/sign-in'
Expand All @@ -150,6 +162,7 @@ export interface RootRouteChildren {
ProtectedRoute: typeof ProtectedRouteWithChildren
PublicRoute: typeof PublicRouteWithChildren
ApiHelloRoute: typeof ApiHelloRoute
ApiOgRoute: typeof ApiOgRoute
}

declare module '@tanstack/react-router' {
Expand Down Expand Up @@ -224,6 +237,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthForgotPasswordRouteImport
parentRoute: typeof AuthRoute
}
'/_api/og': {
id: '/_api/og'
path: '/og'
fullPath: '/og'
preLoaderRoute: typeof ApiOgRouteImport
parentRoute: typeof rootRouteImport
}
'/_api/hello': {
id: '/_api/hello'
path: '/hello'
Expand Down Expand Up @@ -280,6 +300,7 @@ const rootRouteChildren: RootRouteChildren = {
ProtectedRoute: ProtectedRouteWithChildren,
PublicRoute: PublicRouteWithChildren,
ApiHelloRoute: ApiHelloRoute,
ApiOgRoute: ApiOgRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
74 changes: 53 additions & 21 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import type { QueryClient } from '@tanstack/react-query'
import { Toaster } from '@/components/ui/sonner'
import { ThemeProvider } from 'next-themes'
import { authMiddleware } from '@/server/functions/auth'
import { getBaseUrl } from '@/server/functions/request'
import {
createOGMetaTags,
generateOGImageUrl,
OGImageConfig,
OGMetaTags,
} from '@/lib/og-config'

interface MyRouterContext {
queryClient: QueryClient
Expand All @@ -29,32 +36,57 @@ if (import.meta.env.VITE_INSTRUMENTATION_SCRIPT_SRC) {
export const Route = createRootRouteWithContext<MyRouterContext>()({
loader: async () => {
const { currentUser } = await authMiddleware()
const baseUrl = await getBaseUrl()

return {
currentUser,
baseUrl,
}
},
head: ({ loaderData }) => {
const baseUrl =
typeof window !== 'undefined'
? window.location.origin
: (loaderData?.baseUrl ?? 'https://imagine.dev')

const config: OGImageConfig = {
isCustom: false,
}

const ogImageUrl = generateOGImageUrl(config, baseUrl)

const metadata: OGMetaTags = {
title: 'Imagine App',
description: 'Build something real',
image: ogImageUrl,
url: typeof window !== 'undefined' ? window.location.href : baseUrl,
}

const ogTags = createOGMetaTags(metadata)

return {
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Imagine App',
},
...ogTags.meta,
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
scripts: [...scripts],
}
},
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Imagine App',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
scripts: [...scripts],
}),

shellComponent: RootDocument,
})
Expand Down
Loading
Loading