diff --git a/apps/web/content/docs/dev/i18n/index.mdx b/apps/web/content/docs/dev/i18n/index.mdx
index 3a5f719da..69f5598cc 100644
--- a/apps/web/content/docs/dev/i18n/index.mdx
+++ b/apps/web/content/docs/dev/i18n/index.mdx
@@ -163,6 +163,29 @@ export const vitNodeApiConfig = buildApiConfig({
---
+### Your app's own strings
+
+Copy that belongs to no package - your landing page, say - gets a namespace of its own. Pick an id no package uses, put the tree under it, and register it like any other file:
+
+```json title="apps/web/src/locales/site/en.json"
+{
+ "site": {
+ "home": { "title": "A home for your people" }
+ }
+}
+```
+
+```ts title="apps/web/src/locales/app.ts"
+export const appMessages: AppMessagesMap = {
+ en: { site: async () => await import('./site/en.json') },
+ pl: { site: async () => await import('./site/pl.json') },
+}
+```
+
+`i18n:check` treats it like a package: your default-locale file is the source of truth, and every other language is checked against it for missing and leftover keys. No default-locale file at all is an error - there would be nothing to translate from.
+
+---
+
## Translation Architecture
| Source | Role | Order |
diff --git a/apps/web/src/locales/app.ts b/apps/web/src/locales/app.ts
index edd67ca5a..c4961d1a0 100644
--- a/apps/web/src/locales/app.ts
+++ b/apps/web/src/locales/app.ts
@@ -10,8 +10,12 @@ import type { AppMessagesMap } from '@vitnode/core/lib/i18n/types'
* API registers the same file through `vitnode.api.config.ts`.
*/
export const appMessages: AppMessagesMap = {
+ en: {
+ site: async () => await import('./site/en.json'),
+ },
pl: {
'@vitnode/blog': async () => await import('./@vitnode/blog/pl.json'),
'@vitnode/core': async () => await import('./@vitnode/core/pl.json'),
+ site: async () => await import('./site/pl.json'),
},
}
diff --git a/apps/web/src/locales/site/en.json b/apps/web/src/locales/site/en.json
new file mode 100644
index 000000000..0d468d065
--- /dev/null
+++ b/apps/web/src/locales/site/en.json
@@ -0,0 +1,24 @@
+{
+ "site": {
+ "home": {
+ "hero": {
+ "canary": "VitNode 2.0 is an early canary",
+ "description": "Publish content, bring members together and build the features your community needs on an open-source platform you control.",
+ "explore": "Explore what you can build",
+ "preview": {
+ "alt": "The AdminCP dashboard with a sidebar listing Core, Blog and Example plugin sections, a private notes widget and a send-notification widget.",
+ "members": "{count, plural, one {# member} other {# members}}",
+ "toast_meta": "Live toast · just now",
+ "toast_title": "Welcome aboard, {name}!"
+ },
+ "see_product": "See the product",
+ "title": "A home for your people, built around your idea.",
+ "trust": {
+ "agents": "Made for AI coding agents",
+ "license": "MIT licence",
+ "open_source": "Open source"
+ }
+ }
+ }
+ }
+}
diff --git a/apps/web/src/locales/site/pl.json b/apps/web/src/locales/site/pl.json
new file mode 100644
index 000000000..e3970df81
--- /dev/null
+++ b/apps/web/src/locales/site/pl.json
@@ -0,0 +1,24 @@
+{
+ "site": {
+ "home": {
+ "hero": {
+ "canary": "VitNode 2.0 to wczesna wersja canary",
+ "description": "Publikuj treści, łącz ludzi i twórz funkcje, których potrzebuje Twoja społeczność, na platformie open source, nad którą masz pełną kontrolę.",
+ "explore": "Odkryj, co możesz zbudować",
+ "preview": {
+ "alt": "Pulpit AdminCP z paskiem bocznym z sekcjami Core, Blog i wtyczki Example, widżetem prywatnych notatek i widżetem wysyłania powiadomień.",
+ "members": "{count, plural, one {# członek} few {# członkowie} many {# członków} other {# członka}}",
+ "toast_meta": "Komunikat na żywo · przed chwilą",
+ "toast_title": "Witaj na pokładzie, {name}!"
+ },
+ "see_product": "Zobacz produkt",
+ "title": "Dom dla Twojej społeczności, zbudowany wokół Twojego pomysłu.",
+ "trust": {
+ "agents": "Stworzony dla agentów AI do kodowania",
+ "license": "Licencja MIT",
+ "open_source": "Open source"
+ }
+ }
+ }
+ }
+}
diff --git a/apps/web/src/routes/_main/index.tsx b/apps/web/src/routes/_main/index.tsx
index ea2fccaf2..e4c3d7673 100644
--- a/apps/web/src/routes/_main/index.tsx
+++ b/apps/web/src/routes/_main/index.tsx
@@ -1,9 +1,20 @@
import { createFileRoute } from '@tanstack/react-router'
+import { intlQueryOptions } from '@vitnode/core/tanstack/i18n'
import { HomeRouteContent } from '@/site/home/home-content'
+import { HOME_NAMESPACES } from '@/site/home/namespaces'
import { MARKETING_PAGES, marketingHead } from '@/site/marketing/metadata'
export const Route = createFileRoute('/_main/')({
+ loader: async ({ context }) => {
+ await context.queryClient.query({
+ ...intlQueryOptions({
+ locale: context.locale,
+ namespaces: HOME_NAMESPACES,
+ }),
+ staleTime: 'static',
+ })
+ },
head: () => marketingHead(MARKETING_PAGES.home),
component: HomeRouteContent,
})
diff --git a/apps/web/src/site/home/home-content.tsx b/apps/web/src/site/home/home-content.tsx
index b8cb33deb..c97f3bb4d 100644
--- a/apps/web/src/site/home/home-content.tsx
+++ b/apps/web/src/site/home/home-content.tsx
@@ -1,3 +1,6 @@
+import { RouteMessages } from '@vitnode/core/tanstack/i18n'
+
+import { HOME_NAMESPACES } from '@/site/home/namespaces'
import { AgentsSection } from '@/site/home/sections/agents'
import { CommunitySection } from '@/site/home/sections/community'
import { DevelopersSection } from '@/site/home/sections/developers'
@@ -5,7 +8,7 @@ import { FaqSection } from '@/site/home/sections/faq'
import { FeaturesBentoSection } from '@/site/home/sections/features-bento'
import { FinalCtaSection } from '@/site/home/sections/final-cta'
import { HeroSection } from '@/site/home/sections/hero'
-// import { MakerSection } from '@/site/home/sections/maker'
+import { IntegrationsSection } from '@/site/home/sections/integrations'
import { OutcomesSection } from '@/site/home/sections/outcomes'
import { PluginsSection } from '@/site/home/sections/plugins'
import { PoweringBySection } from '@/site/home/sections/powering-by'
@@ -16,23 +19,25 @@ import { MarketingPage } from '@/site/marketing/marketing-page'
import { CanaryNotice } from '@/site/marketing/shared'
export const HomeRouteContent = () => (
-
-
-
-
+ )
+}
diff --git a/apps/web/src/site/home/illustrations/hero-strands.tsx b/apps/web/src/site/home/illustrations/hero-strands.tsx
new file mode 100644
index 000000000..7ff6e22fc
--- /dev/null
+++ b/apps/web/src/site/home/illustrations/hero-strands.tsx
@@ -0,0 +1,69 @@
+import { cn } from 'cn'
+
+const STRANDS = Array.from({ length: 22 }, (_, index) => {
+ const t = index / 21
+
+ return `M-60 ${250 + t * 70} C 380 ${40 + t * 170}, 1040 ${440 - t * 150}, 1500 ${170 + t * 90}`
+})
+
+const PULSES = [3, 9, 14, 19]
+
+const timing = (delay: number, duration: number): React.CSSProperties => ({
+ animationDelay: `${delay}s`,
+ animationDuration: `${duration}s`,
+})
+
+export const HeroStrands = ({ className }: { className?: string }) => (
+
+)
diff --git a/apps/web/src/site/home/namespaces.ts b/apps/web/src/site/home/namespaces.ts
new file mode 100644
index 000000000..b9e5a62db
--- /dev/null
+++ b/apps/web/src/site/home/namespaces.ts
@@ -0,0 +1 @@
+export const HOME_NAMESPACES = ['core.global', 'site.home.hero'] as const
diff --git a/apps/web/src/site/home/sections/hero.tsx b/apps/web/src/site/home/sections/hero.tsx
index a9f5ab173..6c8e7c752 100644
--- a/apps/web/src/site/home/sections/hero.tsx
+++ b/apps/web/src/site/home/sections/hero.tsx
@@ -1,67 +1,88 @@
import { Link } from '@tanstack/react-router'
-import { ArrowRight, Bot, Scale, Server } from 'lucide-react'
+import { buttonVariants } from '@vitnode/core/components/ui/button'
+import { cn } from 'cn'
+import { ArrowRight, Bird, Bot, CodeXml, Scale } from 'lucide-react'
+import { useTranslations } from 'use-intl'
-import { HeroOrbit } from '@/site/home/illustrations/hero-orbit'
-import { MarketingActions } from '@/site/marketing/shared'
+import { HeroIconField } from '@/site/home/illustrations/hero-icon-field'
+import { HeroPreview } from '@/site/home/illustrations/hero-preview'
+import { HeroStrands } from '@/site/home/illustrations/hero-strands'
+import { usePointerParallax } from '@/site/home/use-pointer-parallax'
+import { BUTTON } from '@/site/marketing/shared'
const TRUST = [
- { Icon: Scale, label: 'MIT licence, free forever' },
- { Icon: Server, label: 'Self-host or bring your cloud' },
- { Icon: Bot, label: 'Made for AI coding agents' },
-]
+ { Icon: CodeXml, key: 'open_source' },
+ { Icon: Scale, key: 'license' },
+ { Icon: Bot, key: 'agents' },
+] as const
-export const HeroSection = () => (
-
-
-
+export const HeroSection = () => {
+ const t = useTranslations('site.home.hero')
+ const { handlers, targetRef } = usePointerParallax()
-
-
-
-
-
-
-
- VitNode 2.0 Canary · a very early build
-
-
+ return (
+
+
+
-
- Build the community your people{' '}
- deserve.
-
+
+
+
+
+ {t('canary')}
+
-
- VitNode is a free, open-source community framework. Members, roles,
- content, real-time updates, search and an Admin Control Panel come in
- the box. Your ideas go on top. Plugins hold it all together.
-
- Hey, I’m Maciej. I got tired of rebuilding the same admin panel.
-
-
-
- I have spent years building community platforms, and every single
- one started the same way: a login form, a users table, a roles
- screen, a permissions system, translations, and three weeks of glue
- before anyone saw a feature. At some point you either accept it or
- you fix it.
-
-
- VitNode is me fixing it. Open source, opinionated in the useful
- places, and built so the interesting work happens in plugins. It is
- early and it is honest about that. If you build communities for a
- living, or you just want yours to be truly yours, I would love your
- bug reports, your feedback and your weird plugin ideas.
-