Skip to content

Commit c60282f

Browse files
authored
Merge pull request #164 from rush-db/feature/update-seo
SEO Optimizations
2 parents ccba563 + f0a395c commit c60282f

File tree

18 files changed

+235
-125
lines changed

18 files changed

+235
-125
lines changed

.changeset/wet-lies-jump.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'rushdb-dashboard': patch
3+
'rushdb-website': patch
4+
'rushdb-docs': patch
5+
'@rushdb/javascript-sdk': patch
6+
'rushdb-core': patch
7+
---
8+
9+
SEO Optimizations

platform/dashboard/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"react-day-picker": "^8.8.2",
4848
"react-dom": "18.2.0",
4949
"react-force-graph": "^1.45.4",
50+
"react-helmet-async": "^2.0.5",
5051
"react-hook-form": "~7.41.0",
5152
"tailwind-merge": "^2.5.2",
5253
"tailwindcss-animate": "^1.0.7",

platform/dashboard/src/layout/AuthLayout/index.tsx

+43-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, type ReactNode } from 'react'
1+
import React, { useState, type ReactNode } from 'react'
22

33
import { Logo } from '~/elements/Logo'
44
import { cn } from '~/lib/utils'
@@ -8,19 +8,50 @@ import { motion } from 'framer-motion'
88
import createVideo from '~/assets/videos/create.mp4'
99
import deleteVideo from '~/assets/videos/delete.mp4'
1010
import searchVideo from '~/assets/videos/search.mp4'
11+
import { Helmet } from 'react-helmet-async'
1112

1213
const videos = [searchVideo, createVideo, deleteVideo]
1314

