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/themeGeneral 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 / PNG
  • img_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.

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.

TS
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:

  1. A pack is data. It must not import from @/lib or @/app, and the engine must never gain an if (pack === "…"). If a vertical needs behaviour the contract can't express, extend the contract.
  2. Packs never add columns. Vertical-specific values go in listings.attributes (JSONB, GIN-indexed) via source: "attribute". Only the pre-existing listings columns use source: "column".
  3. Say where a facet's data lives. A FilterSpec with only a kind counts through the listing_master_items link table; one with field + source reads 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 a useT() hook returning t("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