fix(hosting): render POC preview directly instead of iframe
The POC viewer was loading the storefront in an iframe, which hit the
StorefrontAccessMiddleware subscription check (POC sites don't have
subscriptions yet). Fixed by rendering CMS sections directly in the
preview template:
- Load ContentPages and StoreTheme from DB
- Render hero, features, testimonials, CTA sections inline
- Apply template colors/fonts via Tailwind CSS config
- HostWizard preview banner with nav links
- Footer with contact info
- No iframe, no subscription check needed
Also fixed Jinja2 dict.items collision (dict.items is the method,
not the 'items' key — use dict.get('items') instead).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,19 +26,75 @@ async def poc_site_viewer(
|
||||
site_id: int = Path(..., description="Hosted Site ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Render POC site viewer with HostWizard preview banner."""
|
||||
"""Render POC site viewer with HostWizard preview banner.
|
||||
|
||||
Renders CMS content directly (not via iframe to storefront) to
|
||||
bypass the subscription access gate for pre-launch POC sites.
|
||||
"""
|
||||
from app.modules.cms.models.content_page import ContentPage
|
||||
from app.modules.cms.models.store_theme import StoreTheme
|
||||
from app.modules.hosting.models import HostedSite, HostedSiteStatus
|
||||
from app.modules.tenancy.models import StorePlatform
|
||||
|
||||
site = db.query(HostedSite).filter(HostedSite.id == site_id).first()
|
||||
|
||||
# Only allow viewing for poc_ready or proposal_sent sites
|
||||
if not site or site.status not in (HostedSiteStatus.POC_READY, HostedSiteStatus.PROPOSAL_SENT):
|
||||
if not site or site.status not in (
|
||||
HostedSiteStatus.POC_READY,
|
||||
HostedSiteStatus.PROPOSAL_SENT,
|
||||
HostedSiteStatus.ACCEPTED,
|
||||
):
|
||||
return HTMLResponse(content="<h1>Site not available for preview</h1>", status_code=404)
|
||||
|
||||
store = site.store
|
||||
|
||||
# Get platform_id for CMS query
|
||||
store_platform = (
|
||||
db.query(StorePlatform)
|
||||
.filter(StorePlatform.store_id == store.id)
|
||||
.first()
|
||||
) if store else None
|
||||
platform_id = store_platform.platform_id if store_platform else None
|
||||
|
||||
# Load homepage (slug='homepage' from POC builder)
|
||||
page = None
|
||||
if platform_id:
|
||||
page = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.store_id == store.id,
|
||||
ContentPage.slug == "homepage",
|
||||
ContentPage.is_published.is_(True),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Load all nav pages for this store
|
||||
nav_pages = []
|
||||
if platform_id:
|
||||
nav_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.store_id == store.id,
|
||||
ContentPage.is_published.is_(True),
|
||||
ContentPage.show_in_header.is_(True),
|
||||
)
|
||||
.order_by(ContentPage.display_order)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Load theme
|
||||
theme = db.query(StoreTheme).filter(StoreTheme.store_id == store.id).first() if store else None
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"site": site,
|
||||
"store_url": f"/stores/{site.store.subdomain}" if site.store else "#",
|
||||
"store": store,
|
||||
"page": page,
|
||||
"nav_pages": nav_pages,
|
||||
"theme": theme,
|
||||
}
|
||||
return templates.TemplateResponse(
|
||||
"hosting/public/poc-viewer.html",
|
||||
|
||||
@@ -4,64 +4,180 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site.business_name }} - Preview by HostWizard</title>
|
||||
{% if page and page.meta_description %}
|
||||
<meta name="description" content="{{ page.meta_description }}">
|
||||
{% endif %}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '{{ theme.colors.primary if theme and theme.colors else "#3b82f6" }}',
|
||||
'primary-dark': '{{ theme.colors.secondary if theme and theme.colors else "#1e40af" }}',
|
||||
accent: '{{ theme.colors.accent if theme and theme.colors else "#f59e0b" }}',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
.hw-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background: linear-gradient(135deg, #0D9488, #14B8A6);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.hw-banner-left { display: flex; align-items: center; gap: 12px; }
|
||||
.hw-banner-logo { font-weight: 700; font-size: 16px; }
|
||||
.hw-banner-text { opacity: 0.9; }
|
||||
.hw-banner-right { display: flex; align-items: center; gap: 12px; }
|
||||
.hw-banner-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid rgba(255,255,255,0.4);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.hw-banner-link:hover { background: rgba(255,255,255,0.15); }
|
||||
.hw-iframe-container {
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.hw-iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
@import url('https://fonts.googleapis.com/css2?family={{ (theme.font_family_heading or "Inter")|replace(" ", "+") }}:wght@400;600;700;800&family={{ (theme.font_family_body or "Inter")|replace(" ", "+") }}:wght@300;400;500;600&display=swap');
|
||||
body { font-family: '{{ theme.font_family_body if theme else "Inter" }}', sans-serif; }
|
||||
h1, h2, h3, h4, h5, h6 { font-family: '{{ theme.font_family_heading if theme else "Inter" }}', sans-serif; }
|
||||
.gradient-primary { background: linear-gradient(135deg, {{ theme.colors.primary if theme and theme.colors else "#3b82f6" }}, {{ theme.colors.secondary if theme and theme.colors else "#1e40af" }}); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hw-banner">
|
||||
<div class="hw-banner-left">
|
||||
<span class="hw-banner-logo">HostWizard</span>
|
||||
<span class="hw-banner-text">Preview for {{ site.business_name }}</span>
|
||||
<body class="bg-white text-gray-900">
|
||||
|
||||
{# HostWizard Preview Banner #}
|
||||
<div style="position:fixed;top:0;left:0;right:0;z-index:9999;background:linear-gradient(135deg,#0D9488,#14B8A6);color:white;padding:10px 20px;display:flex;align-items:center;justify-content:space-between;font-family:system-ui;font-size:14px;box-shadow:0 2px 8px rgba(0,0,0,0.15);">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<span style="font-weight:700;font-size:16px;">HostWizard</span>
|
||||
<span style="opacity:0.9;">Preview for {{ site.business_name }}</span>
|
||||
</div>
|
||||
<div class="hw-banner-right">
|
||||
<a href="https://hostwizard.lu" class="hw-banner-link" target="_blank">hostwizard.lu</a>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
{% for np in nav_pages %}
|
||||
<a href="/hosting/sites/{{ site.id }}/preview?page={{ np.slug }}" style="color:white;text-decoration:none;padding:4px 12px;border:1px solid rgba(255,255,255,0.3);border-radius:6px;font-size:13px;">{{ np.title }}</a>
|
||||
{% endfor %}
|
||||
<a href="https://hostwizard.lu" style="color:white;text-decoration:none;padding:6px 16px;border:1px solid rgba(255,255,255,0.4);border-radius:6px;font-size:13px;" target="_blank">hostwizard.lu</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hw-iframe-container">
|
||||
<iframe src="{{ store_url }}" title="Site preview"></iframe>
|
||||
|
||||
{# Main Content — rendered directly, no iframe #}
|
||||
<div style="margin-top:48px;">
|
||||
{% if page and page.sections %}
|
||||
{# Section-based rendering (from POC builder templates) #}
|
||||
{% set lang = 'fr' %}
|
||||
{% set default_lang = 'fr' %}
|
||||
|
||||
{% if page.sections.hero %}
|
||||
{% set hero = page.sections.hero %}
|
||||
<section class="relative py-24 lg:py-32 gradient-primary text-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
{% set t = hero.get('title', {}) %}
|
||||
{% set tr = t.get('translations', {}) if t is mapping else {} %}
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6">{{ tr.get(lang) or tr.get(default_lang) or site.business_name }}</h1>
|
||||
{% set st = hero.get('subtitle', {}) %}
|
||||
{% set str_ = st.get('translations', {}) if st is mapping else {} %}
|
||||
{% set subtitle_text = str_.get(lang) or str_.get(default_lang, '') %}
|
||||
{% if subtitle_text %}
|
||||
<p class="text-xl md:text-2xl opacity-90 mb-10 max-w-2xl mx-auto">{{ subtitle_text }}</p>
|
||||
{% endif %}
|
||||
{% set hero_buttons = hero.get('buttons', []) %}
|
||||
{% if hero_buttons %}
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% for btn in hero_buttons %}
|
||||
{% set bl = btn.get('label', {}) %}
|
||||
{% set bltr = bl.get('translations', {}) if bl is mapping else {} %}
|
||||
{% set btn_label = bltr.get(lang) or bltr.get(default_lang, 'Learn More') %}
|
||||
<a href="{{ btn.get('url', '#') }}" class="px-8 py-4 rounded-lg font-semibold text-lg {{ 'bg-white text-gray-900 hover:bg-gray-100' if btn.get('style') == 'primary' else 'border-2 border-white text-white hover:bg-white/10' }} transition-colors">
|
||||
{{ btn_label }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if page.sections.features %}
|
||||
{% set features = page.sections.features %}
|
||||
<section class="py-16 lg:py-24 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{% set ft = features.get('title', {}) %}
|
||||
{% set ftr = ft.get('translations', {}) if ft is mapping else {} %}
|
||||
{% set title = ftr.get(lang) or ftr.get(default_lang, '') %}
|
||||
{% if title %}
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-center mb-12">{{ title }}</h2>
|
||||
{% endif %}
|
||||
{% set feature_items = features.get('items', []) %}
|
||||
{% if feature_items %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ [feature_items|length, 4]|min }} gap-8">
|
||||
{% for item in feature_items %}
|
||||
{% set it = item.get('title', {}) %}
|
||||
{% set itr = it.get('translations', {}) if it is mapping else {} %}
|
||||
{% set item_title = itr.get(lang) or itr.get(default_lang, '') %}
|
||||
{% set id_ = item.get('description', {}) %}
|
||||
{% set idr = id_.get('translations', {}) if id_ is mapping else {} %}
|
||||
{% set item_desc = idr.get(lang) or idr.get(default_lang, '') %}
|
||||
<div class="bg-gray-50 rounded-xl p-8 text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full gradient-primary flex items-center justify-center">
|
||||
<span class="text-white text-2xl font-bold">{{ item_title[0]|upper if item_title else '?' }}</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold mb-2">{{ item_title }}</h3>
|
||||
<p class="text-gray-600">{{ item_desc }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if page.sections.testimonials %}
|
||||
{% set testimonials = page.sections.testimonials %}
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
{% set tt = testimonials.get('title', {}) %}
|
||||
{% set ttr = tt.get('translations', {}) if tt is mapping else {} %}
|
||||
{% set title = ttr.get(lang) or ttr.get(default_lang, '') %}
|
||||
{% if title %}
|
||||
<h2 class="text-3xl font-bold mb-12">{{ title }}</h2>
|
||||
{% endif %}
|
||||
<p class="text-gray-400">Coming soon</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if page.sections.cta %}
|
||||
{% set cta = page.sections.cta %}
|
||||
<section class="py-16 gradient-primary text-white">
|
||||
<div class="max-w-4xl mx-auto px-4 text-center">
|
||||
{% set ct = cta.get('title', {}) %}
|
||||
{% set ctr = ct.get('translations', {}) if ct is mapping else {} %}
|
||||
<h2 class="text-3xl font-bold mb-8">{{ ctr.get(lang) or ctr.get(default_lang, '') }}</h2>
|
||||
{% set cta_buttons = cta.get('buttons', []) %}
|
||||
{% if cta_buttons %}
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% for btn in cta_buttons %}
|
||||
{% set bl = btn.get('label', {}) %}
|
||||
{% set bltr = bl.get('translations', {}) if bl is mapping else {} %}
|
||||
<a href="{{ btn.get('url', '#') }}" class="px-8 py-4 bg-white text-gray-900 rounded-lg font-semibold text-lg hover:bg-gray-100 transition-colors">
|
||||
{{ bltr.get(lang) or bltr.get(default_lang, 'Contact') }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% elif page and page.content %}
|
||||
{# Plain HTML content #}
|
||||
<div class="max-w-4xl mx-auto px-4 py-16 prose lg:prose-lg">
|
||||
{{ page.content | safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="max-w-4xl mx-auto px-4 py-16 text-center text-gray-400">
|
||||
<p>No content yet. Build a POC to populate this site.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Footer #}
|
||||
<footer class="bg-gray-900 text-gray-400 py-12">
|
||||
<div class="max-w-7xl mx-auto px-4 text-center">
|
||||
<p class="text-lg font-semibold text-white mb-2">{{ site.business_name }}</p>
|
||||
{% if site.contact_email %}
|
||||
<p>{{ site.contact_email }}</p>
|
||||
{% endif %}
|
||||
{% if site.contact_phone %}
|
||||
<p>{{ site.contact_phone }}</p>
|
||||
{% endif %}
|
||||
<p class="mt-6 text-sm text-gray-500">Powered by <a href="https://hostwizard.lu" class="text-teal-400 hover:underline" target="_blank">HostWizard</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user