+ {theme.name} +
+Preview Atlas, Mono & SaaS templates β
+diff --git a/apps/ottabase-template-app-nextjs-homepage/app/layout.tsx b/apps/ottabase-template-app-nextjs-homepage/app/layout.tsx index 356d2946a..f71611cc4 100644 --- a/apps/ottabase-template-app-nextjs-homepage/app/layout.tsx +++ b/apps/ottabase-template-app-nextjs-homepage/app/layout.tsx @@ -1,32 +1,27 @@ import { buildCriticalCSS } from '@ottabase/brand-engine'; import type { Metadata } from 'next'; +import { siteContent, homePage } from '../config/landing.config'; import { generateBrandConfig } from '../lib/brand-server'; import './globals.css'; import { Providers } from './providers'; export const metadata: Metadata = { - title: 'Ottabase Next.js Homepage Template', - description: 'A barebone Next.js homepage template with OpenNext and Cloudflare Workers deployment', + title: homePage.title || siteContent.name, + description: homePage.metaDescription || siteContent.tagline, keywords: ['nextjs', 'cloudflare', 'workers', 'opennext', 'homepage', 'template', 'brand-engine'], robots: 'index, follow', - authors: [{ name: 'Ottabase' }], + authors: [{ name: siteContent.name }], }; export default function RootLayout({ children }: { children: React.ReactNode }) { - // Generate brand config server-side (SSR) - // Note: Using 'light' for initial SSR. BrandProvider will handle dynamic theme switching on client. const brandConfig = generateBrandConfig('light'); const theme = brandConfig.brandKitsMap.default.theme; - - // Generate critical CSS for SSR (prevents FOUC) const criticalCSS = buildCriticalCSS(theme); return (
- {/* Inject critical CSS for theme variables */} - {/* Load fonts - only if URLs are defined */} {theme.typography.heading.url && } {theme.typography.body.url && } {theme.typography.handwriting.url && } diff --git a/apps/ottabase-template-app-nextjs-homepage/app/page.tsx b/apps/ottabase-template-app-nextjs-homepage/app/page.tsx index 562fc6fbe..85d60e63f 100644 --- a/apps/ottabase-template-app-nextjs-homepage/app/page.tsx +++ b/apps/ottabase-template-app-nextjs-homepage/app/page.tsx @@ -1,88 +1,10 @@ -import { DarkModeToggle } from '@ottabase/ui-components/dark-mode-toggle'; -import { Button } from '@ottabase/ui-shadcn'; -import Link from 'next/link'; +import { getLandingTheme, initOttaLanding, renderPage } from '@ottabase/ottalanding'; +import { homePage, landingThemeId, siteContent } from '@/config/landing.config'; -export default function HomePage() { - return ( -- A barebone Next.js homepage template with OpenNext and Cloudflare Workers deployment -
- -- Built with Next.js 16, TypeScript, Tailwind CSS, - and Brand Engine. Perfect for creating beautiful, dynamic homepages deployed to - Cloudflare Workers. -
- - {/* Action Buttons */} -- Deploy to Cloudflare Workers with OpenNext for edge-optimized performance -
-- Built-in theme system with 8+ presets and full customization support -
-- Latest Next.js with App Router and React Server Components -
-- Full TypeScript support for better developer experience -
-- Β© {new Date().getFullYear()} Ottabase. Open Source. -
-+ 8 built-in brand themes. Click any theme to preview a full marketing page. +
+Preview Atlas, Mono & SaaS templates β
+Loading...
+ ) : sections.length === 0 ? ( ++ Add sections like Hero, Features, Pricing, etc. +
++ {SECTION_LABELS[section.sectionType] ?? section.sectionType} +
+ +Loading page...
; + } + + if (!page) { + returnPage not found.
; + } + + return ( +/{page.slug}
+{description}
+ )} + + {fieldType === 'textarea' ? ( ++ No structured fields defined for this section type. Editing raw JSON content. +
+{error}
} +Loading section...
; + } + + if (!section) { + returnSection not found.
; + } + + const sectionLabel = SECTION_LABELS[section.sectionType] ?? section.sectionType; + + return ( +Edit section content.
++ Pages in this site. Click "Set as homepage" to choose which page shows at /. +
+ +Loading...
+ ) : pages.length === 0 ? ( ++ Add a page to start building your landing site. +
+ +{page.title}
+/{page.slug}
+ +Loading site...
+Site not found.
+Site settings, pages, and content management.
+Manage your marketing landing page sites.
+Failed to load sites: {error.message}
+Loading...
+ ) : sites.length === 0 ? ( ++ Create your first landing site to get started. +
+ ++ {site.name} +
+ {site.tagline && ( ++ {site.tagline} +
+ )} +{eyebrow}
} +{mission}
} +{story}
+{v.description}
+{m.role}
+ {m.bio &&{m.bio}
} ++ {subheadline} +
+ )} + +{eyebrow}
} +{subheadline}
} ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} +{eyebrow}
} +{description}
+ {bullets && bullets.length > 0 && ( ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} ++ {feature.description} +
++ {subheadline} +
+ )} + + {/* CTAs */} + {(primaryCta || secondaryCta) && ( ++ {socialProof.count}{' '} + {socialProof.label} +
+ )} + + {/* Product image */} + {image && ( ++ {label} +
+ )} + ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} + + {/* Billing toggle */} + {hasAnnual && ( +{plan.name}
+ {plan.description && ( +{plan.description}
+ )} ++ {eyebrow} +
+ )} + {headline && ( ++ {stat.value} +
+{stat.label}
+ {stat.description && ( +{stat.description}
+ )} ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} +{step.description}
++ {eyebrow} +
+ )} + {headline && ( ++ “{t.quote}” +
+{t.author}
+ {(t.role || t.company) && ( ++ {[t.role, t.company].filter(Boolean).join(', ')} +
+ )} +{eyebrow}
} +{subheadline}
} +{event.date}
+{event.description}
+{event.date}
+{event.description}
+{event.date}
+{event.description}
+β {eyebrow}
} +{mission}
} +{story}
+β Values
+{v.description}
+β Team
+{m.name}
+{m.role}
+{m.bio}
} ++ {subheadline} +
+ )} +β {eyebrow}
} +{subheadline}
} +β Send a message
+ ++ β {eyebrow} +
+ )} +{subheadline}
+ )} +β {eyebrow}
} +{description}
+ {bullets && bullets.length > 0 && ( ++ β {eyebrow} +
+ )} ++ {subheadline} +
+ )} ++ {feature.description} +
++ β {badge} +
+ )} + + {/* Headline */} ++ {subheadline} +
+ )} + + {(primaryCta || secondaryCta) && ( ++ {label} +
+ )} + ++ β {eyebrow} +
+ )} +{subheadline}
} +| + Feature + | + {plans.map((plan) => ( +
+
+
+
+
+ {plan.name}
+
+ {plan.badge && (
+
+ {plan.badge}
+
+ )}
+
+ + {billing === 'annual' && plan.price.annual + ? plan.price.annual + : plan.price.monthly} + + {plan.price.suffix ?? '/mo'} + + + {plan.description && ( +{plan.description} + )} + |
+ ))}
+
|---|---|
| + {featureLabel} + | + {plans.map((plan) => { + const f = plan.features.find((x) => x.label === featureLabel); + return ( ++ {!f || f.included === false ? ( + β + ) : typeof f.included === 'string' ? ( + + {f.included} + + ) : ( + + )} + | + ); + })} +
| + {plans.map((plan) => ( + | + + {plan.cta.label} + + | + ))} +
+ β {eyebrow} +
+ )} + {headline &&+ {stat.value} +
++ {stat.label} +
+ {stat.description && ( +{stat.description}
+ )} ++ β {eyebrow} +
+ )} +{subheadline}
} +{step.description}
++ β {eyebrow} +
+ )} + {headline && ( ++ {current.quote} +
+ +{current.author}
+ {(current.role || current.company) && ( ++ {[current.role, current.company].filter(Boolean).join(' Β· ')} +
+ )} +β {eyebrow}
} +{subheadline}
} +{event.description}
+{eyebrow}
} +{mission}
} +{story}
+{v.description}
+{m.role}
+ {m.bio &&{m.bio}
} ++ {subheadline} +
+ )} + +{eyebrow}
} +{subheadline}
} ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} +{eyebrow}
} +{description}
+ {bullets && bullets.length > 0 && ( ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} ++ {feature.description} +
++ {subheadline} +
+ )} + + {(primaryCta || secondaryCta) && ( ++ {socialProof.count}{' '} + {socialProof.label} +
+ )} + + {image && ( ++ {label} +
+ )} + ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} + + {hasAnnual && ( +{plan.name}
+ {plan.description && ( +{plan.description}
+ )} ++ {eyebrow} +
+ )} + {headline && ( ++ {stat.value} +
+{stat.label}
+ {stat.description && ( +{stat.description}
+ )} ++ {eyebrow} +
+ )} ++ {subheadline} +
+ )} +{step.description}
++ {eyebrow} +
+ )} + {headline && ( ++ “{t.quote}” +
+{t.author}
+ {(t.role || t.company) && ( ++ {[t.role, t.company].filter(Boolean).join(', ')} +
+ )} +{eyebrow}
} +{subheadline}
} +{event.date}
+{event.description}
+{event.date}
+{event.description}
+