Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/api/middleware/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export async function GET(request: Request) {
const content_type_uid = searchParams.get("content_type_uid")
const pageUrl = searchParams.get("url")
const live_preview = searchParams.get("live_preview")
const preview_timestamp = searchParams.get("preview_timestamp")

// Use custom endpoints if provided, otherwise fall back to region-based endpoints
// For internal testing purposes at Contentstack we look for a custom region/hostnames in the env vars, you do not have to do this.
Expand All @@ -29,6 +30,9 @@ export async function GET(request: Request) {
if (live_preview) {
headers.append("live_preview", live_preview as string);
headers.append("preview_token", process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN as string);
if (preview_timestamp) {
headers.append("preview_timestamp", preview_timestamp as string);
}
}

const environment = process.env.NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT as string;
Expand Down
13 changes: 7 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import type { Metadata } from "next";
import "./globals.css";
import { ContentstackLivePreview } from "@/components/ContentstackLivePreview";
import type { Metadata } from "next"; // Importing the Metadata type from Next.js
import "./globals.css"; // Importing global CSS styles

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

// RootLayout component that wraps the entire application
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode; // Type definition for children prop
}>) {
return (
<html lang="en">
{/* Setting the language attribute for the HTML document */}
<body>
{children}
<ContentstackLivePreview />
<main className="max-w-(--breakpoint-md) mx-auto">{children}</main>
</body>
{/* Rendering the children components inside the body */}
</html>
);
}
164 changes: 22 additions & 142 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,149 +1,29 @@
import DOMPurify from "isomorphic-dompurify";
import Image from "next/image";
// Importing function to fetch page data and preview mode checker from Contentstack utilities
import { getPage, isPreview } from "@/lib/contentstack";
// Importing the Page component to render static content
import Page from "@/components/Page";
// Importing the Preview component to render live preview content
import Preview from "@/components/Preview";
import { headers } from "next/headers";
import { Page } from "@/lib/types";

export default async function Home({
searchParams,
}: {
searchParams: Promise<any>;
}) {
// Home page component - serves as the main entry point for the application
// This is an async server component that can fetch data at build time or request time
export default async function Home() {
const headersList = await headers();
let { url, content_type_uid } = await searchParams;
const { live_preview } = await searchParams;

if (!url) {
url = "/"; // Default to home page URL
}

if (!content_type_uid) {
content_type_uid = "page";
const host = headersList.get("host") || "localhost:3000";
const protocol = headersList.get("x-forwarded-proto") || "http";
const baseUrl = `${protocol}://${host}`;
// Check if the application is running in preview mode
// Preview mode enables live editing capabilities for content creators
if (isPreview) {
// Return the Preview component which handles real-time content updates
// The path "/" represents the home page URL in Contentstack
return <Preview path="/" baseUrl={baseUrl} />;
}

const getContent = async () => {
const host = headersList.get('host') || 'localhost:3000';
const protocol = headersList.get('x-forwarded-proto') || 'http';
const baseUrl = `${protocol}://${host}`;

const result = await fetch(
live_preview
? `${baseUrl}/api/middleware?content_type_uid=${content_type_uid}&url=${encodeURIComponent(
url
)}&live_preview=${live_preview}`
: `${baseUrl}/api/middleware?content_type_uid=${content_type_uid}&url=${encodeURIComponent(
url
)}`
);

return await result.json();
};

const page: Page = await getContent();

return (
<main className="max-w-(--breakpoint-md) mx-auto">
<section className="p-4">
{live_preview ? (
<ul className="mb-8 text-sm">
<li>
live_preview_hash: <code>{live_preview}</code>
</li>
<li>
content_type_uid: <code>{content_type_uid}</code>
</li>
<li>
url: <code>{url}</code>
</li>
</ul>
) : null}

{page?.title ? (
<h1
className="text-4xl font-bold mb-4"
{...(page?.$ && page?.$.title)}
>
{page?.title}
</h1>
) : null}

{page?.description ? (
<p className="mb-4" {...(page?.$ && page?.$.description)}>
{page?.description}
</p>
) : null}

{page?.image ? (
<Image
className="mb-4"
width={768}
height={414}
src={page?.image.url}
alt={page?.image.title}
{...(page?.image?.$ && page?.image?.$.url)}
/>
) : null}

{page?.rich_text ? (
<div
{...(page?.$ && page?.$.rich_text)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(page?.rich_text),
}}
/>
) : null}

<div
className="space-y-8 max-w-full mt-4"
{...(page?.$ && page?.$.blocks)}
>
{page?.blocks?.map((item, index) => {
const { block } = item;
const isImageLeft = block.layout === "image_left";
// In production mode, fetch the page data server-side for better performance
const page = await getPage(baseUrl, "/"); // Fetch home page content from Contentstack

return (
<div
key={block._metadata.uid}
{...(page?.$ && page?.$[`blocks__${index}`])}
className={`flex flex-col md:flex-row items-center space-y-4 md:space-y-0 bg-white ${
isImageLeft ? "md:flex-row" : "md:flex-row-reverse"
}`}
>
<div className="w-full md:w-1/2">
{block.image ? (
<Image
src={block.image.url}
alt={block.image.title}
width={200}
height={112}
className="w-full"
{...(block?.$ && block?.$.image)}
/>
) : null}
</div>
<div className="w-full md:w-1/2 p-4">
{block.title ? (
<h2
className="text-2xl font-bold"
{...(block?.$ && block?.$.title)}
>
{block.title}
</h2>
) : null}
{block.copy ? (
<div
{...(block?.$ && block?.$.copy)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(block.copy),
}}
className="prose"
/>
) : null}
</div>
</div>
);
})}
</div>
</section>
</main>
);
// Return the static Page component with the pre-fetched data
return <Page page={page} />;
}
21 changes: 0 additions & 21 deletions components/ContentstackLivePreview.tsx

