Enhance public access and error handling in site-related views and loaders

- Updated `DebugScopedRateThrottle` to allow public access for blueprint list requests with site filters.
- Modified `SiteViewSet` and `SiteBlueprintViewSet` to permit public read access for list requests.
- Enhanced `loadSiteDefinition` to resolve site slugs to IDs, improving the loading process for site definitions.
- Improved error handling in `SiteDefinitionView` and `loadSiteDefinition` for better user feedback.
- Adjusted CSS styles for better layout and alignment in shared components.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-18 22:40:00 +00:00
parent 8ab15d1d79
commit 6c6133a683
14 changed files with 361 additions and 76 deletions

View File

@@ -24,7 +24,8 @@ function App() {
<Suspense fallback={<LoadingFallback />}>
<Routes>
{/* Public Site Renderer Routes (No Auth) */}
<Route path="/:siteId/*" element={<SiteRenderer />} />
<Route path="/:siteSlug/:pageSlug?" element={<SiteRenderer />} />
<Route path="/:siteSlug" element={<SiteRenderer />} />
<Route path="/" element={<div>IGNY8 Sites Renderer</div>} />
{/* Builder Routes (Auth Required) */}

View File

@@ -41,11 +41,53 @@ function getApiBaseUrl(): string {
const API_URL = getApiBaseUrl();
/**
* Load site definition by site ID.
* First tries to load from filesystem (deployed sites),
* Resolve site slug to site ID.
* Queries the Site API to get the site ID from the slug.
*/
async function resolveSiteIdFromSlug(siteSlug: string): Promise<number> {
try {
// Query sites by slug - slug is unique per account, but we need to search across all accounts for public sites
const response = await axios.get(`${API_URL}/v1/auth/sites/`, {
params: { slug: siteSlug },
timeout: 10000,
headers: {
'Accept': 'application/json',
},
});
const sites = Array.isArray(response.data?.results) ? response.data.results :
Array.isArray(response.data) ? response.data : [];
if (sites.length > 0) {
return sites[0].id;
}
throw new Error(`Site with slug "${siteSlug}" not found`);
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
throw new Error(`Site with slug "${siteSlug}" not found`);
}
throw new Error(`Failed to resolve site slug: ${error.message}`);
}
throw error;
}
}
/**
* Load site definition by site slug.
* First resolves slug to ID, then tries to load from filesystem (deployed sites),
* then falls back to API.
*/
export async function loadSiteDefinition(siteId: string): Promise<SiteDefinition> {
export async function loadSiteDefinition(siteSlug: string): Promise<SiteDefinition> {
// First, resolve slug to site ID
let siteId: number;
try {
siteId = await resolveSiteIdFromSlug(siteSlug);
} catch (error) {
throw error; // Re-throw slug resolution errors
}
// Try API endpoint for deployed site definition first
try {
const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`, {
@@ -82,7 +124,7 @@ export async function loadSiteDefinition(siteId: string): Promise<SiteDefinition
return transformBlueprintToSiteDefinition(blueprint);
}
throw new Error(`No blueprint found for site ${siteId}`);
throw new Error(`No blueprint found for site ${siteSlug}`);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to load site: ${error.message}`);
@@ -110,6 +152,7 @@ function transformBlueprintToSiteDefinition(blueprint: any): SiteDefinition {
type: page.type,
blocks: page.blocks_json || [],
status: page.status,
order: page.order || 0,
})) || [],
config: blueprint.config_json || {},
created_at: blueprint.created_at,

View File

