diff --git a/apps/landing/devup.json b/apps/landing/devup.json
index b100ac5f..b6db3ff7 100644
--- a/apps/landing/devup.json
+++ b/apps/landing/devup.json
@@ -492,7 +492,21 @@
},
null,
null
- ]
+ ],
+ "code": {
+ "fontFamily": "D2Coding",
+ "fontWeight": 400,
+ "fontSize": "13px",
+ "lineHeight": 1.5,
+ "letterSpacing": "0em"
+ },
+ "eyebrow": {
+ "fontFamily": "D2Coding",
+ "fontWeight": 400,
+ "fontSize": "11px",
+ "lineHeight": 1.4,
+ "letterSpacing": "0.14em"
+ }
}
}
}
\ No newline at end of file
diff --git a/apps/landing/src/app/_components/code-tabs.tsx b/apps/landing/src/app/_components/code-tabs.tsx
new file mode 100644
index 00000000..506b1d7d
--- /dev/null
+++ b/apps/landing/src/app/_components/code-tabs.tsx
@@ -0,0 +1,238 @@
+import { Box, Text } from '@devup-ui/react'
+
+import { CodeWindow, HighlightedCode } from './code-window'
+
+export interface CodeExample {
+ key: string
+ label: string
+ file: string
+ html: string
+}
+
+export type CodeExampleQuad = [CodeExample, CodeExample, CodeExample, CodeExample]
+
+// devup-ui extracts `selectors` strings at build time, so each of the 4
+// fixed tabs is written out literally rather than generated by a .map()
+// over a runtime key — a template-literal selector key can't be statically
+// extracted into CSS.
+//
+// Each target element (label / title / panel) declares its own "when am I
+// shown" condition anchored on the checkbox's id (`#code-tab-N:checked ~ *
+// &`), rather than the checkbox declaring rules for elements elsewhere in
+// the tree. The checkbox's own `:checked` state isn't observable from the
+// checkbox's position in isolation, so ownership of the visibility rule
+// belongs on the element it affects.
+export function CodeTabs({ examples: [a, b, c, d] }: { examples: CodeExampleQuad }) {
+ return (
+
+
+
+
+
+
+
+ {a.label}
+
+
+ {b.label}
+
+
+ {c.label}
+
+
+ {d.label}
+
+ >
+ }
+ title={
+ <>
+
+ {a.file}
+
+
+ {b.file}
+
+
+ {c.file}
+
+
+ {d.file}
+
+ >
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/landing/src/app/_components/code-window.tsx b/apps/landing/src/app/_components/code-window.tsx
new file mode 100644
index 00000000..2200a9ff
--- /dev/null
+++ b/apps/landing/src/app/_components/code-window.tsx
@@ -0,0 +1,108 @@
+import { Box, Flex, globalCss, Text } from '@devup-ui/react'
+import type { ComponentProps, ReactNode } from 'react'
+
+globalCss({
+ '.shiki, .shiki span': {
+ fontFamily: 'D2Coding',
+ fontSize: '13px',
+ lineHeight: '1.65',
+ },
+ '.shiki': {
+ background: 'transparent !important',
+ padding: '0',
+ margin: '0',
+ overflowX: 'auto',
+ },
+ '[data-theme="dark"] .shiki, [data-theme="dark"] .shiki span': {
+ color: 'var(--shiki-dark) !important',
+ backgroundColor: 'transparent !important',
+ },
+})
+
+export function CodeWindow({
+ title,
+ tabs,
+ children,
+ ...props
+}: {
+ title: ReactNode
+ tabs?: ReactNode
+ children: ReactNode
+} & Omit>, 'title'>) {
+ return (
+
+
+
+ {Array.from({ length: 3 }, (_, i) => (
+
+ ))}
+
+
+ {title}
+
+ {tabs && (
+
+ {tabs}
+
+ )}
+
+
+ {children}
+
+
+ )
+}
+
+export function HighlightedCode({ html }: { html: string }) {
+ return
+}
+
+export function StaticCodeBlock({
+ title,
+ html,
+}: {
+ title: string
+ html: string
+}) {
+ return (
+
+
+
+ )
+}
+
+export function HeroCodeWrapper({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/landing/src/app/_components/copy-install.tsx b/apps/landing/src/app/_components/copy-install.tsx
new file mode 100644
index 00000000..b602c7f6
--- /dev/null
+++ b/apps/landing/src/app/_components/copy-install.tsx
@@ -0,0 +1,60 @@
+'use client'
+
+import { Box, Flex, Text } from '@devup-ui/react'
+import { useState } from 'react'
+
+export function CopyInstall({
+ command = 'cargo install vespertide-cli',
+}: {
+ command?: string
+}) {
+ const [copied, setCopied] = useState(false)
+
+ const copy = () => {
+ navigator.clipboard?.writeText(command)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1400)
+ }
+
+ return (
+
+
+ $
+
+
+ {command}
+
+ {
+ e.stopPropagation()
+ copy()
+ }}
+ px="9px"
+ py="5px"
+ transition="color .15s, border-color .15s"
+ typography="eyebrow"
+ >
+ {copied ? 'copied' : 'copy'}
+
+
+ )
+}
diff --git a/apps/landing/src/app/_components/example.tsx b/apps/landing/src/app/_components/example.tsx
deleted file mode 100644
index 6e69244b..00000000
--- a/apps/landing/src/app/_components/example.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client'
-
-import { Flex, Image } from '@devup-ui/react'
-import { ComponentProps, createContext, useContext, useState } from 'react'
-
-const ExampleContext = createContext<{
- selected: string
- setSelected: (selected: string) => void
- selectedExample?: {
- id: string
- title: string
- description: string
- imageUrl: string
- }
-} | null>(null)
-
-export function useExample() {
- const context = useContext(ExampleContext)
- if (!context) {
- throw new Error('useExample must be used within a ExampleProvider')
- }
- return context
-}
-
-export function ExampleProvider({
- defaultSelected = '',
- examples,
- children,
-}: {
- defaultSelected?: string
- examples: {
- id: string
- title: string
- description: string
- imageUrl: string
- }[]
- children: React.ReactNode
-}) {
- const [selected, setSelected] = useState(defaultSelected)
- const selectedExample = examples.find((example) => example.id === selected)
- return (
-
- {children}
-
- )
-}
-
-export function ExampleContainer({
- value,
- ...props
-}: ComponentProps> & { value?: string }) {
- const { selected, setSelected } = useExample()
- const isSelected = selected === value
- return (
- setSelected(value) : undefined}
- overflow="hidden"
- px="$spacingSpacing24"
- py="$spacingSpacing20"
- styleOrder={1}
- transition="all .1s"
- {...props}
- />
- )
-}
-
-export function ExampleImage({
- ...props
-}: Omit>, 'src'>) {
- const { selectedExample } = useExample()
- return (
-
- )
-}
-
-export function Example() {}
diff --git a/apps/landing/src/app/_components/join-icon-button.tsx b/apps/landing/src/app/_components/join-icon-button.tsx
deleted file mode 100644
index 24128d88..00000000
--- a/apps/landing/src/app/_components/join-icon-button.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Flex } from '@devup-ui/react'
-import { ComponentProps } from 'react'
-
-export function JoinIconButton(props: ComponentProps>) {
- return (
-
- )
-}
diff --git a/apps/landing/src/app/_lib/highlight.ts b/apps/landing/src/app/_lib/highlight.ts
new file mode 100644
index 00000000..82e8cf23
--- /dev/null
+++ b/apps/landing/src/app/_lib/highlight.ts
@@ -0,0 +1,19 @@
+import { codeToHtml } from 'shiki'
+
+export type CodeLang = 'json' | 'shell' | 'rust'
+
+const LANG_MAP: Record = {
+ json: 'json',
+ shell: 'bash',
+ rust: 'rust',
+}
+
+export async function highlight(code: string, lang: CodeLang): Promise {
+ return codeToHtml(code, {
+ lang: LANG_MAP[lang],
+ themes: {
+ light: 'github-light',
+ dark: 'github-dark',
+ },
+ })
+}
diff --git a/apps/landing/src/app/page.tsx b/apps/landing/src/app/page.tsx
index b61eaf4c..d7c80551 100644
--- a/apps/landing/src/app/page.tsx
+++ b/apps/landing/src/app/page.tsx
@@ -1,18 +1,14 @@
-import { JoinIconButton } from '@app/_components/join-icon-button'
-import { Box, Center, css, Flex, Text, VStack } from '@devup-ui/react'
-import { Image } from '@devup-ui/react'
+import { Box, Flex, Text, VStack } from '@devup-ui/react'
import type { Metadata } from 'next'
import Link from 'next/link'
+import type { ComponentProps } from 'react'
import { Button } from '@/components/button'
-import { GnbIcon } from '@/components/header/gnb-icon'
-import { HeaderSentinel } from '@/components/header/header-sentinel'
-import {
- ExampleContainer,
- ExampleImage,
- ExampleProvider,
-} from './_components/example'
+import { CodeTabs, type CodeExampleQuad } from './_components/code-tabs'
+import { CodeWindow, HighlightedCode } from './_components/code-window'
+import { CopyInstall } from './_components/copy-install'
+import { highlight } from './_lib/highlight'
export const metadata: Metadata = {
alternates: {
@@ -20,290 +16,976 @@ export const metadata: Metadata = {
},
}
-const EXAMPLES = [
+const VERSION = '0.1.61'
+const GITHUB_URL = 'https://github.com/dev-five-git/vespertide'
+const DOCS_URL = '/documentation'
+const DISCORD_URL = 'https://discord.com/invite/8zjcGc7cWh'
+const KAKAO_URL = 'https://open.kakao.com/o/giONwVAh'
+const CRATES_URL = 'https://crates.io/crates/vespertide-cli'
+
+const HERO_MODEL_JSON = `{
+ "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/main/schemas/model.schema.json",
+ "name": "user",
+ "columns": [
+ { "name": "id", "type": "integer", "primary_key": true },
+ { "name": "email", "type": "text", "unique": true, "index": true },
+ { "name": "name", "type": { "kind": "varchar", "length": 100 } },
+ {
+ "name": "status",
+ "type": { "kind": "enum", "name": "user_status",
+ "values": ["active", "inactive", "banned"] },
+ "default": "'active'"
+ },
+ { "name": "created_at", "type": "timestamptz", "default": "NOW()" }
+ ]
+}`
+
+const EXAMPLE_MODEL = `{
+ "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/main/schemas/model.schema.json",
+ "name": "post",
+ "columns": [
+ { "name": "id", "type": "integer", "primary_key": true },
+ { "name": "title", "type": { "kind": "varchar", "length": 200 } },
+ { "name": "body", "type": "text" },
+ {
+ "name": "author_id",
+ "type": "integer",
+ "foreign_key": {
+ "ref_table": "user",
+ "ref_columns": ["id"],
+ "on_delete": "cascade"
+ },
+ "index": true
+ },
+ {
+ "name": "status",
+ "type": { "kind": "enum", "name": "post_status",
+ "values": ["draft", "published", "archived"] },
+ "default": "'draft'"
+ }
+ ]
+}`
+
+const EXAMPLE_CLI = `# Initialize a new project
+$ vespertide init
+
+# Scaffold a model
+$ vespertide new post
+
+# Edit models/post.json, then preview the diff
+$ vespertide diff
++ create_table post (id, title, body, author_id, status)
++ create_enum post_status [draft, published, archived]
++ create_index ix_post_author_id ON post (author_id)
+
+# Inspect dialect-specific SQL
+$ vespertide sql --backend postgres
+
+# Persist as a migration file
+$ vespertide revision -m "create post table"`
+
+const EXAMPLE_RUNTIME = `use sea_orm::Database;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let db = Database::connect("postgres://user:pass@localhost/mydb").await?;
+
+ // Generated at compile time, run on startup.
+ vespertide::vespertide_migration!(db).await?;
+
+ Ok(())
+}`
+
+const EXAMPLE_EXPORT = `# Generate Rust SeaORM entities
+$ vespertide export --orm seaorm
+
+# Or Python — SQLAlchemy
+$ vespertide export --orm sqlalchemy
+
+# Or FastAPI-flavoured SQLModel
+$ vespertide export --orm sqlmodel
+
+# Or Go — GORM
+$ vespertide export --orm gorm
+
+# Or Django
+$ vespertide export --orm django`
+
+type FeatureIconName =
+ | 'hamburger'
+ | 'arrow-up-right'
+ | 'chevron'
+ | 'logo-image'
+ | 'devfive'
+ | 'theme-dark'
+ | 'search'
+ | 'external-link'
+ | 'github'
+
+const FEATURES: { icon: FeatureIconName; title: string; desc: string }[] = [
+ {
+ icon: 'hamburger',
+ title: 'Declarative schema',
+ desc: 'Describe your desired database state in JSON files. The current model is the source of truth.',
+ },
+ {
+ icon: 'arrow-up-right',
+ title: 'Automatic diffing',
+ desc: 'Vespertide replays applied migrations and compares them to your models to compute changes.',
+ },
+ {
+ icon: 'chevron',
+ title: 'Typed migration plans',
+ desc: 'Generates safe, portable MigrationAction enums — not raw SQL. Review before you commit.',
+ },
+ {
+ icon: 'logo-image',
+ title: 'Multi-database',
+ desc: 'PostgreSQL, MySQL, and SQLite — same schema, identical semantics, backend-aware quoting.',
+ },
+ {
+ icon: 'devfive',
+ title: 'Native enums',
+ desc: 'First-class string and integer enums. Add new integer values without ever touching the DB.',
+ },
+ {
+ icon: 'theme-dark',
+ title: 'Zero-runtime macro',
+ desc: 'vespertide_migration!() generates database-specific SQL at compile time. Nothing to ship at runtime.',
+ },
{
- id: '1',
- title: 'How to Use',
- description:
- 'Lorem ipsum dolor sit amet. Etiam sit amet feugiat turpis. Proin nec ante a sem vestibulum sodales non ut ex.',
- imageUrl: '/images/hero-figure.webp',
+ icon: 'search',
+ title: 'JSON Schema validation',
+ desc: 'Ships with JSON Schemas — autocomplete, hover docs, and instant errors in your editor.',
},
{
- id: '2',
- title: 'How to Use',
- description:
- 'Lorem ipsum dolor sit amet. Etiam sit amet feugiat turpis. Proin nec ante a sem vestibulum sodales non ut ex.',
- imageUrl: '/images/join-us-bg.webp',
+ icon: 'external-link',
+ title: 'ORM export',
+ desc: 'One command emits SeaORM, SQLAlchemy, Django, GORM and more — entities stay in lockstep with schema.',
},
{
- id: '3',
- title: 'How to Use',
- description:
- 'Lorem ipsum dolor sit amet. Etiam sit amet feugiat turpis. Proin nec ante a sem vestibulum sodales non ut ex.',
- imageUrl: '/images/code.webp',
+ icon: 'github',
+ title: 'Built in Rust',
+ desc: "Single binary CLI, no Node, no Python, no JVM. cargo install vespertide-cli and you're done.",
},
]
-export default function HomePage() {
+const STEPS: { title: string; desc: string; mono: string }[] = [
+ {
+ title: 'Define',
+ desc: 'Author JSON models in your editor with full IDE validation via JSON Schema.',
+ mono: 'models/user.json',
+ },
+ {
+ title: 'Replay',
+ desc: 'Vespertide reconstructs the baseline schema by replaying applied migrations.',
+ mono: 'migrations/*.sql',
+ },
+ {
+ title: 'Diff',
+ desc: 'Current models are compared to the baseline to find what changed.',
+ mono: 'vespertide diff',
+ },
+ {
+ title: 'Plan',
+ desc: 'Differences are converted into typed MigrationAction enums.',
+ mono: 'MigrationAction',
+ },
+ {
+ title: 'Emit',
+ desc: 'Actions translate to dialect-specific SQL — Postgres, MySQL, or SQLite.',
+ mono: 'vespertide sql',
+ },
+]
+
+const DBS = [
+ {
+ key: 'PG',
+ name: 'PostgreSQL',
+ quote: '"identifier"',
+ note: 'Full feature support — native enums, JSONB, INET, CIDR, TSVECTOR.',
+ },
+ {
+ key: 'MY',
+ name: 'MySQL',
+ quote: '`identifier`',
+ note: 'Full feature support with MySQL-aware identifier quoting and types.',
+ },
+ {
+ key: 'SL',
+ name: 'SQLite',
+ quote: '"identifier"',
+ note: 'Full feature support — perfect for tests, CLIs, and embedded apps.',
+ },
+]
+
+const ORMS = [
+ { lang: 'Rust', name: 'SeaORM' },
+ { lang: 'Python', name: 'SQLAlchemy' },
+ { lang: 'Python', name: 'SQLModel · FastAPI' },
+ { lang: 'Python', name: 'Django' },
+ { lang: 'Java', name: 'JPA · Hibernate' },
+ { lang: 'TypeScript', name: 'Prisma' },
+ { lang: 'TypeScript', name: 'Drizzle' },
+ { lang: 'Go', name: 'GORM' },
+]
+
+function MaskIcon({
+ icon,
+ size = '24px',
+ color = '$vespertidePrimary',
+ ...props
+}: {
+ icon: FeatureIconName | 'discord' | 'kakao'
+ size?: string
+ color?: string
+} & ComponentProps>) {
return (
- <>
-
-
+ )
+}
+
+function SectionHead({
+ eyebrow,
+ title,
+ emphasis,
+ lede,
+}: {
+ eyebrow: string
+ title: string
+ emphasis?: string
+ lede?: string
+}) {
+ return (
+
+
+ — {eyebrow}
+
+
+ {title}
+ {emphasis && (
+
+ {emphasis}
+
+ )}
+
+ {lede && (
+
+ {lede}
+
+ )}
+
+ )
+}
+
+function HeroSection({ codeHtml }: { codeHtml: string }) {
+ return (
+
+
+
+
+
+
+
+
+ v{VERSION} · Apache-2.0 · Rust
+
+
+
+
+ Define schemas.
+
+
+ Forget migrations.
+
+
+
+
+
+ Vespertide is a declarative database schema manager for Rust. Write
+ your tables in JSON, and let it diff, plan, and emit type-safe
+ migrations to Postgres, MySQL, and SQLite — automatically.
+
+
+
+
+
+
+
+
+
+
+
+ View on GitHub
+
+
+
+
+
+
+
+
+ {[
+ { num: `v${VERSION}`, lbl: 'Version' },
+ { num: '3', lbl: 'Databases' },
+ { num: '4', lbl: 'ORM exports' },
+ { num: '0ms', lbl: 'Runtime cost' },
+ ].map((s) => (
+
+
+ {s.num}
+
+
+ {s.lbl}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function FeaturesSection() {
+ return (
+
+
+
+
+
+
-
+ {FEATURES.map((f) => (
-
- Lorem ipsum dolor sit amet,
- consectetur adipiscing elit.
+
+
+ {f.title}
-
- Etiam sit amet feugiat turpis. Proin nec ante a sem vestibulum
- sodales non ut ex.
- Morbi diam turpis, fringilla vitae enim et, egestas consequat
- nibh.
- Etiam auctor cursus urna sit amet elementum.
+
+ {f.desc}
-
-
-
-
+ ))}
+
+
+
+ )
+}
-
+
+
+
+
+
-
-
-
- Title
+ {STEPS.map((s, i) => (
+
+
+ {String(i + 1).padStart(2, '0')}
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam
- venenatis, elit in hendrerit porta, augue ante scelerisque diam,{' '}
-
- ac egestas lacus est nec urna. Cras commodo risus hendrerit,
- suscipit nibh at, porttitor dui.
+
+ {s.title}
-
-
- {[0, 1, 2, 3].map((i) => (
-
+ {s.desc}
+
+
+
-
+
+
+ ))}
+
+
+
+ )
+}
+
+function ExamplesSection({ examples }: { examples: CodeExampleQuad }) {
+ return (
+
+
+
+
+
+ — Examples
+
+
+ One source of truth,
+
+ four ways to use it.
+
+
+ Your JSON models drive the diff, the SQL, the ORM entities, and the
+ runtime macro. Pick the workflow that fits your team — Vespertide
+ stays consistent.
+
+
+ {[
+ {
+ k: 'Models.',
+ v: 'Inline foreign keys, enums, and constraints — no separate schema language.',
+ },
+ {
+ k: 'CLI.',
+ v: 'diff, sql, revision, status, log — every step is a plain command.',
+ },
+ {
+ k: 'Runtime.',
+ v: 'Compile-time macro, zero overhead, no migrations folder shipped to prod.',
+ },
+ {
+ k: 'Export.',
+ v: 'SeaORM, SQLAlchemy, Django, GORM and more — typed entities, generated.',
+ },
+ ].map((it) => (
+
+
-
- Feature title
-
-
- Lorem ipsum dolor sit amet. Etiam sit amet feugiat turpis.
- Proin nec ante a sem vestibulum sodales non ut ex.{' '}
+ →
+
+
+
+ {it.k}
-
+ {it.v}
+
))}
-
-
-
-
-
-
-
- Title
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- Nullam venenatis ac egestas lacus est nec urna.{' '}
-
-
-
+
+
+
+
+
+
+ )
+}
+
+function CompatibilitySection() {
+ return (
+
+
+
+
+
+
+
+ {DBS.map((db) => (
+
+
-
-
-
-
+ {db.key}
+
-
- {EXAMPLES.map(({ id, title, description }) => (
-
-
-
- {title}
-
-
- {description}
-
-
-
- ))}
-
-
+
+ {db.name}
+
+
+
+ {db.quote}
+
+
+ {db.note}
+
-
-
-
-
+
+
+
+ — ORM export
+
+
+ Generate typed entities for the runtime you use.
+
+
+
+ vespertide export --orm <target>
+ {' '}
+ emits up-to-date entities from your current models.
+
+
+ {ORMS.map((orm) => (
+
+
+ {orm.lang}
+
+ {orm.name}
+
+ ))}
+
+
+
+
+ )
+}
+
+function ChannelRow({
+ href,
+ icon,
+ name,
+ meta,
+}: {
+ href: string
+ icon: 'github' | 'discord' | 'kakao'
+ name: string
+ meta: string
+}) {
+ return (
+
+
+
+
+
+
+ {name}
+
+
-
+
+
+ )
+}
+
+function CommunitySection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Apache-2.0 · v{VERSION} ·{' '}
+
+ crates.io
+
+
+
+ built with Rust · maintained in Seoul
+
+
- >
+
+ )
+}
+
+export default async function HomePage() {
+ const [heroHtml, modelHtml, cliHtml, runtimeHtml, exportHtml] =
+ await Promise.all([
+ highlight(HERO_MODEL_JSON, 'json'),
+ highlight(EXAMPLE_MODEL, 'json'),
+ highlight(EXAMPLE_CLI, 'shell'),
+ highlight(EXAMPLE_RUNTIME, 'rust'),
+ highlight(EXAMPLE_EXPORT, 'shell'),
+ ])
+
+ const examples: CodeExampleQuad = [
+ { key: 'model', label: 'Model', file: 'models/post.json', html: modelHtml },
+ { key: 'cli', label: 'CLI', file: '~/projects/blog', html: cliHtml },
+ { key: 'runtime', label: 'Runtime', file: 'src/main.rs', html: runtimeHtml },
+ {
+ key: 'export',
+ label: 'ORM export',
+ file: '$ vespertide export',
+ html: exportHtml,
+ },
+ ]
+
+ return (
+
+
+
+
+
+
+
+
)
}