Customization Guide
Tutor is built to be rebranded and re-themed without touching code. Almost everything below is done from the admin Theme Options panel (/admin/settings/theme); changes persist to the database and uploaded assets go to Cloudflare R2, so no rebuild is needed. This guide also points to where each setting lives in code when you want to go deeper.
Theming & Appearance
Path: /admin/settings/theme
Theme settings are saved through GET/PUT /api/v1/admin/theme-settings and applied to the storefront at runtime. The public site is wrapped in a .site-theme element so storefront theming never affects the admin UI.
Colors
Brand colors are CSS variables defined in src/app/globals.css and overridden per-token from the admin panel. The token map lives in src/lib/theme-tokens.ts; each token has a CSS variable, a settings key, and light/dark defaults:
| Token (settings key) | CSS variable | Light default | Dark default |
|---|---|---|---|
primary_color |
--primary |
#CD4323 (terracotta) |
#E8734F |
primary_fg |
--primary-foreground |
#FFFFFF |
#2A1206 |
secondary_color |
--secondary |
#334155 |
#1E293B |
accent_color |
--accent |
#F7D9CF |
#7C2D12 |
text_color |
--foreground |
#1E293B |
#E2E8F0 |
bg_color |
--background |
#FFFFFF |
#0F172A |
border_color |
--border |
#E2E8F0 |
#334155 |
success_color |
--success |
#16A34A |
#22C55E |
warning_color |
--warning |
#F59E0B |
#FBBF24 |
destructive_color |
--destructive |
#DC2626 |
#EF4444 |
Each token has a dark variant (settings key suffixed _dark). Dark mode is scoped to .site-theme.dark. Change a token once and every component that consumes the variable updates — never hardcode hex values in components.
Typography
Pick a heading font and a body font (Raleway, Lexend, Inter, Poppins, Roboto, Playfair Display, Montserrat, Lato, Open Sans) and a base font size (14–18px). Selections are applied at runtime.
Layout & surfaces
The panel exposes tabs for Appearance (theme mode light/dark/system, gradient surfaces for banner, hero, listing detail, footer, CTA, auth), Header (layout, logo/nav alignment, colors, sticky/transparent, search), Menu/Navigation (active color, mobile menu, megamenu mode, pagination type, listing page size), Footer (background, content background, text/link colors, social icons, back-to-top, copyright), Homepage (section order, stats, intro block), and Hero Slider display (width, height, autoplay, pagination, arrows).
Brand & Site Settings
Path: /admin/settings/theme → General tab
| Field | Purpose |
|---|---|
business_name |
Display name across the site |
legal_name |
Registered company name |
business_description |
Used in meta description / SEO |
website |
Main website link |
logo |
Horizontal logo (transparency preserved) |
app_icon |
Square 1:1 app icon |
favicon |
Browser-tab icon |
placeholder_image |
Fallback when an image is missing |
app_status |
active / inactive / coming_soon |
maintenance_mode |
Show a maintenance page to visitors |
Host commission and payout defaults (revenue model, default commission %, minimum payout, payout schedule) live under Settings → Monetization (/admin/settings/monetization).
Images
The Images tab controls the image pipeline used everywhere uploads happen (admin upload → sharp resize/convert → R2):
img_format— WebP / JPG / PNGimg_quality— 1–100 (default 82)img_delete_original— keep or discard the original- Per-type dimensions (
WIDTHxHEIGHT, comma-separated) for avatar, listing thumbnail, listing cover, category, cover photo, blog, logo, app icon, and partner logo.
Navigation menus
Header and footer menus are managed separately from theme colors via the MenuBuilder in the theme page (stored in a menu_items table; nestable via parent_id), edited through /api/v1/admin/menu-items.
Custom code
Inject header_scripts (into <head>), footer_scripts (before </body>), and custom_css for one-off overrides without editing source.
Editing Storefront Content (CMS)
Each storefront surface is editable from its own admin page. Content lives in dedicated tables and is rendered on the public site.
| Content | Admin page | Editable fields |
|---|---|---|
| Hero slider | /admin/hero-slider |
eyebrow, title + highlight, subtitle, desktop/tablet/mobile images, primary & secondary CTAs, sort order, active |
| Pages | /admin/pages |
slug, title, subtitle, markdown content, image, banner, tags, SEO, per-language translations |
| Theme Builder | /admin/theme-builder |
header, footer, and menu templates for the active theme (block builder) |
| About | /admin/about |
header, hero overlay, Our Story, values, team, journey timeline, stats, SEO |
| Why Choose Us | /admin/why-choose-us |
section title/subtitle, main image, icon/image + title + description items |
| FAQs | /admin/faqs |
category, question, answer, sort order, active |
| Gallery | /admin/gallery |
title, type (image/video), category, tags, description, multi-image, SEO |
| Testimonials | /admin/testimonials |
name, location, rating, message, photo, listing reference, sort order, active |
| Partners | /admin/partners |
name, logo, website URL, sort order, active |
| Blog | /admin/blogs (+ /categories) |
title, slug, category, author, excerpt, markdown content, image, tags, published |
Long-form fields (page content, blog posts, FAQ answers) accept Markdown; image fields run through the optimization pipeline described above.
Message & Notification Templates
Path: /admin/settings/notifications
Transactional templates are managed per channel — Email, SMS, and WhatsApp. A template has a name, category, channel, and (for email) subject, preheader, and from fields, plus a body. Bodies use {{token}} placeholders that are substituted at send time. Commonly available tokens include:
{{first_name}}, {{last_name}}, {{email}}, {{otp}}, {{booking_reference}}, {{amount}}, {{currency}}, {{class_name}}, {{session_date}}, {{dashboard_url}}, {{reset_link}}, {{verify_link}}.
Email bodies accept HTML; WhatsApp bodies use WhatsApp markdown (*bold*, _italic_). The editor offers a live preview with sample data, and broadcasts can target all users, a role, or specific recipients. Booking events — confirmation, cancellation, refund, and payout notifications — send through these templates across Email, SMS, and WhatsApp.
Vertical Packs — Changing What the Site Sells
Tutor ships one booking engine and a vertical pack. A pack is the master data, vocabulary, listing fields, browse facets, booking style and demo catalogue for one kind of business. This install runs the Tutoring & Education pack. Switching pack in Admin → Master Data → Vertical packs turns the booking marketplace into a completely different business — a dance studio or a studio-hire site, for example — without touching code.
The two booking mechanics
| Mode | Used by | How availability works |
|---|---|---|
seat |
Cooking, dance, art, kids, outdoor, adventure, local, spiritual, events | The host publishes dated sittings with a seat count. Guests buy seats. |
slot |
Tutoring, fitness, music, photography, languages, beauty, rentals, healthcare, professional, pets | The host publishes weekly hours and a slot length. The engine generates appointments. |
Tutoring & Education runs in slot mode: tutors publish weekly availability and a
lesson length, and the engine generates bookable lesson times from that schedule.
Slot listings never store thousands of empty rows: availability is expanded from
availability_rules when someone looks, and a slot becomes a real row only when it is
booked. Tutors set their hours under Host → Calendar → Weekly hours, and the allowed
lesson lengths come from the pack, narrowable in Admin → Settings → Booking Payments.
Rules store local wall-clock time plus the tutor's timezone, never UTC — otherwise a tutor's 09:00 would move twice a year at the daylight-saving boundaries.
Writing your own pack
Create src/product/verticals/<key>/pack.ts exporting a VerticalPack, then add it to
the array in src/product/verticals/index.ts. That is the whole integration.
export const myPack: VerticalPack = {
key: "detailing", name: "Car Detailing", tagline: "…", icon: "Car",
terms: { listing: { one: "Service", many: "Services" }, host: { one: "Detailer", … } },
masterKinds: [ /* your own lookup tables */ ],
categories: [ { name: "Interior Valet" }, … ], // seeded as the `category` kind
fields: [ ...SHARED_LISTING_FIELDS, /* yours */ ],
filters: [ ...SHARED_FILTERS, /* yours */ ],
booking: { mode: "slot", slotMinutes: [60, 120, 240], … },
demo: { listings: [ … ], reviewSnippets: [ … ], homeCopy: { … } },
};
Three rules keep additional packs from becoming forks:
- A pack is data. It must not import from
@/libor@/app, and the engine must never gain anif (pack === "…"). If a vertical needs behaviour the contract can't express, extend the contract. - Packs never add columns. Vertical-specific values go in
listings.attributes(JSONB, GIN-indexed) viasource: "attribute". Only the pre-existinglistingscolumns usesource: "column". - Say where a facet's data lives. A
FilterSpecwith only akindcounts through thelisting_master_itemslink table; one withfield+sourcereads a column or an attribute. Point a facet at the wrong storage and every option counts zero, which reads to a visitor as "nothing matches" rather than "misconfigured".
Demo content
There is one seeder for every pack — npm run vertical:demo -- [pack]. It creates the
shared five hosts and five customers (identical across packs; only their title changes),
the pack's listings, availability in whichever mechanic the pack uses, bookings with
payments, and reviews. Demo rows are tagged with a demo- slug prefix and the
@tutordemo.com email domain, so re-running replaces rather than duplicates and a
teardown can never take a real account with it.
Imagery is real. Each pack declares demo.photoQueries and demo.videoQueries, and
the seeder pulls matching photography and video from Pexels — the tutoring pack gets
classrooms and one-to-one lessons, a pet pack gets grooming salons. Every item carries
the photographer's credit and the original alt text.
The first fetch for a pack writes public/demo/media/<pack>.json; every seed after that
reads the manifest and makes no network calls, because Pexels' free tier allows 200
requests an hour and re-seeding packs on demand would exhaust it. Warm every pack
up front with npm run vertical:media, force a re-fetch after changing a pack's queries
with --refresh, and note that a missing PEXELS_API_KEY degrades to placeholder images
rather than failing the seed.
Media is referenced from the Pexels CDN, so this works with no storage channel
configured. To move the files onto your own R2/S3, run scripts/pexels-media.ts
afterwards.
Languages & Internationalization
Path: /admin/settings/languages
Tutor ships a lightweight i18n system (src/lib/i18n.ts). Translation strings are split by surface (frontend / backend) and keyed by the English source string, so any string falls back to English when not translated.
- Manage languages on the Languages screen: code, name, native name, flag emoji, locale, script, and direction (LTR/RTL). You can set a default language and mark languages active/inactive.
- English is the default source language and the fallback for any untranslated string; add further languages (including RTL languages such as Arabic) from the Languages screen.
- Open the translation editor from a language row (the globe action) to edit per-language translations of UI strings:
/admin/settings/languages/[id]/translations. - Server code translates via
getT(lang, scope)/getServerT(scope); admin UI uses auseT()hook returningt("English string").
Locale-related defaults (default language, multi-language switcher, timezone, date format) also live in the theme panel's Localization tab.
© CreativeCape Solutions · creative-cape.com · support@creative-cape.com