Appearance
From an HTML Template
Introduction
Most sites start from a design that already exists as static HTML: a template bought or downloaded, or pages a designer handed over. This page is the way from such a template to a kitloom theme whose every word and picture is edited in the admin — "wiring a template into the CMS".
Two bundled themes were made exactly like this, and are the examples here:
| Theme | From | What the admin edits |
|---|---|---|
| Journal | Start Bootstrap's Clean Blog (MIT) | Only what WordPress already has: posts and pages, featured images, excerpts, the reading settings, menus, a form |
| Bureau | Start Bootstrap's Agency (MIT) | The same, plus a field group on the front page for the landing sections: services, portfolio, timeline, team, logos, contact |
The rest of Writing a Theme still applies; this page is the order to do things in and the decisions along the way.
1. Check the license
The theme's code becomes yours to ship, so the template must allow it:
- MIT, BSD, Apache 2.0 — fine. Keep the copyright notice: put the template's license into a
NOTICE.mdin the theme, and leave the comment at the top of its CSS as it is. - GPL — the theme would have to be GPL too, and it could not be sold with kitloom; do not mix them.
- "Free for personal use", no license at all — not usable.
Photos and illustrations often come under a different license from the code, or none. Leave them out: the pictures should come from the site's media library anyway. Journal and Bureau ship none of their template's photos.
2. Map the pages to templates
Every page of the template becomes a template of the hierarchy. Write the table down first:
| Template page | Journal | Bureau |
|---|---|---|
| The home page | front-page — or the blog itself while the latest posts are at / | front-page: the one-page landing |
| A post | single | single |
| "About", "Contact" | page | page |
| A list of posts | index (the blog page), archive (category, tag, author, date), search | index, archive, search — drawn as portfolio tiles |
| (not in the template) | 404 | 404 |
index is required: every page falls back to it. A template the HTML does not have (search, 404, an author page) is made in the template's style.
3. Map the content to the admin
This is the decision that makes the theme "editable". For every piece of text and every picture, ask where an editor will change it — and prefer the places WordPress already has. A site moved from WordPress then looks right on the first day, and editors find things where they expect them.
| In the template | Where it is edited | Example |
|---|---|---|
| The site's name, the tagline | Settings → General | Navbar brand, the blog's big title |
| The menu | Appearance → Menus, in a location the theme registers or one added on Menu Locations | journal-primary, bureau-primary |
| Social icons | A second menu location with Custom links; the icon follows from the address | journal-social, bureau-social |
| Footer links | A third menu location | bureau-footer |
| A page's title and text | The page | About, Contact |
| The line under a title | The excerpt — written by hand | Journal's subtitles |
| A header photo | The featured image | Journal's mastheads |
| A photo for lists | The featured image of the blog page (Settings → Reading) | Journal's category headers |
| What is the home page, what is the blog | Settings → Reading | Both themes |
| A contact form | Forms (Contact Form 7), its shortcode in a page — or chosen in a field | Journal's Contact page, Bureau's contact section |
| Repeated blocks of a landing page: services, team, timeline, logos | A field group of Custom Fields, shown on the front page | Bureau's "Front page sections" |
Fields are for what WordPress has no place for. Journal needs none; Bureau needs one group, because "three services with an icon each" is not a post, a page or a menu.
4. Make the package
Generate it rather than starting from an empty folder: php artisan kitloom:make-theme Journal (Generating a Theme) writes the package below with every template, a demo command and tests, wired into the application and passing the checks. The steps that follow replace its neutral styles and templates with the template's.
text
journal-theme/
├── composer.json # extra.laravel.themes + extra.kitloom.theme
├── LICENSE.md, NOTICE.md # your license; the template's
├── README.md # what is edited where — the admin's help
├── screenshot.jpg # 800×600, shown on Appearance → Themes
├── src/
│ ├── ThemeServiceProvider.php
│ ├── Site/ # page parts the theme computes
│ ├── Demo/ # the demo content
│ └── Console/ # the demo command
├── database/migrations/ # Bureau: the field group
├── resources/
│ ├── app.js
│ ├── css/clean-blog.css # the template's CSS, as it is
│ ├── css/journal.css # the theme's own additions
│ ├── views/app.blade.php
│ └── js/
│ ├── Layouts/, Components/
│ └── Templates/ # front-page.vue, index.vue, single.vue…
└── tests/The provider names the theme, the engine and the page parts each template gets; the menu locations the theme needs on a fresh site are registered when it boots (others are added on Menu Locations):
php
class ThemeServiceProvider extends BaseThemeServiceProvider
{
protected string $theme = 'Journal';
protected string $engine = 'inertia';
protected array $parts = [
'*' => [SiteInfo::class, Content::class, Masthead::class, Menus::class, Locations::class, ShortcodeData::class, Head::class, Schema::class],
'front-page' => [Posts::class, LatestPosts::class],
'index' => [Posts::class],
'archive' => [Posts::class],
];
protected function bootTheme(): void
{
$this->app->make(PageParts::class)->provide(Masthead::class, MastheadProvider::class);
$this->app->make(MenuLocations::class)->register(['journal-primary' => 'Journal header']);
}
}5. Bring the styles
Take the template's compiled CSS as it is — Bootstrap included — and put the theme's own rules in a second file. Updating the template later is then a file copy. Remove only what you cannot ship: Bureau's copy has the two url(...) photos taken out.
Replace the template's JavaScript with Vue. A Bootstrap template uses its script for a few things, each a few lines in a component:
| Template behaviour | In the theme |
|---|---|
The navbar collapse (data-bs-toggle) | A ref toggling the show class |
The navbar that hides on scroll, or darkens (navbar-shrink) | A scroll listener in the layout |
| Scrollspy | The last section[id] above 40 % of the window lights up its menu item |
| A modal | A component in <Teleport to="body"> with the template's classes, closed by Escape |
| An icon font (Font Awesome) | Inline SVG — Bootstrap Icons are MIT |
No jQuery, no Bootstrap JS, no new npm package.
6. Turn the markup into templates
Copy the template's HTML into the Vue components and replace each piece of sample content with a prop. Every template gets the parts the provider lists: content (the record, term or author), posts (a list), site, menus, menuLocations, shortcodes…
vue
<Masthead variant="post" :image="masthead.image" :title="content.title" :subtitle="masthead.subtitle">
<template #meta>
<PostMeta :author="content.author" :date="content.formattedDate" />
</template>
</Masthead>What the template needs but the site does not give directly is a page part of the theme, computed on the server: Journal's Masthead picks the header photo (the record's, else the blog page's, else the front page's); its LatestPosts lists the newest posts under a static front page; Bureau's Sections turns the front page's fields into what the landing draws. A part is also what a headless frontend gets as JSON.
A few rules:
- Titles and excerpts carry HTML entities (
&). Decode them as text; never put them into the page withv-html. - Links to the site go through Inertia; links elsewhere stay plain. A menu item
/#teamscrolls when the section is on the page and opens the front page at it otherwise — Inertia scrolls to the anchor after a visit. - A page's text may hold shortcodes: kitloom leaves
<div data-shortcode="…">where each one was. The theme draws the ones it knows — the contact form at least — and keeps the HTML around them. - Give every layout a single root element: Inertia passes the page props to the template, and Vue warns about props it cannot place.
7. Fields, when WordPress has no place
Bureau's landing sections are an ACF-style field group, written by a theme migration that runs on activation:
php
// database/migrations/…_add_bureau_front_page_fields.php
public function up(): void
{
FrontPageFields::install(); // GroupWriter::save() of the group, unless it is there
}The group is the array Advanced Custom Fields exports — one tab per section, page_type == front_page as its location — so the admin shows it on that page only, in Filament and in Nova alike. The page part reads the values with $page->field('services'); images, posts and the form come back as ids, and the part turns them into pictures (Images), list entries (Entries) and the form's data.
8. A demo
A {theme}:demo command fills the site with neutral content written the way the admin writes it — PostWriter, TermWriter, MenuEditor, SettingWriter, $page->setFields() — so the theme can be seen before real content exists. Make the pictures yourself (SVG drawn in PHP is enough) rather than shipping photos.
9. Build and check
bash
npm run build:themes # or: APP_THEME=Journal npx vite build
php artisan journal:demoActivate the theme in Appearance → Themes, then go through every template and every admin field once. The bundled themes keep that check as tests: the pages and their props on the demo (tests/ of the theme, run from a skeleton with the theme active), and the admin form of the front page in each admin.