@@ -1,23 +1,23 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { loadSiteDefinition } from '../loaders/loadSiteDefinition';
import { renderLayout } from '../utils/layoutRenderer';
import type { SiteDefinition } from '../types';
function SiteRenderer() {
const { siteId } = useParams<{ siteId: string }>();
const { siteSlug, pageSlug } = useParams<{ siteSlug: string; pageSlug?: string }>();
const [siteDefinition, setSiteDefinition] = useState<SiteDefinition | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!siteId) {
setError('Site ID is required');
if (!siteSlug) {
setError('Site slug is required');
setLoading(false);
return;
}
loadSiteDefinition(siteId)
loadSiteDefinition(siteSlug)
.then((definition) => {
setSiteDefinition(definition);
setLoading(false);
@@ -26,7 +26,7 @@ function SiteRenderer() {
setError(err.message || 'Failed to load site');
setLoading(false);
});
}, [siteId]);
}, [siteSlug, pageSlug]);
if (loading) {
return <div>Loading site...</div>;
@@ -40,9 +40,122 @@ function SiteRenderer() {
return <div>Site not found</div>;
}
// Build navigation from site definition
// Show pages that are published, ready, or in navigation (excluding home and draft/generating)
const navigation = siteDefinition.navigation || siteDefinition.pages
.filter(p =>
p.slug !== 'home' &&
(p.status === 'published' || p.status === 'ready' || siteDefinition.navigation?.some(n => n.slug === p.slug))
)
.sort((a, b) => {
// Try to get order from navigation or use page order
const navA = siteDefinition.navigation?.find(n => n.slug === a.slug);
const navB = siteDefinition.navigation?.find(n => n.slug === b.slug);
return (navA?.order ?? a.order ?? 0) - (navB?.order ?? b.order ?? 0);
})
.map(page => ({
label: page.title,
slug: page.slug,
order: page.order || 0
}));
// Filter pages based on current route
const currentPageSlug = pageSlug || 'home';
const currentPage = siteDefinition.pages.find(p => p.slug === currentPageSlug);
// If specific page requested, show only that page; otherwise show all published/ready pages
const pagesToRender = currentPageSlug && currentPageSlug !== 'home' && currentPage
? [currentPage]
: siteDefinition.pages.filter(p =>
p.status === 'published' ||
p.status === 'ready' ||
(p.slug === 'home' && p.status !== 'draft' && p.status !== 'generating')
);
return (
<div className="site-renderer">
{renderLayout(siteDefinition)}
<div className="site-renderer" style={{
maxWidth: '1200px',
margin: '0 auto',
padding: '0 1rem',
width: '100%'
}}>
{/* Navigation Menu */}
<nav style={{
padding: '1rem 0',
borderBottom: '1px solid #e5e7eb',
marginBottom: '2rem'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '1rem'
}}>
<Link
to={`/${siteSlug}`}
style={{
fontSize: '1.5rem',
fontWeight: 'bold',
textDecoration: 'none',
color: '#0f172a'
}}
>
{siteDefinition.name}
</Link>
<div style={{
display: 'flex',
gap: '1.5rem',
flexWrap: 'wrap'
}}>
<Link
to={`/${siteSlug}`}
style={{
textDecoration: 'none',
color: currentPageSlug === 'home' ? '#4c1d95' : '#64748b',
fontWeight: currentPageSlug === 'home' ? 600 : 400
}}
>
Home
</Link>
{navigation.map((item) => (
<Link
key={item.slug}
to={`/${siteSlug}/${item.slug}`}
style={{
textDecoration: 'none',
color: currentPageSlug === item.slug ? '#4c1d95' : '#64748b',
fontWeight: currentPageSlug === item.slug ? 600 : 400
}}
>
{item.label}
</Link>
))}
</div>
</div>
</nav>
{/* Main Content */}
{renderLayout({ ...siteDefinition, pages: pagesToRender })}
{/* Footer */}
<footer style={{
marginTop: '4rem',
padding: '2rem 0',
borderTop: '1px solid #e5e7eb',
textAlign: 'center',
color: '#64748b',
fontSize: '0.875rem'
}}>
<p style={{ margin: 0 }}>
© {new Date().getFullYear()} {siteDefinition.name}. All rights reserved.
</p>
{siteDefinition.description && (
<p style={{ margin: '0.5rem 0 0', fontSize: '0.75rem' }}>
{siteDefinition.description}
</p>
)}
</footer>
</div>
);
}

View File

@@ -25,6 +25,7 @@ export interface PageDefinition {
type: string;
blocks: Block[];
status: string;
order?: number;
}
export interface Block {

View File

@@ -56,26 +56,31 @@ export function renderLayout(siteDefinition: SiteDefinition): React.ReactElement
* Uses shared DefaultLayout component with fully styled modern design.
*/
function renderDefaultLayout(siteDefinition: SiteDefinition): React.ReactElement {
// Find home page for hero
// Find home page for hero (only show hero on home page or when showing all pages)
const homePage = siteDefinition.pages.find(p => p.slug === 'home');
const heroBlock = homePage?.blocks?.find(b => b.type === 'hero');
const hero: React.ReactNode = heroBlock ? (renderTemplate(heroBlock) as React.ReactNode) : undefined;
// Only show hero if we're on home page or showing all pages
const isHomePage = siteDefinition.pages.length === 1 && siteDefinition.pages[0]?.slug === 'home';
const showHero = isHomePage || (homePage && siteDefinition.pages.length > 1);
const hero: React.ReactNode = (showHero && heroBlock) ? (renderTemplate(heroBlock) as React.ReactNode) : undefined;
// Render all pages as sections (excluding hero from home page if it exists)
const sections = siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => {
// Filter out hero block if it's the home page (already rendered as hero)
const blocksToRender = page.slug === 'home' && heroBlock
const blocksToRender = page.slug === 'home' && heroBlock && showHero
? page.blocks?.filter(b => b.type !== 'hero') || []
: page.blocks || [];
return (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.slug !== 'home' && <h2>{page.title}</h2>}
<div key={page.id} className="page" data-page-slug={page.slug} style={{ textAlign: 'center' }}>
{page.slug !== 'home' && <h2 style={{ textAlign: 'center', marginBottom: '1.5rem' }}>{page.title}</h2>}
{blocksToRender.length > 0 ? (
blocksToRender.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
<div key={index} className="block" data-block-type={block.type} style={{ textAlign: 'center' }}>
{renderTemplate(block)}
</div>
))