This file was deleted.

129 changes: 129 additions & 0 deletions components/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Importing DOMPurify for sanitizing HTML content to prevent XSS attacks
import DOMPurify from "isomorphic-dompurify";
// Importing Next.js optimized Image component for better performance
import Image from "next/image";
// Importing the Page type definition from our types file
import { Page } from "@/lib/types";
// Importing Visual Builder class for empty block handling in Contentstack Live Preview
import { VB_EmptyBlockParentClass } from "@contentstack/live-preview-utils";

// Interface defining the props for the ContentDisplay component
interface ContentDisplayProps {
page: Page | undefined; // Page data that may be undefined during loading
}

// Main component for displaying page content with Contentstack Live Preview support
export default function ContentDisplay({ page }: ContentDisplayProps) {
return (
<section className="p-4">
{/* Display page title if it exists */}
{page?.title ? (
<h1
className="text-4xl font-bold mb-4 text-center"
// Spread live preview attributes for editing capability in Contentstack
{...(page?.$ && page?.$.title)}
>
{page?.title} with Next
</h1>
) : null}

{/* Display page description if it exists */}
{page?.description ? (
<p className="mb-4 text-center" {...(page?.$ && page?.$.description)}>
{page?.description}
</p>
) : null}

{/* Display hero image if it exists */}
{page?.image ? (
<Image
className="mb-4"
width={768}
height={414}
src={page?.image.url}
alt={page?.image.title}
// Spread live preview attributes for the image field
{...(page?.image?.$ && page?.image?.$.url)}
/>
) : null}

{/* Display rich text content if it exists, sanitized for security */}
{page?.rich_text ? (
<div
{...(page?.$ && page?.$.rich_text)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(page?.rich_text), // Sanitize HTML to prevent XSS attacks
}}
/>
) : null}

{/*
Container for modular blocks with Visual Builder support
Adds empty block class when no blocks exist for better editing experience
*/}
<div
className={`space-y-8 max-w-full mt-4 ${
!page?.blocks || page.blocks.length === 0
? VB_EmptyBlockParentClass // Special class for empty state in Visual Builder
: ""
}`}
{...(page?.$ && page?.$.blocks)}
>
{/* Map through blocks array to render each modular content block */}
{page?.blocks?.map((item, index) => {
const { block } = item; // Extract block data from item
const isImageLeft = block.layout === "image_left"; // Determine layout direction

return (
<div
key={block._metadata?.uid || `block-${index}`} // Use unique identifier as key for React
{...(page?.$ && page?.$[`blocks__${index}`])} // Live preview attributes for each block
className={`flex flex-col md:flex-row items-center space-y-4 md:space-y-0 bg-white ${
isImageLeft ? "md:flex-row" : "md:flex-row-reverse" // Conditional layout based on block settings
}`}
>
{/* Image container - takes half width on medium screens and up */}
<div className="w-full md:w-1/2">
{block.image ? (
<Image
key={`image-${block._metadata?.uid || index}`} // Unique key for image
src={block.image.url}
alt={block.image.title}
width={200}
height={112}
className="w-full"
{...(block?.$ && block?.$.image)} // Live preview attributes for image
/>
) : null}
</div>

{/* Content container - takes half width on medium screens and up */}
<div className="w-full md:w-1/2 p-4">
{/* Block title */}
{block.title ? (
<h2
className="text-2xl font-bold"
{...(block?.$ && block?.$.title)} // Live preview attributes for title
>
{block.title}
</h2>
) : null}

{/* Block rich text content, sanitized for security */}
{block.copy ? (
<div
{...(block?.$ && block?.$.copy)} // Live preview attributes for copy
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(block.copy), // Sanitize HTML content
}}
className="prose" // Apply prose styling for better typography
/>
) : null}
</div>
</div>
);
})}
</div>
</section>
);
}
Loading