diff --git a/.gitignore b/.gitignore
index 7a45d903..789820ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# Dependencies
node_modules
+package-lock.json
.pnp
.pnp.js
@@ -42,3 +43,4 @@ yarn-error.log*
.DS_Store
*.pem
/.playwright-mcp
+og-test
diff --git a/apps/editor/app/editor/[projectId]/layout.tsx b/apps/editor/app/editor/[projectId]/layout.tsx
new file mode 100644
index 00000000..82f03def
--- /dev/null
+++ b/apps/editor/app/editor/[projectId]/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Editor Workspace',
+ description: 'Edit your Pascal projects in a full 3D workspace.',
+ robots: {
+ index: false,
+ follow: false,
+ },
+}
+
+export default function EditorProjectLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return children
+}
diff --git a/apps/editor/app/favicon.ico b/apps/editor/app/favicon.ico
index 718d6fea..e9f36729 100644
Binary files a/apps/editor/app/favicon.ico and b/apps/editor/app/favicon.ico differ
diff --git a/apps/editor/app/layout.tsx b/apps/editor/app/layout.tsx
index ddb23ce8..b378f05b 100644
--- a/apps/editor/app/layout.tsx
+++ b/apps/editor/app/layout.tsx
@@ -4,6 +4,7 @@ import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
import { VercelToolbar } from '@vercel/toolbar/next'
import { UsernameGate } from '@/features/community/components/username-gate'
+import { siteConfig } from './seo'
import './globals.css'
const geistSans = localFont({
@@ -16,8 +17,48 @@ const geistMono = localFont({
})
export const metadata: Metadata = {
- title: 'Pascal Editor',
- description: 'Open-source 3D building editor',
+ metadataBase: new URL(siteConfig.url),
+ title: {
+ default: siteConfig.name,
+ template: '%s | Pascal Editor',
+ },
+ description: siteConfig.description,
+ applicationName: siteConfig.name,
+ keywords: [...siteConfig.keywords],
+ authors: [{ name: 'Pascal', url: 'https://pascal.app' }],
+ creator: 'Pascal',
+ publisher: 'Pascal',
+ alternates: {
+ canonical: '/',
+ },
+ icons: [{ rel: 'icon', url: '/favicon.ico' }],
+ openGraph: {
+ title: siteConfig.name,
+ description: siteConfig.description,
+ url: siteConfig.url,
+ siteName: siteConfig.name,
+ images: [{ url: siteConfig.ogImage, alt: 'Pascal Editor' }],
+ locale: 'en_US',
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: siteConfig.name,
+ description: siteConfig.description,
+ creator: siteConfig.twitterHandle,
+ images: [siteConfig.ogImage],
+ },
+ robots: {
+ index: true,
+ follow: true,
+ googleBot: {
+ index: true,
+ follow: true,
+ 'max-image-preview': 'large',
+ 'max-snippet': -1,
+ 'max-video-preview': -1,
+ },
+ },
}
export default function RootLayout({
diff --git a/apps/editor/app/og/route.tsx b/apps/editor/app/og/route.tsx
new file mode 100644
index 00000000..a7baf229
--- /dev/null
+++ b/apps/editor/app/og/route.tsx
@@ -0,0 +1,163 @@
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { ImageResponse } from 'next/og'
+import * as z from 'zod'
+import { siteConfig } from '@/app/seo'
+
+const ogImageSchema = z.object({
+ title: z.string().default(''),
+ description: z.string().default(''),
+ theme: z.enum(['light', 'dark']).default('dark'),
+})
+
+let sansFont: ArrayBuffer | null = null
+let jakartaFont: ArrayBuffer | null = null
+
+async function loadFonts() {
+ if (!(sansFont && jakartaFont)) {
+ const fontDir = join(process.cwd(), 'public', 'fonts')
+ const [sans, jakarta] = await Promise.all([
+ readFile(join(fontDir, 'geist-regular.ttf')),
+ readFile(join(fontDir, 'PlusJakartaSans-SemiBold.ttf')),
+ ])
+ sansFont = sans.buffer.slice(sans.byteOffset, sans.byteOffset + sans.byteLength)
+ jakartaFont = jakarta.buffer.slice(jakarta.byteOffset, jakarta.byteOffset + jakarta.byteLength)
+ }
+ return { sans: sansFont, jakarta: jakartaFont }
+}
+
+function PascalLogo({ color = '#fff', size = 100 }: { color?: string; size?: number }) {
+ return (
+
+ )
+}
+
+export async function GET(req: Request) {
+ try {
+ const url = new URL(req.url)
+ const values = ogImageSchema.parse(Object.fromEntries(url.searchParams))
+ const heading =
+ values.title.length > 140 ? `${values.title.substring(0, 140)}...` : values.title
+
+ const { theme } = values
+ const paint = theme === 'dark' ? '#fff' : '#000'
+ const logoColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)'
+
+ const fontSize = heading.length > 100 ? '70px' : '100px'
+
+ const showLargeLogo = !(values.title || values.description)
+
+ return new ImageResponse(
+
+ {showLargeLogo ? (
+
+
+
+ Pascal Editor
+
+
+ {siteConfig.description}
+
+
+ ) : (
+
+
+
+ {heading}
+
+
+ {values.description}
+
+
+ )}
+ {!showLargeLogo && (
+
+ editor.pascal.app
+
+ )}
+
,
+ {
+ width: 1200,
+ height: 630,
+ fonts: await loadFonts().then(({ sans, jakarta }) => [
+ {
+ name: 'sans-regular',
+ data: sans,
+ style: 'normal' as const,
+ weight: 400 as const,
+ },
+ {
+ name: 'jakarta-semibold',
+ data: jakarta,
+ style: 'normal' as const,
+ weight: 600 as const,
+ },
+ ]),
+ },
+ )
+ } catch (error) {
+ console.log('error', error)
+ return new Response('Failed to generate image', {
+ status: 500,
+ })
+ }
+}
diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx
index a7013249..303567bf 100644
--- a/apps/editor/app/page.tsx
+++ b/apps/editor/app/page.tsx
@@ -1,5 +1,12 @@
+import type { Metadata } from 'next'
import CommunityHub from '@/features/community/components/community-hub'
+export const metadata: Metadata = {
+ title: 'Community Projects',
+ description:
+ 'Create and share 3D home projects with Pascal Editor, the open-source building editor.',
+}
+
export default function Home() {
return
}
diff --git a/apps/editor/app/robots.ts b/apps/editor/app/robots.ts
new file mode 100644
index 00000000..d1f931f3
--- /dev/null
+++ b/apps/editor/app/robots.ts
@@ -0,0 +1,16 @@
+import type { MetadataRoute } from 'next'
+import { siteConfig } from './seo'
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: [
+ {
+ userAgent: '*',
+ allow: ['/', '/viewer/', '/u/'],
+ disallow: ['/api/', '/editor/', '/settings', '/_next/'],
+ },
+ ],
+ sitemap: `${siteConfig.url}/sitemap.xml`,
+ host: siteConfig.url,
+ }
+}
diff --git a/apps/editor/app/seo.ts b/apps/editor/app/seo.ts
new file mode 100644
index 00000000..a87f5215
--- /dev/null
+++ b/apps/editor/app/seo.ts
@@ -0,0 +1,22 @@
+import { BASE_URL } from '@/lib/utils'
+
+export const siteConfig = {
+ name: 'Pascal Editor',
+ description:
+ 'Pascal Editor is an open-source 3D building editor for designing, editing, and sharing home projects.',
+ url: BASE_URL,
+ website: 'editor.pascal.app',
+ ogImage: '/og',
+ keywords: [
+ 'Pascal Editor',
+ 'open-source 3D editor',
+ '3D building editor',
+ 'home design software',
+ 'architecture editor',
+ 'collaborative design',
+ ],
+ twitterHandle: '@pascal_app',
+ links: {
+ github: 'https://github.com/pascalorg/editor',
+ },
+} as const
diff --git a/apps/editor/app/settings/page.tsx b/apps/editor/app/settings/page.tsx
index f588597b..fefd6f20 100644
--- a/apps/editor/app/settings/page.tsx
+++ b/apps/editor/app/settings/page.tsx
@@ -1,10 +1,20 @@
export const dynamic = 'force-dynamic'
+import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getSession } from '@/features/community/lib/auth/server'
import { getUserProfile, getConnectedAccounts } from '@/features/community/lib/auth/actions'
import { SettingsPage } from '@/features/community/components/settings-page'
+export const metadata: Metadata = {
+ title: 'Account Settings',
+ description: 'Manage your Pascal Editor account profile and connected providers.',
+ robots: {
+ index: false,
+ follow: false,
+ },
+}
+
export default async function Settings() {
const session = await getSession()
if (!session?.user) {
diff --git a/apps/editor/app/sitemap.ts b/apps/editor/app/sitemap.ts
new file mode 100644
index 00000000..4328abf2
--- /dev/null
+++ b/apps/editor/app/sitemap.ts
@@ -0,0 +1,22 @@
+import type { MetadataRoute } from 'next'
+import { siteConfig } from './seo'
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const currentDate = new Date()
+ const baseUrl = siteConfig.url
+
+ return [
+ {
+ url: baseUrl,
+ lastModified: currentDate,
+ changeFrequency: 'daily',
+ priority: 1,
+ },
+ {
+ url: `${baseUrl}/viewer/demo_1`,
+ lastModified: currentDate,
+ changeFrequency: 'weekly',
+ priority: 0.7,
+ },
+ ]
+}
diff --git a/apps/editor/app/u/[username]/page.tsx b/apps/editor/app/u/[username]/page.tsx
index 135e6f42..7b0e7e12 100644
--- a/apps/editor/app/u/[username]/page.tsx
+++ b/apps/editor/app/u/[username]/page.tsx
@@ -1,10 +1,27 @@
export const dynamic = 'force-dynamic'
+import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPublicProfile } from '@/features/community/lib/auth/actions'
import { getPublicProjectsByUserId } from '@/features/community/lib/projects/actions'
import { PublicProfilePage } from '@/features/community/components/public-profile-page'
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ username: string }>
+}): Promise {
+ const { username } = await params
+
+ return {
+ title: `${username}'s Projects`,
+ description: `Public projects shared by @${username} on Pascal Editor.`,
+ alternates: {
+ canonical: `/u/${encodeURIComponent(username)}`,
+ },
+ }
+}
+
export default async function ProfilePage({
params,
}: {
diff --git a/apps/editor/app/viewer/[id]/layout.tsx b/apps/editor/app/viewer/[id]/layout.tsx
new file mode 100644
index 00000000..9a53f302
--- /dev/null
+++ b/apps/editor/app/viewer/[id]/layout.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Project Viewer',
+ description: 'View and share 3D projects built with Pascal Editor.',
+}
+
+export default function ViewerLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return children
+}
diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx
index 7f2c2c33..a29d1ff7 100644
--- a/apps/editor/components/ui/sidebar/icon-rail.tsx
+++ b/apps/editor/components/ui/sidebar/icon-rail.tsx
@@ -36,7 +36,7 @@ export function IconRail({
className,
)}
>
- {/* Pascal Logo - Link to Hub */}
+ {/* Pascal logo - link to the home page */}
- Back to Hub
+ Back to Pascal Editor
{/* Divider */}
diff --git a/apps/editor/features/community/components/community-hub.tsx b/apps/editor/features/community/components/community-hub.tsx
index c7aa5cf8..839fdc9b 100644
--- a/apps/editor/features/community/components/community-hub.tsx
+++ b/apps/editor/features/community/components/community-hub.tsx
@@ -118,18 +118,19 @@ export default function CommunityHub() {
height={64}
className="h-5 w-5"
/>
- Hub
+ Pascal Editor
-
{!isAuthenticated ? (