1. Complete theme structure
A production theme should keep its entire implementation inside one theme folder. The supplied theme uses this structure:
2. Manifest and theme bootstrap
theme.json
The marketplace and StudioCMS can identify a theme from its manifest. Keep the values plain JSON and place the file at the theme root.
{
"name": "Your Theme Name",
"slug": "your-theme-name",
"version": "1.0.0",
"author": "StudioCMS",
"description": "A concise description of the theme.",
"category": "Business"
}theme.php
The supplied theme returns the display name and the settings class. Match settings_class to the class declared in settings/options.php.
<?php
return [
'name' => 'Your Theme Name',
'settings_class' => 'YourThemeSettings',
];Theme screenshot
Include screenshot.webp, screenshot.jpg, screenshot.png, or screenshot.svg in the ZIP. The StudioCMS marketplace package included with this release can read the screenshot directly from the ZIP.
3. Template responsibilities
$posts and render $pagination when supplied.$data for title, slug, description, content and page-specific routing.$data, including featured image/ALT and trusted rendered content.$tag and $posts.Supporting files that complete the theme
| File | Role |
|---|---|
style.css | Public theme styles, responsive layouts, status-page styles, blog/page/post/gallery styling. |
assets/theme.js | Theme-side interactions such as navigation, galleries, lightbox behavior or other progressive enhancements. |
screenshot.svg (or supported image format) | Theme preview artwork for review and marketplace display. |
README.txt | Version, features, settings, installation steps, content behavior and release notes. |
send-request.php | Theme request endpoint when the theme needs its own form bridge; the supplied version boots StudioCMS and uses native Forms validation/submission classes. |
4. Data available to templates
| Template | Observed variables / fields | Recommended handling |
|---|---|---|
page.php | $data['slug'], title, meta_description, content | Escape titles/descriptions; render CMS HTML content only where StudioCMS has already prepared it for output. |
post.php | $data['title'], featured_image, featured_alt, content | Use base_url() for media and escape attributes. |
blog.php | $posts, optional $pagination | Each post may include slug, title, content, featured image and ALT text. |
tag.php | $tag, $posts | Escape the tag title and post text. |
<?php require __DIR__ . '/header.php'; ?> <article> <h1><?= htmlspecialchars((string)($data['title'] ?? 'Page')) ?></h1> <div class="content"><?= (string)($data['content'] ?? '') ?></div> </article> <?php require __DIR__ . '/footer.php'; ?>
5. Header, SEO, menus and assets
The supplied theme loads functions.php from header.php, reads theme options, derives the page title/description from $data when available, then uses StudioCMS SeoFrontend::output(). It also supports theme keywords, a theme favicon, native menu data, Google measurement helpers and the optional ThemeDeveloperContract::renderHeadIntegrations().
$themeFolder = basename(__DIR__);
$pageTitle = !empty($data['title']) ? (string)$data['title'] : vcr_option('seo_title', 'Default title');
$pageDescription = !empty($data['meta_description']) ? (string)$data['meta_description'] : vcr_option('meta_description', 'Default description');
SeoFrontend::output([
'title' => $pageTitle,
'description' => $pageDescription,
]);Build asset URLs with base_url('themes/'.$themeFolder.'/style.css') instead of assuming the website is installed at the domain root. The current theme also uses base_url('storage/uploads/...') for CMS media.
6. Theme Settings contract
settings/options.php declares a settings class. Its static fields(): array method returns field definitions. The supplied theme uses the field types image, text, textarea, number, url, and menu, grouped into Branding, Hero, Packages, Add-On Services, Gallery Image, Videos, Reservation Request, FAQ, Contact, Social, Navigation, SEO, and Google Measurement.
class YourThemeSettings
{
public static function fields(): array
{
return [
['name'=>'logo', 'label'=>'Site Logo', 'type'=>'image', 'group'=>'Branding'],
['name'=>'site_name', 'label'=>'Site Name', 'type'=>'text', 'group'=>'Branding'],
['name'=>'seo_title', 'label'=>'SEO Title', 'type'=>'text', 'group'=>'SEO'],
['name'=>'meta_description', 'label'=>'Default Meta Description', 'type'=>'textarea', 'group'=>'SEO'],
['name'=>'seo_keywords', 'label'=>'SEO Keywords', 'type'=>'text', 'group'=>'SEO'],
];
}
}Use ThemeOptions::get() to read stored values. If you seed defaults, only create a default when the option does not already exist; never overwrite an administrator's saved customization during a theme update.
7. Sections, Gallery and native CMS modules
Reusable front-page blocks can live under sections/. The supplied theme has separate booking, gallery and album templates. The gallery uses native StudioCMS gallery records and creates album URLs such as /gallery/{album-slug}. Album pages use server-side pagination and read native gallery album/image data through the CMS database object.
Keep reusable UI in section files, but keep routing decisions in the primary template. In the supplied theme, page.php detects the Gallery page and loads either sections/gallery.php or sections/album.php when an album is requested.
8. 404, Coming Soon and Maintenance
404
<?php http_response_code(404); require __DIR__.'/header.php'; ?> <section class="not-found">...</section> <?php require __DIR__.'/footer.php'; ?>
Coming Soon
The supplied status template uses Settings::get('coming_title') and Settings::get('coming_message'), sets noindex,nofollow, reads the theme logo/favicon, and can show theme contact details.
Maintenance
Use the same standalone pattern with maintenance_title and maintenance_message. Status pages should not rely on front-page sections being available, and they should work even when normal public routing is disabled.
9. Forms, anti-spam and bot-abuse protection
The supplied theme integrates with StudioCMS Forms instead of creating theme-specific tables. Its booking/contact section should load the native Forms renderer, while send-request.php boots StudioCMS and uses the core Forms validator, anti-spam, rate-limit, upload and submission classes.
Required public-form protections
Every public theme form should use the StudioCMS protections below. The theme should not attempt to reimplement these checks with custom database tables or client-side JavaScript alone.
| Protection | Purpose | Theme responsibility |
|---|---|---|
| CSRF validation | Rejects forged cross-site submissions. | Render the native form token and validate it in send-request.php before processing input. |
| Honeypot / bot trap | Stops simple automated form-fill bots. | Render the StudioCMS honeypot field and reject submissions when it is populated. |
| Signed render time | Proves the form was rendered by StudioCMS and lets the server measure submission timing. | Preserve the native signed timing fields generated by the Forms renderer. |
| Minimum submit delay | Rejects unrealistically fast submissions commonly produced by bots. | Use the core anti-spam validator; do not hard-code timing logic inside the theme. |
| Stale-form protection | Rejects very old or replayed form submissions. | Allow StudioCMS to validate the signed form age server-side. |
| Per-IP rate limiting | Limits repeated attempts and automated abuse. | Use the native rate limiter and display the returned warning to the visitor. |
| Link / payload checks | Blocks excessive links and obvious spam payloads. | Pass the submitted values through the StudioCMS anti-spam service before saving or emailing. |
| Secure uploads | Prevents unsafe filenames and unsupported file types. | Use StudioCMS Upload::validate() and Upload::filename(); never trust the browser filename or MIME type by itself. |
Recommended send-request.php flow
<?php
// 1. Boot StudioCMS.
require_once __DIR__ . '/../../bootstrap.php';
// 2. Accept POST only.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
// 3. Validate the native StudioCMS form security fields.
// This includes CSRF, honeypot, signed timing, minimum-submit
// delay, stale-form checks and rate limiting.
$spamResult = FormAntiSpam::validate($_POST, $_SERVER);
if (!$spamResult['ok']) {
// Return a visible validation / anti-abuse message.
// Example: "Too many request attempts. Please try again later."
// Do not silently hide this from the user.
// ...
}
// 4. Validate normal form fields with the native Forms validator.
// 5. Validate uploads with Upload::validate().
// 6. Generate stored filenames with Upload::filename().
// 7. Submit through the native Forms submission service.Visible validation and anti-abuse messages
A secure form is not complete if the visitor cannot read the response. Theme CSS must keep validation errors, anti-spam warnings, rate-limit messages and success messages visible against the form background. Avoid setting these containers to transparent text, low opacity or colors that match the panel behind them.
/* Example theme-side visibility rules */
.cms-form-alert-error,
.cms-form-alert-error *,
.cms-form-errors,
.cms-form-errors *,
[role="alert"],
[role="alert"] * {
color: #111 !important;
opacity: 1 !important;
visibility: visible !important;
}
.cms-form-alert-success,
.cms-form-alert-success * {
color: #111 !important;
opacity: 1 !important;
visibility: visible !important;
}Mobile date fields
Native iOS/Safari date inputs can keep an intrinsic width that overflows a responsive form card. Themes that use input[type="date"] should explicitly constrain the field and its grid wrapper.
.cms-form-field {
min-width: 0;
max-width: 100%;
}
input[type="date"] {
-webkit-appearance: none;
appearance: none;
display: block;
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
}
@media (max-width: 760px) {
.cms-form-grid {
grid-template-columns: minmax(0, 1fr);
}
}10. Google measurement and developer integrations
The supplied settings expose GA4 Measurement ID, Google Tag Manager Container ID, Google Ads Conversion ID/label, and Search Console verification token. Measurement output is handled through theme helper functions. Header integration also checks for ThemeDeveloperContract before rendering CMS-provided head integrations.
Validate identifiers before output, escape verification values, and do not hard-code a client's tracking credentials into a redistributable theme ZIP.
11. Security and portability rules
- Escape text and attributes with
htmlspecialchars(..., ENT_QUOTES, 'UTF-8'). - Use
base_url()rather than absolute domain URLs. - Use
basename()when a stored media filename is expected, to prevent path traversal. - Do not put API secrets, SMTP passwords, PayPal secrets, or private credentials inside a theme.
- Do not trust query-string values. Validate album slugs, IDs, pagination numbers and form input.
- All public forms must use StudioCMS CSRF validation, honeypot checks, signed render timing, minimum-submit delay, stale-form protection and per-IP rate limiting.
- Pass public form text through the native anti-spam checks for excessive links and obvious bot/spam payloads before saving or emailing.
- Validate uploads with the StudioCMS upload service and generate safe stored filenames; never trust a user-supplied filename, extension or MIME type alone.
- Display validation, anti-spam, rate-limit and success messages clearly; security feedback must not be hidden by theme colors or CSS.
- Avoid duplicate JavaScript submit handlers when using the StudioCMS native Forms renderer, because double submissions can trigger abuse protection and create duplicate entries.
- Use the CMS database layer with bound parameters for native data queries.
- Keep status templates independent and set correct HTTP/robots behavior.
- Lazy-load non-critical images and provide meaningful ALT text.
12. ZIP packaging checklist
- The ZIP contains one root theme folder—not loose files mixed at archive root.
theme.jsonhas name, slug, version, author, description and category.theme.phppoints to the correct settings class.- Include
index.php,page.php,post.php,blog.php,tag.php,404.php,coming_soon.php, andmaintenance.php. - Include
header.php,footer.php,functions.php, CSS and JS assets. - Include
settings/options.phpand any reusablesections/. - If the theme has a public form, verify CSRF, honeypot, signed timing, minimum-submit delay, stale-form protection, per-IP rate limiting, spam/link checks and secure upload handling.
- Verify all form validation, anti-spam, rate-limit and success messages remain readable on desktop and mobile.
- Verify
input[type="date"]does not overflow its form card on iOS/Safari. - Include a screenshot and README with version, features and install notes.
- Test the home page, page, post, blog pagination, tag archive, 404, Coming Soon, Maintenance, forms, Gallery, mobile navigation, SEO output and all Theme Settings before release.