Search pages, headings and code across the docs, examples and tutorials.

to navigateto open

Rayleigh

A minimalistic portfolio with a gallery, an About page, a blog and a contact form, edited in a CMS.

The home page with the interior design galleryThe home page with the interior design gallery

What it shows

The parts of ohne it puts to work.

Each card opens the guide that explains it.

  • CMS

    The CMS

    Install the layer, map paths to collections, and edit pages live.

  • CMS

    Your website

    Read pages with @ohnejs/client and join the live preview.

  • CMS

    Frameworks

    The whole loop in Nuxt, Next, React, and plain HTML.

  • Database

    Blocks

    Ordered lists of mixed, reusable shapes.

  • Database

    Collections and fields

    Your data model, and the built-in field types it is made of.

  • Database

    Rich text

    Formatted text with links to addresses and records.

  • Dashboard

    Field layouts

    Rows, cards, tabs, and rules for every form the dashboard renders.

  • Uploads

    Uploads

    Install the layer, configure storage, upload and serve files.

  • Project

    Boot files

    Code that runs before the server opens.

  • Project

    Monorepo

    Your ohne app and your Nuxt or Next website in one repository.

  • HTTP API

    The collections API

    REST endpoints a collection opts into.

  • HTTP API

    Middleware

    Code that runs before the handler on every matching request.

A blog post with its image and textA blog post with its image and text

Get it

Clone it and run it.

You need Node 26 and pnpm. The dashboard opens on 9000.

Terminal
git clone https://github.com/ohnejs/rayleigh-nuxt rayleigh
cd rayleigh
pnpm install
pnpm dev

Inside

The files that do the work.

Exactly as the repository holds them.

apps/web/app/pages/[...slug].vue
<script setup lang="ts">
// The `ohne` middleware asks the CMS what this path shows, once, before the page renders.
// A redirect or a missing page answers with its own status code, so this page only renders content.
definePageMeta({ middleware: 'ohne' });

// Each block renders its own heading, so a page adds no `<h1>` of its own.
// A path no page answers shows your CMS page with the slug `404`, when you create one.
// `footerBorder` is a switch on each page in the dashboard, which draws a line above the footer.
const page = useOhnePage();

// `cms.routes` in the backend shows each post at `/blog/<slug>`, so a post renders here too.
// It links back to the page an editor picked as the blog in `Settings`.
const settings = await useOhneRead('Settings');
</script>

<template>
  <main
    v-if="page?.kind === 'page' && page.collection === 'Pages'"
    class="main"
    :class="{ 'main--bordered': page.record.footerBorder }"
  >
    <OhneBlocks :blocks="page.record.content" />
  </main>
  <main
    v-else-if="page?.kind === 'page' && page.collection === 'Posts'"
    class="main main--bordered main--post"
  >
    <Container>
      <header class="post-header">
        <BackButton :link="settings.blogPage">Blog</BackButton>
        <h1>{{ page.record.headline || page.record.title }}</h1>
        <WrittenOn :date="page.record.publishedAt" :author="page.record.author" />
      </header>
    </Container>
    <OhneBlocks :blocks="page.record.content" />
  </main>
  <main
    v-else-if="page?.kind === 'notFound' && page.page"
    class="main"
    :class="{ 'main--bordered': page.page.footerBorder }"
  >
    <OhneBlocks :blocks="page.page.content" />
  </main>
</template>

README

From the repository.

README.md

Tutorial

Build this site yourself, lesson by lesson, at ohne.dev/tutorials/rayleigh/nuxt.

The same site in Next lives at ohnejs/rayleigh-next. Its apps/backend is identical to this one.

What it shows

  • Pages built from blocks. apps/backend/blocks/ holds PageIntro, Portfolio, Image, Prose, Posts and ContactForm, and apps/web/app/components/blocks/ one component for each. See blocks.
  • A blog with its own route. cms.routes in apps/backend/ohne.config.ts shows a post at /blog/<slug> and every other path as a page. One catch-all page, apps/web/app/pages/[...slug].vue, renders both. See routes.
  • A public relation. Each post names its author from Authors, a collection anyone may read, so the website receives the whole author. See one reference.
  • Site settings. Settings is a singleton with the logo, the menu and the footer links, which the header and the footer read with useOhneRead. See site data and singletons.
  • A paginated list. The Posts block lists three posts to a page with useOhneList, and answers 404 past the last page. See Nuxt.
  • A contact form. Messages lets any visitor create a message and nobody but an editor read one. The form sends it with useOhne().create, in apps/web/app/components/blocks/ContactForm.vue, and shows the CMS's own message under each invalid field. See forms.
  • An optional spam check. Turn on reCAPTCHA v3 in Settings, and apps/backend/middleware/recaptcha.ts asks Google about each message before it is stored. The secret key is readable: false, so no read returns it. See middleware and write-only fields.
  • Rich text. The Prose block's editor offers only what the design has styles for. See rich text.
  • Uploads in a seed. apps/backend/boot/seed.ts uploads every image with putUpload, as an editor would. See uploads in server code and boot files.
  • The live editor. The dashboard shows the website beside the form, and updates it as you type. See the live editor.
  • No UI library. Dark mode follows the system until you pick a theme, with no flash on load (apps/web/app/utils/theme.ts). The full-screen gallery is a native <dialog> (apps/web/app/components/Slider.vue), and the mobile menu a little state in apps/web/app/composables/mobile-menu.ts.
  • Plain CSS. apps/web/app/assets/styles/ holds Rayleigh's colors, fonts and sizes as custom properties, then one stylesheet per part of the page. The fonts are self-hosted in apps/web/public/fonts/.
  • One monorepo. apps/backend and apps/web share one pnpm workspace. See monorepo.
  • Tests. apps/backend/tests/ boots the API in the test process and checks what the seed serves, who may send and read a message, and that the secret key stays secret.

Getting started

In a copy of this repository:

Terminal
pnpm install
pnpm dev

The first pnpm dev fills the CMS with the demo and prints the admin's password. Sign in to the dashboard as admin@example.com with it.

The seed fills an empty site only, and never under NODE_ENV=production. Delete apps/backend/.data/ and apps/backend/.uploads/ to start over.

The checks:

Terminal
pnpm typecheck
pnpm test
pnpm lint
pnpm format --check

Upgrading

Bump the versions in the catalog of pnpm-workspace.yaml and follow the framework's release notes. To pick up later changes to this example, compare your copy with this repository.

Deploy

Each app deploys on its own. See deploying.

  • The backend deploys like any ohne app. Set SITE_URL to the website's public address.
  • The website builds with pnpm build and runs as node apps/web/.output/server/index.mjs, with NUXT_OHNE_API set to your API.

The seed stays out of production, so a new install starts empty. The first visit to the dashboard opens the install page, where you create the admin.

Licence and credits

  • MIT, see LICENSE.
  • The images come from the Rayleigh template by Muris Ceman, recompressed. See apps/backend/seed/CREDITS.md.
  • Lato and Poppins are under the SIL Open Font License. See apps/web/public/fonts/.