15+
export const signupTitle = 'Sign Up – RushDB'
16+
export const signupDescription =
17+
'Create your RushDB account. Set up a graph database in seconds—no config, no hassle. Perfect for AI, SaaS, and ML projects.'
18+
19+
export const signinTitle = 'Sign In – RushDB'
20+
export const signinDescription =
21+
'Access your RushDB dashboard. Sign in to manage your graph-powered projects, run queries, and build AI-ready apps instantly.'
22+
23+
export const restoreTitle = 'Reset Password – RushDB'
24+
export const restoreDescription =
25+
'Forgot your password? Reset it securely and get back to building with RushDB — your zero-config graph database for AI and modern apps.'
26+
27+
export const notFoundTitle = 'Page Not Found – RushDB'
28+
export const notFoundDescription =
29+
'This page doesn’t exist or has been moved. Head back to RushDB to explore the zero-config graph database for AI and modern apps.'
30+
31+
export const defaultTitle = 'RushDB – Instant Graph Database for AI & Modern Apps'
32+
export const defaultDescription =
33+
'RushDB is a zero-config, graph-powered database built for AI, SaaS, and ML. Fast queries, seamless scaling, no setup. Try it now!'
34+
35+
const metadataMap = {
36+
signin: { title: signinTitle, description: signinDescription },
37+
signup: { title: signupTitle, description: signupDescription },
38+
recover: { title: restoreTitle, description: restoreDescription },
39+
'404': { title: notFoundTitle, description: notFoundDescription }
40+
}
41+
1442
export function AuthLayout({
1543
children,
1644
className,
1745
title,
46+
type,
1847
...props
19-
}: TPolymorphicComponentProps<'div', { title?: ReactNode }>) {
48+
}: TPolymorphicComponentProps<'div', { title?: ReactNode; type: 'signin' | 'signup' | 'recover' | '404' }>) {
2049
const [videoIdx, setVideoIdx] = useState(0)
2150

2251
const videoSrc = videos[videoIdx]
2352

53+
const meta = metadataMap[type] ?? { title: defaultTitle, description: defaultDescription }
54+
2455
return (
2556
<div
2657
className={cn(
@@ -29,6 +60,11 @@ export function AuthLayout({
2960
)}
3061
{...props}
3162
>
63+
<Helmet>
64+
<title>{meta.title}</title>
65+
<meta name="description" content={meta.description} />
66+
</Helmet>
67+
3268
<motion.video
3369
src={videoSrc}
3470
autoPlay
@@ -39,19 +75,15 @@ export function AuthLayout({
3975
initial={{ opacity: 0 }}
4076
animate={{ opacity: 0.2 }}
4177
transition={{ duration: 0.5 }}
42-
onEnded={() =>
43-
setVideoIdx((currentIdx) => (currentIdx + 1) % videos.length)
44-
}
78+
onEnded={() => setVideoIdx((currentIdx) => (currentIdx + 1) % videos.length)}
4579
/>
4680

47-
<main className="relative z-10 flex w-full max-w-xl flex-col items-stretch gap-5 sm:rounded-md sm:border sm:bg-fill/60 sm:p-5 sm:backdrop-blur-sm">
81+
<main className="sm:bg-fill/60 relative z-10 flex w-full max-w-xl flex-col items-stretch gap-5 sm:rounded-md sm:border sm:p-5 sm:backdrop-blur-sm">
4882
<Logo className="mx-auto" />
4983

50-
{title ? (
51-
<h1 className="mb-5 text-center text-2xl font-bold leading-tight tracking-tight">
52-
{title}
53-
</h1>
54-
) : null}
84+
{title ?
85+
<h1 className="mb-5 text-center text-2xl font-bold leading-tight tracking-tight">{title}</h1>
86+
: null}
5587

5688
{children}
5789
</main>

platform/dashboard/src/main.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import 'virtual:svg-icons-register'
44

55
import { App } from './App'
66
import './index.css'
7+
import { HelmetProvider } from 'react-helmet-async'
78

89
if (import.meta.env.NODE_ENV === 'development') {
910
import('~/lib/logger')
1011
}
1112

1213
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
1314
<React.StrictMode>
14-
<App />
15+
<HelmetProvider>
16+
<App />
17+
</HelmetProvider>
1518
</React.StrictMode>
1619
)

platform/dashboard/src/pages/404.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,11 @@ import { getRoutePath } from '~/lib/router'
44

55
export function NotFoundPage() {
66
return (
7-
<AuthLayout>
7+
<AuthLayout type="404">
88
<div className="grid h-full place-content-center place-items-center gap-2">
99
<h1 className="text-3xl font-bold">404</h1>
1010

11-
<Button
12-
as="a"
13-
href={getRoutePath('home')}
14-
size="large"
15-
variant="primary"
16-
>
11+
<Button as="a" href={getRoutePath('home')} size="large" variant="primary">
1712
Return to Sign In page
1813
</Button>
1914
</div>

platform/dashboard/src/pages/forgot-password.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function PasswordRecoveryPage() {
137137
const { token } = searchParams
138138

139139
return (
140-
<AuthLayout title={'Recover your RushDB account'}>
140+
<AuthLayout title={'Recover your RushDB account'} type="recover">
141141
{token ?
142142
<ChangePasswordForm token={token} />
143143
: <SendPasswordForm />}

platform/dashboard/src/pages/signin.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function SignInPage() {
9494

9595
if (platformSettings.loading) {
9696
return (
97-
<AuthLayout title={'Sign in to RushDB'}>
97+
<AuthLayout title={'Sign in to RushDB'} type="signin">
9898
<div className="m-auto flex content-center items-center justify-between">
9999
<Spinner />
100100
</div>
@@ -103,7 +103,7 @@ export function SignInPage() {
103103
}
104104

105105
return (
106-
<AuthLayout title={'Sign in to RushDB'}>
106+
<AuthLayout title={'Sign in to RushDB'} type="signin">
107107
{hasOauth ?
108108
<>
109109
<div className="flex w-full justify-between gap-2">

platform/dashboard/src/pages/signup.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function SignUpPage() {
7878

7979
if (platformSettings.loading) {
8080
return (
81-
<AuthLayout title={'Sign in to RushDB'}>
81+
<AuthLayout title={'Sign in to RushDB'} type="signin">
8282
<div className="m-auto flex content-center items-center justify-between">
8383
<Spinner />
8484
</div>
@@ -87,7 +87,7 @@ export function SignUpPage() {
8787
}
8888

8989
return (
90-
<AuthLayout title={'Create new RushDB account'}>
90+
<AuthLayout title={'Create new RushDB account'} type="signup">
9191
{hasOauth ?
9292
<>
9393
<div className="flex w-full justify-between gap-2">

pnpm-lock.yaml

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/src/components/CodeBlock.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CSSProperties, PropsWithoutRef, forwardRef, ReactNode, Children } from 'react'
1+
import { CSSProperties, PropsWithoutRef, forwardRef, ReactNode, Children, useState, useEffect } from 'react'
22
import { materialDark as codeTheme } from 'react-syntax-highlighter/dist/cjs/styles/prism'
33
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
44
import cx from 'classnames'
@@ -56,6 +56,17 @@ export const CodeBlock = forwardRef<
5656
},
5757
ref
5858
) => {
59+
const [isClient, setIsClient] = useState(false)
60+
61+
useEffect(() => {
62+
// Ensures this runs only in the browser
63+
setIsClient(true)
64+
}, [])
65+
66+
if (!isClient) {
67+
return <div className="text-content3 text-sm italic">Loading code snippet...</div>
68+
}
69+
5970
return (
6071
<div className={cx('sm:text-[14px]', className)} ref={ref} style={style}>
6172
<SyntaxHighlighter

website/src/components/Meta.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import Head from 'next/head'
22
import { useRouter } from 'next/router'
33
import { getAbsoluteURL } from '~/utils'
4+
import { metaThemeColor } from '~/config/theme'
45

56
export const defaultTitle = 'RushDB – Instant Graph Database for AI & Modern Apps'
67

78
export const defaultDescription =
89
'RushDB is a zero-config, graph-powered database built for AI, SaaS, and ML. Fast queries, seamless scaling, no setup. Try it now!'
910

11+
export const defaultKeywords =
12+
'RushDB, Firebase alternative, Supabase alternative, Neo4j, AI database, graph database, vector database, knowledge graph, NoSQL database, scalable database, cloud database, open source database, serverless database, AI embeddings, vector search, time-series database, backend as a service, REST API, data-intensive apps, developer-friendly database'
13+
1014
export const Meta = ({
1115
title = defaultTitle,
1216
description = defaultDescription,
@@ -24,11 +28,13 @@ export const Meta = ({
2428
<Head>
2529
<meta name="viewport" content="width=device-width, initial-scale=1" />
2630
<meta charSet="utf-8" />
27-
<meta name="theme-color" content="hsl(223.81, 0%, 13.34%)" />
31+
<meta name="theme-color" content={metaThemeColor} />
32+
<link rel="icon" href="/favicon.ico" />
2833

2934
{/* HTML Meta Tags */}
3035
<title>{titleWithSuffix}</title>
3136
<meta name="description" content={description} />
37+
<meta name="keywords" content={defaultKeywords} />
3238

3339
{/*Facebook Meta Tags */}
3440
<meta property="og:url" content={getAbsoluteURL(router.asPath)} />

website/src/pages/_app.tsx

+43-30
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import '~/styles/globals.css'
22
import type { AppProps } from 'next/app'
33

44
import { jetBrainsMono, manrope } from '~/styles/fonts'
5-
import React, { useRef } from 'react'
5+
import React, { useMemo } from 'react'
66
import cx from 'classnames'
77
import { CodingLanguageProvider } from '~/contexts/CodingLanguage'
88
import Script from 'next/script'
99
import { generateJsonLd } from '~/utils/jsonLd'
1010
import { useRouter } from 'next/router'
1111
import { getAbsoluteURL } from '~/utils'
1212
import Head from 'next/head'
13+
import { metaThemeColor } from '~/config/theme'
1314

1415
const isProd = process.env.NODE_ENV === 'production'
1516

@@ -19,42 +20,54 @@ gtag('js', new Date());
1920
gtag('config', 'G-Y678D4CC1J');`
2021

2122
export default function App({ Component, pageProps }: AppProps) {
22-
const ref = useRef<HTMLElement>(null)
23-
2423
const router = useRouter()
25-
2624
const route = router.asPath
2725

28-
let jsonLdData = [
29-
generateJsonLd('breadcrumb', {
30-
items: [
31-
{ name: 'Home', url: 'https://rushdb.com/' },
32-
...route
33-
.split('/')
34-
.filter((p) => p)
35-
.map((p, index, arr) => ({
36-
name: p.charAt(0).toUpperCase() + p.slice(1).replace('-', ' '),
37-
url: getAbsoluteURL(`/${arr.slice(0, index + 1).join('/')}`)
38-
}))
39-
]
40-
})
41-
]
42-
if (route === '/') {
43-
jsonLdData.push(generateJsonLd('homepage', {}))
44-
} else if (route === '/blog') {
45-
jsonLdData.push(generateJsonLd('blogRoot', {}))
46-
} else if (route === '/pricing') {
47-
jsonLdData.push(generateJsonLd('pricing', {}))
48-
} else if (route === '/privacy-policy') {
49-
jsonLdData.push(generateJsonLd('legal', { url: getAbsoluteURL(route), name: 'Privacy Policy' }))
50-
} else if (route === '/terms-of-service') {
51-
jsonLdData.push(generateJsonLd('legal', { url: getAbsoluteURL(route), name: 'Terms of Service' }))
52-
}
26+
const jsonLdData = useMemo(() => {
27+
const items = route
28+
.split('/')
29+
.filter(Boolean)
30+
.map((p, index, arr) => ({
31+
name: p.charAt(0).toUpperCase() + p.slice(1).replace('-', ' '),
32+
url: getAbsoluteURL(`/${arr.slice(0, index + 1).join('/')}`)
33+
}))
34+
35+
const data = [
36+
generateJsonLd('breadcrumb', { items: [{ name: 'Home', url: 'https://rushdb.com/' }, ...items] })
37+
]
38+
39+
if (route === '/') {
40+
data.push(generateJsonLd('homepage', {}))
41+
} else if (route === '/blog') {
42+
data.push(generateJsonLd('blogRoot', {}))
43+
} else if (route === '/pricing') {
44+
data.push(generateJsonLd('pricing', {}))
45+
} else if (route === '/privacy-policy') {
46+
data.push(generateJsonLd('legal', { url: getAbsoluteURL(route), name: 'Privacy Policy' }))
47+
} else if (route === '/terms-of-service') {
48+
data.push(generateJsonLd('legal', { url: getAbsoluteURL(route), name: 'Terms of Service' }))
49+
}
50+
51+
return data
52+
}, [route])
5353

5454
return (
5555
<>
5656
<Head>
57+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
58+
<link rel="icon" type="image/png" href="/favicon.png" />
59+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
60+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
61+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
62+
<link rel="manifest" href="/site.webmanifest" />
63+
<meta name="msapplication-TileColor" content={metaThemeColor} />
64+
<meta name="theme-color" content={metaThemeColor} />
65+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
66+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
67+
5768
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }} />
69+
70+
<meta name="referrer" content="strict-origin-when-cross-origin" />
5871
</Head>
5972
<style jsx global>{`
6073
html,
@@ -71,7 +84,7 @@ export default function App({ Component, pageProps }: AppProps) {
7184
</>
7285
)}
7386

74-
<main className={cx(jetBrainsMono.variable, manrope.variable)} ref={ref}>
87+
<main className={cx(jetBrainsMono.variable, manrope.variable)}>
7588
<CodingLanguageProvider>
7689
<Component {...pageProps} />
7790
</CodingLanguageProvider>

0 commit comments

Comments
 (0)