Microsites

Mini-sites — Developer Reference

Model hierarchy

Website (owner FK → User)
  ├── WebsiteSection        (sort_order)
  │     └── SectionAsset   (parent_page FK self, depth limit 4)
  │           └── PageBlock (block_type CharField, content JSONField, sort_order)
  ├── WebsiteLogo           (image, sort_order, is_active)
  └── WebsiteRevision       (snapshot JSONField, note, created_at)

ContactMessage  (website FK)
SectionAssetLog (asset FK, user FK, status, message, filename, created_at)

Key Website model fields

  • slug — internal slug, scoped per owner (not globally unique)
  • public_slug — globally unique, required for publishing; cleared to unpublish
  • is_published (bool) + publish_visibility (PUBLIC/UNLISTED/GROUP)
  • allowed_groups (M2M → contacts.ContactGroup) — used when visibility = GROUP
  • Image fields: header_image, logo, favicon, animated_logo
  • Colour fields: logo_bg_color, header_bg_color, footer_bg_color
  • Logo carousel: logo_opacity, logo_text_opacity, logo_slide_interval, logo_text_align, logo_text_valign
  • Nav: navigation_layout (LEFT/TOP/RIGHT), navigation_theme (LIGHT/DARK/TRANSPARENT)
  • Contact form: contact_show_phone, contact_phone_required, contact_email_required

URL patterns (microsites/urls.py)

NamePathView
microsite_owner_dashboard/microsites/owner_dashboard
microsite_create/microsites/new/website_create
microsite_edit/microsites/<pk>/edit/website_edit
microsite_builder/microsites/<pk>/builder/website_builder
microsite_owner_preview/microsites/<pk>/preview/website_owner_preview
microsite_page_add/microsites/<pk>/pages/add/microsite_page_add
microsite_page_edit/microsites/<pk>/pages/<page_id>/edit/microsite_page_edit
microsite_page_duplicate/microsites/<pk>/pages/<page_id>/duplicate/microsite_page_duplicate
microsite_page_details_update/microsites/<pk>/pages/<page_id>/details/microsite_page_details_update
microsite_page_delete/microsites/<pk>/pages/<page_id>/delete/microsite_page_delete
microsite_page_visibility_update/microsites/<pk>/pages/<page_id>/visibility/microsite_page_visibility_update
microsite_page_reorder/microsites/<pk>/pages/reorder/microsite_page_reorder
microsite_builder_settings_update/microsites/<pk>/builder/settings/microsite_builder_settings_update
microsite_block_add/microsites/<pk>/pages/<page_id>/blocks/add/microsite_block_add
microsite_block_bulk_reorder/microsites/<pk>/pages/<page_id>/blocks/reorder/microsite_block_bulk_reorder
microsite_block_edit/microsites/<pk>/pages/<page_id>/blocks/<block_id>/edit/microsite_block_edit
microsite_block_delete/microsites/<pk>/pages/<page_id>/blocks/<block_id>/delete/microsite_block_delete
microsite_block_reorder/microsites/<pk>/pages/<page_id>/blocks/<block_id>/reorder/microsite_block_reorder
microsite_site_duplicate/microsites/<pk>/duplicate/microsite_site_duplicate
microsite_revisions_list/microsites/<pk>/revisions/microsite_revisions_list
microsite_revision_restore/microsites/<pk>/revisions/<revision_id>/restore/microsite_revision_restore
microsite_contact_submit/microsites/<pk>/contact/contact_submit
microsite_contact_messages/microsites/<pk>/messages/contact_messages_list
microsite_message_status_update/microsites/<pk>/messages/<msg_id>/status/contact_message_status_update
microsite_publish_public/microsites/<pk>/publish/website_publish_public
microsite_publish_owner/microsites/<pk>/unpublish/website_publish_owner
microsite_delete/microsites/<pk>/delete/website_delete
microsite_export/microsites/<pk>/export/microsite_export
microsite_import/microsites/import/microsite_import
microsite_template_download/microsites/download-template/microsite_template_download
microsite_public_slug_check/microsites/api/public-slug-check/public_slug_check
microsite_public_index/sites/public_sites_index
microsite_public_site/sites/<site_slug>/public_site_by_slug
microsite_public_home/u/<username>/websites/<website_slug>/public_home
microsite_public_tab/u/<username>/websites/<website_slug>/<section_slug>/<tab_slug>/public_tab

ZIP format

mysite-slug.zip
├── site.json          ← UTF-8 JSON, version "1"
├── README.md          ← generated by _build_readme_text(website.title)
├── AI_CONTEXT.md      ← module constant _AI_CONTEXT_TEXT
├── images/
│   ├── header_image.<ext>
│   ├── logo.<ext>
│   ├── favicon.<ext>
│   ├── animated_logo.<ext>
│   └── logos/0.<ext>, 1.<ext> ...
└── files/
    └── <section_slug>_<asset_idx>.<ext>

