Building with Uniweb
You know how to build a React app. You pick a bundler (Vite), write components, set up routes, and wire everything together. A typical single-page application (SPA) needs a router config, lazy imports, maybe a layout system. In Uniweb, the wiring is the file structure itself — markdown files name the components they want, folders define routes, and the build connects them.
This guide shows what a Uniweb project looks like and why almost everything in it is standard React and Vite. The unfamiliar part is small and mechanical.
The Two Packages
A Uniweb project is a workspace with two packages (or more, as it grows):
my-project/
├── src/ # React components (Vite library build) — the foundation
│ └── sections/
│ └── Hero.jsx
├── site/ # Content + routing (Vite app)
│ └── pages/
│ └── home/
│ └── hero.md
└── pnpm-workspace.yamlFoundation is a standard Vite library project, living in src/ (the site's source code — that's the framing). It builds to a single entry.js bundle plus a meta/schema.json that describes what's inside. Your React code lives here — components, hooks, styles, whatever you need.
Site is a standard Vite app with file-based routing. Each folder in pages/ is a route. Markdown files inside a folder define the sections on that page. The runtime loads the foundation and renders the right component for each section.
Both packages are independent. A foundation can serve multiple sites. A site can swap foundations. You can have several of each in one workspace — or just one of either, depending on what you're building.
The Connection
Here's the complete round trip — a component and the markdown that addresses it.
The component — a bare .jsx file in sections/:
export default function Hero({ content }) {
return (
<div className="py-20 text-center max-w-4xl mx-auto px-6">
<h1 className="text-4xl font-bold">{content.title}</h1>
<p className="text-xl mt-4">{content.paragraphs[0]}</p>
{content.links[0] && (
<a
href={content.links[0].href}
className="inline-block mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg"
>
{content.links[0].label}
</a>
)}
</div>
)
}The markdown that uses it:
---
type: Hero
---
# Welcome to Our Product
Create something great with less effort.
[Get Started](/about)What arrives in content:
| Markdown element | Content field | Value |
|---|---|---|
# Welcome to... | content.title | "Welcome to Our Product" |
| Paragraph text | content.paragraphs[0] | "Create something great..." |
[Get Started](/about) | content.links[0] | { href: "/about", label: "Get Started" } |
The parser extracts semantic elements from markdown — headings become titles, paragraph text becomes paragraphs, links become links, images become images. Your component reads the structure it needs and ignores the rest. See Content Structure for the full mapping.
The component is a regular React function. It receives content (parsed markdown), params (configuration from meta.js, if you add one), and block (the runtime object for this section). For a first component, content is all you need.
That's Component Content Architecture
Uniweb calls this pattern Component Content Architecture (CCA) — components and content are separate artifacts connected by convention, not code. The architecture is the file structure. There's no component registry to maintain. No router config to update. No mapping between URLs and components.
sections/is the component catalog. The build discovers every.jsxfile and folder at the root automatically. A file namedHero.jsxbecomes a section type called "Hero" — content authors reference it withtype: Heroin frontmatter.pages/is the routing table. Each folder is a route.pages/home/→/,pages/about/→/about,pages/blog/→/blog.Frontmatter is the connection.
type: Heroin a markdown file tells the runtime which component to render. That's the entire binding mechanism.
Adding a component means dropping a file in sections/. Adding a page means creating a folder in pages/. No configuration files to update, no imports to add.
The build scans src/sections/ for section types. If you need additional search paths, configure them via the sections option in defineFoundationConfig() — those extra paths require meta.js for addressability.
What You Get
Without extra configuration, a Uniweb project gives you:
- File-based routing — folder = route, no
react-routerconfig needed - Hot reload on both content and component changes
- Pre-rendered static HTML for every page, with client-side hydration
- Localization — locale detection, locale-aware URLs, translation extraction and matching
- Parametric pages — a
[slug]folder becomes one page per record of a query - Records — markdown, YAML or JSON files in
records/become structured data, with automatic i18n support - Media processing — image optimization, video poster generation, PDF preview thumbnails
- Search indexing — content is indexed at build time for client-side search
- Section backgrounds — image, video, gradient, and color backgrounds rendered by the engine (components don't repeat this logic)
- Semantic theming — CSS tokens that resolve per-section based on context, controlled by the site
In a plain Vite + React app, you'd wire each of these yourself: install a router, write route definitions, set up lazy imports, build an SSG pipeline, add an i18n library, write slug-based route expansion, set up image processing, integrate a search engine. Uniweb handles all of this through project structure and build conventions — you opt into features by using them, not by configuring them.
You still write React. You still use Tailwind (or CSS, or whatever you prefer). You still structure your components however makes sense. The framework handles the infrastructure. Your job is the components.
Growing From Here
Everything below is opt-in. A foundation that's just bare files in sections/ with no meta.js, no params, and no theming is a valid foundation. Each addition solves a specific problem when you're ready for it.
More section types
Add more files to sections/. Each .jsx file or folder at the root becomes a section type:
src/sections/
├── Hero.jsx
├── Features.jsx
├── Testimonial.jsx
├── CallToAction.jsx
├── Header.jsx
└── Footer.jsxContent authors can now use type: Hero, type: Features, type: Testimonial, and so on. Header and Footer go in the site's layout/header.md and layout/footer.md to render on every page.
Configuration with params
When a component needs options — layout style, column count, whether to show an image — promote the bare file to a folder and add meta.js:
src/sections/
└── Hero/
├── meta.js
└── Hero.jsx// meta.js
export default {
params: {
layout: {
type: 'select',
label: 'Layout',
options: ['centered', 'split'],
default: 'centered',
},
},
}// Hero.jsx
export default function Hero({ content, params }) {
if (params.layout === 'split') {
return (
<div className="grid md:grid-cols-2 gap-12 items-center py-20 px-6">
<div>
<h1 className="text-4xl font-bold">{content.title}</h1>
<p className="text-xl mt-4">{content.paragraphs[0]}</p>
</div>
{content.images[0] && (
<img
src={content.images[0].src}
alt={content.images[0].alt}
className="rounded-lg"
/>
)}
</div>
)
}
return (
<div className="py-20 text-center max-w-4xl mx-auto px-6">
<h1 className="text-4xl font-bold">{content.title}</h1>
<p className="text-xl mt-4">{content.paragraphs[0]}</p>
</div>
)
}Content authors set it in frontmatter:
---
type: Hero
layout: split
---Params force you to ask "what actually varies in this component?" — and that question produces a tighter interface. It's the same discipline as writing testable code: the constraint improves the design.
Content-driven components
The progression from hardcoded to content-driven is natural. You start with a component that has its text in JSX. When that text needs to change without a code deploy, you move it to markdown and read from content. The Converting Existing Designs guide walks through this step by step.
Theme
A foundation declares what's customizable. A site sets the values. CSS variables bridge them:
// src/main.js
export default {
vars: {
'header-height': { default: '4rem' },
'max-width': { default: '80rem' },
},
}# site/theme.yml
vars:
header-height: 5remComponents use var(--header-height) and adapt automatically. The Thinking in Contexts guide covers the full theming system — semantic tokens, context classes, and how components adapt to light/dark/medium contexts without hardcoding colors.
Site-aware components with kit
When a component needs site context — navigation, locale, routing — you import hooks from @uniweb/kit:
import { useWebsite, useActiveRoute } from '@uniweb/kit'
function Header({ content }) {
const { website } = useWebsite()
const { isActiveOrAncestor } = useActiveRoute()
const pages = website.getPageHierarchy({ for: 'header' })
return (
<header>
<span>{website.name}</span>
<nav>
{pages.map(page => (
<a
key={page.id}
href={page.route}
className={isActiveOrAncestor(page) ? 'active' : ''}
>
{page.label}
</a>
))}
</nav>
</header>
)
}Kit provides hooks (useWebsite, useActiveRoute, useVersion, useAppearance), primitive components (Link, Image, Icon), and utilities (getLocaleLabel). The data classes underneath — Website, Page, Block from @uniweb/core — are what hooks return. You access them through kit, not by importing core directly. See Kit Reference for the full API.
Variants and the Front Desk
When one section type needs multiple layouts (a homepage hero vs. a pricing hero), the Front Desk pattern keeps a single section type with a variant param that delegates to different renderers. One component in the author's palette, multiple implementations underneath. See Component Patterns for the full pattern.
See Also
- Quickstart — Step-by-step project creation (
uniweb create) - Content Structure — How markdown becomes
content.title,content.items, etc. - Component Metadata — Full
meta.jsreference (params, content expectations, presets) - Converting Existing Designs — Bringing existing React code into a foundation
- Component Patterns — Front Desk, Building Blocks, and other design patterns
- Thinking in Contexts — Semantic theming and how components adapt to any brand
- Project Structures — Workspace layouts for multi-site, extension, and co-located projects