Site kit
Start a marketing site: server-rendered, prerendered, SEO heads, i18n, a blog and a newsletter form.
For: Teams launching a marketing site that will grow forms, a blog and more than one language.
kitr new my-app --kit siteFirst time? Run npm i -g @kitr/kitr once. Inside a project, the global kitr runs your project's own version.
- kitr dev
- The site and its API
- kitr build
- Prerenders every page into
dist/site/client - npm test
- Vitest, starting with the newsletter test
Prerendered pages
Home, pricing and a blog, written to HTML at build time and listed in config/seo.ts.
Two languages
English and German in app/locales/, with a locale switch kept in a cookie.
A newsletter form
The form posts to server/controllers/newsletter-controller.ts, and a test covers it.
Ready to share
Page titles, share tags and analytics are configured in config/.
What's inside
Read from the kit's output in the kitr repository, so it matches what kitr new writes today. 13 packages · 8 components · 58 files.
Every file it writes
Start with app/pages/home.tsx. Pick any file to read it.
import { useSchemaForm } from '@kitr/forms'
import { useTranslation } from '@kitr/i18n'
import { useMutation } from '@kitr/query'
import { mutationFrom } from '@kitr/query'
import { Link } from '@kitr/router'
import { useState } from 'react'
import { api } from '#app/api/client'
import { subscribeSchema } from '#app/validators/newsletter'
import { buttonVariants } from '#components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '#components/ui/card'
import { FieldInput, Form, FormField, FormMessage, SubmitButton } from '#components/ui/form'
const subscribe = mutationFrom(api.newsletter.subscribe.mutation())
export default function HomePage() {
const { t } = useTranslation()
const features = ['ssr', 'i18n', 'api'] as const
return (
<main className="mx-auto max-w-5xl px-4">
<section className="py-20 text-center">
<h1 className="mx-auto max-w-2xl text-4xl font-bold tracking-tight sm:text-5xl">{t('hero.title')}</h1>
<p className="mx-auto mt-4 max-w-xl text-lg text-muted-foreground">{t('hero.subtitle')}</p>
<div className="mt-8 flex justify-center gap-3">
<Link to="/blog" className={buttonVariants({ size: 'lg' })}>
{t('hero.cta')}
</Link>
<Link to="/pricing" className={buttonVariants({ size: 'lg', variant: 'outline' })}>
{t('hero.secondary')}
</Link>
</div>
</section>
<section className="grid gap-4 sm:grid-cols-3">
{features.map((key) => (
<Card key={key}>
<CardHeader>
<CardTitle>{t('features.' + key + '.title')}</CardTitle>
<CardDescription>{t('features.' + key + '.body')}</CardDescription>
</CardHeader>
</Card>
))}
</section>
<Newsletter />
</main>
)
}
function Newsletter() {
const { t } = useTranslation()
const mutation = useMutation(subscribe)
const [done, setDone] = useState(false)
const form = useSchemaForm(subscribeSchema, { defaultValues: { email: '' } })
return (
<section className="mt-16">
<Card className="mx-auto max-w-md">
<CardHeader>
<CardTitle>{t('newsletter.title')}</CardTitle>
</CardHeader>
<CardContent>
{done ? (
<p role="status" className="text-sm text-muted-foreground">
{t('newsletter.success')}
</p>
) : (
<Form
form={form}
className="flex flex-col gap-3"
onSubmit={async (values) => {
await mutation.mutateAsync({ body: values })
setDone(true)
}}
>
<FormMessage />
<FormField name="email" label="Email">
<FieldInput type="email" placeholder={t('newsletter.placeholder')} autoComplete="email" />
</FormField>
<SubmitButton pendingText="…">{t('newsletter.button')}</SubmitButton>
</Form>
)}
</CardContent>
</Card>
</section>
)
}