site.json structure:

  • version: "1"
  • website: all Website scalar fields (not PKs, not public_slug, not is_published)
  • sections: array of sections → assets → blocks. parent_page_index is an array index (not a PK) for cross-DB portability.
  • logos: array of WebsiteLogo entries

NOT exported: public_slug, is_published, publish_visibility, allowed_groups, contact messages.

Security validation (_validate_zip_security)

Runs before any DB writes in microsite_import. Raises ValueError on violation:

  • Path traversal: any entry name containing .. or starting with / or \
  • Unexpected paths: files outside site.json, README.md, AI_CONTEXT.md, images/, files/
  • Disallowed image exts: only .jpg .jpeg .png .gif .ico .webp .svg
  • Disallowed file exts: only .pdf .md .html .htm .txt
  • ZIP bomb: >500 entries, total uncompressed >300 MB, single file >50 MB
  • Unsafe HTML in rawhtml/content_text: regex scan for <script, javascript:, on\w+=, <iframe, <object, <embed

Key helper functions (microsites/views.py)

FunctionPurpose
_build_export_data(website, zf)Writes all binary files into open ZipFile; returns site.json dict. Also writes README.md and AI_CONTEXT.md.
_build_readme_text(site_title)Returns the README.md string (personalised with site title). Also used by microsite_template_download.
_AI_CONTEXT_TEXTModule-level constant string — the AI_CONTEXT.md content. Identical in every ZIP.
_validate_zip_security(zf, data)Raises ValueError with user-friendly message on any security violation.
_zip_read_field(field, arc_name, zf, zip_names)Saves a binary file from an open ZipFile into a Django FileField (save=False).
_apply_zip_sections(data, zf, zip_names, website)Two-pass creation of sections, assets, blocks, and logos from parsed ZIP data on an existing Website object.
_build_site_snapshot(website)Returns JSON-serialisable snapshot keyed by PKs. Used for WebsiteRevision (different schema from export — uses PKs not indices).
_unique_site_slug(user, base_slug)Returns a slug unique to this user. Tries slug → slug-2 → slug-3 ... up to 200 attempts, then falls back to UUID suffix.
_get_owner_website_or_404(user, pk)Ownership guard. Raises 404 if the website does not belong to the user.

Block type content schemas (PageBlock.content JSONField)

block_typecontent schema
text{"html": "<p>...</p>"}
rawhtml{"html": "<div class='container'>...</div>"} — Bootstrap 5 only, no scripts
image{"url": "https://...", "alt": "", "caption": "", "width": "100%"}
button{"text": "Click", "url": "#slug or https://...", "style": "primary"}
collapse{"title": "Question?", "body": "<p>Answer</p>"}
form{"show_phone": false, "phone_required": false, "email_required": true}
map{"query": "123 Main St, Toronto", "height": 400}
youtube{"url": "https://youtube.com/watch?v=..."}
video{"url": "https://..."}
file{"label": "Download PDF", "url": "https://..."}
embed{"html": "<iframe ...></iframe>"}
columns{"col1": "<p>...</p>", "col2": "<p>...</p>"}
toc{} — auto-generated from page headings
navbar{"links": [{"label": "Home", "url": "#"}]}
divider{}
spacer{"height": 40}

Template architecture

TemplatePurpose
microsites/owner_list.htmlDashboard — lists all sites owned by the user. Contains Import ZIP, Starter Template, + New Site buttons.
microsites/website_preview.htmlFull builder SPA. Contains all inline JS (MS_PROMPTS, MS_DESCS, MS_TEMPLATES, msAiPanel(), msBlockBody(), block save/undo/delete handlers). Also includes the topbar and settings tab.
microsites/_block_row.htmlDjango template included for each block in edit mode. Renders the correct form fields per block_type. Contains the rawhtml AI panel and tips panel.
microsites/import.htmlZIP import form. Shows the optional existing-site dropdown when user has sites.
microsites/revisions_list.htmlRevision history. Each card has an expandable preview showing block counts and snippets.
microsites/_public_canvas.htmlPublic page renderer. Renders each block_type via template conditionals. Contact form is rendered for pages named "contact".

Running tests

python manage.py test microsites

Adding a new block type — checklist

  1. Add to _VALID_BLOCK_TYPES set in microsites/views.py
  2. Add a case "newtype": branch in msBlockBody() in website_preview.html to return the edit form HTML
  3. Add a save/load case in the block save JS handler (msBlockSave() or equivalent) in website_preview.html
  4. Add a render branch in _public_canvas.html so it displays on the public site
  5. Add the block's schema to _block_row.html for the edit-existing-block path
  6. Add the block to the Quick Start tiles array in website_preview.html if it warrants an AI prompt template
  7. Add the block type and its content schema to _AI_CONTEXT_TEXT in microsites/views.py so exported ZIPs include it
  8. Update this developer guide