Skip to content
Preset · built on fullstack

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 site

First 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.

Platforms
site: webssrapi: server
Packages
routerloggerqueryformstailwindthemei18nstorageanalyticstestingseoapivite
Components
buttoncardformiconinputlabelpage-headertheme-toggle

Every file it writes

Start with app/pages/home.tsx. Pick any file to read it.

Source
app/pages/home.tsx
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>
  )
}

Other kits

All kits →