Skip to content

Commit 1ae39bb

Browse files
committed
Add optional what's included section to product pages
1 parent acae3eb commit 1ae39bb

File tree

8 files changed

+224
-6
lines changed

8 files changed

+224
-6
lines changed

src/components/ProductLanding.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Pricing } from '@/components/Pricing'
88
import { TableOfContents } from '@/components/TableOfContents'
99
import { CanvasPattern } from '@/components/CanvasPattern'
1010
import { RealTestimonials } from '@/components/RealTestimonials'
11+
import { WhatsIncluded } from '@/components/WhatsIncluded'
1112
import { Content } from '@/types'
1213
import RenderNumYearsExperience from '@/components/NumYearsExperience'
1314
import Image from 'next/image'
@@ -107,7 +108,8 @@ export function ProductLanding({ content }: { content: Content }) {
107108
description: 'One-time purchase with unlimited future access'
108109
}
109110
],
110-
testimonials: []
111+
testimonials: [],
112+
whatsIncluded: []
111113
};
112114

113115
// Use provided landing data or fallback to default
@@ -147,6 +149,16 @@ export function ProductLanding({ content }: { content: Content }) {
147149
description={safeDescription}
148150
features={landingData.features}
149151
/>
152+
153+
{/* What's Included Section - only render if whatsIncluded data exists */}
154+
{landingData?.whatsIncluded && landingData.whatsIncluded.length > 0 && (
155+
<WhatsIncluded
156+
items={landingData.whatsIncluded}
157+
sectionTitle="What's Included"
158+
sectionSubtitle="Everything you need to build production-ready solutions"
159+
/>
160+
)}
161+
150162
<NavBar />
151163
<TableOfContents content={content} />
152164
<FreeChapters

src/components/WhatsIncluded.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Image from 'next/image'
2+
import { StaticImageData } from 'next/image'
3+
4+
interface WhatsIncludedItem {
5+
title: string
6+
description: string
7+
image: string | StaticImageData
8+
imageAlt?: string
9+
}
10+
11+
interface WhatsIncludedProps {
12+
items: WhatsIncludedItem[]
13+
sectionTitle?: string
14+
sectionSubtitle?: string
15+
}
16+
17+
export function WhatsIncluded({
18+
items,
19+
sectionTitle = "What's Included",
20+
sectionSubtitle = "Everything you need to build production-ready solutions"
21+
}: WhatsIncludedProps) {
22+
if (!items || items.length === 0) {
23+
return null
24+
}
25+
26+
return (
27+
<div className="py-16 sm:py-24">
28+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
29+
{/* Section Header */}
30+
<div className="mx-auto max-w-2xl text-center">
31+
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
32+
{sectionTitle}
33+
</h2>
34+
<p className="mt-4 text-lg leading-8 text-slate-600 dark:text-slate-300">
35+
{sectionSubtitle}
36+
</p>
37+
</div>
38+
39+
{/* Items */}
40+
<div className="mx-auto mt-16 max-w-7xl">
41+
{items.map((item, index) => (
42+
<div
43+
key={index}
44+
className={`flex flex-col gap-8 lg:gap-16 ${
45+
index % 2 === 0 ? 'lg:flex-row' : 'lg:flex-row-reverse'
46+
} items-center ${index > 0 ? 'mt-16 lg:mt-24' : ''}`}
47+
>
48+
{/* Image */}
49+
<div className="w-full lg:w-1/2">
50+
<div className="relative overflow-hidden rounded-2xl bg-slate-50 dark:bg-slate-800/60 p-8">
51+
<Image
52+
src={item.image}
53+
alt={item.imageAlt || item.title}
54+
className="h-auto w-full object-cover rounded-xl"
55+
width={600}
56+
height={400}
57+
/>
58+
</div>
59+
</div>
60+
61+
{/* Content */}
62+
<div className="w-full lg:w-1/2">
63+
<div className="max-w-xl">
64+
<h3 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
65+
{item.title}
66+
</h3>
67+
<p className="mt-6 text-lg leading-8 text-slate-600 dark:text-slate-300">
68+
{item.description}
69+
</p>
70+
</div>
71+
</div>
72+
</div>
73+
))}
74+
</div>
75+
</div>
76+
</div>
77+
)
78+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { render, screen } from '@testing-library/react'
2+
import { WhatsIncluded } from '../WhatsIncluded'
3+
4+
// Mock Next.js Image component
5+
jest.mock('next/image', () => ({
6+
__esModule: true,
7+
default: ({ src, alt, ...props }: any) => (
8+
<img src={src} alt={alt} {...props} />
9+
),
10+
}))
11+
12+
describe('WhatsIncluded', () => {
13+
const mockItems = [
14+
{
15+
title: 'Interactive Jupyter Notebook',
16+
description: 'A complete, ready-to-run notebook that walks you through data processing.',
17+
image: '/images/test-notebook.webp',
18+
imageAlt: 'Jupyter Notebook interface'
19+
},
20+
{
21+
title: 'Next.js Application',
22+
description: 'Full source code for a modern web application.',
23+
image: '/images/test-app.webp',
24+
imageAlt: 'Next.js application'
25+
}
26+
]
27+
28+
it('renders nothing when no items provided', () => {
29+
const { container } = render(<WhatsIncluded items={[]} />)
30+
expect(container.firstChild).toBeNull()
31+
})
32+
33+
it('renders section title and subtitle', () => {
34+
render(<WhatsIncluded items={mockItems} />)
35+
36+
expect(screen.getByText("What's Included")).toBeInTheDocument()
37+
expect(screen.getByText("Everything you need to build production-ready solutions")).toBeInTheDocument()
38+
})
39+
40+
it('renders custom section title and subtitle', () => {
41+
render(
42+
<WhatsIncluded
43+
items={mockItems}
44+
sectionTitle="Custom Title"
45+
sectionSubtitle="Custom subtitle"
46+
/>
47+
)
48+
49+
expect(screen.getByText("Custom Title")).toBeInTheDocument()
50+
expect(screen.getByText("Custom subtitle")).toBeInTheDocument()
51+
})
52+
53+
it('renders all items with correct content', () => {
54+
render(<WhatsIncluded items={mockItems} />)
55+
56+
// Check first item
57+
expect(screen.getByText('Interactive Jupyter Notebook')).toBeInTheDocument()
58+
expect(screen.getByText('A complete, ready-to-run notebook that walks you through data processing.')).toBeInTheDocument()
59+
60+
// Check second item
61+
expect(screen.getByText('Next.js Application')).toBeInTheDocument()
62+
expect(screen.getByText('Full source code for a modern web application.')).toBeInTheDocument()
63+
})
64+
65+
it('renders images with correct alt text', () => {
66+
render(<WhatsIncluded items={mockItems} />)
67+
68+
const images = screen.getAllByRole('img')
69+
expect(images).toHaveLength(2)
70+
71+
expect(screen.getByAltText('Jupyter Notebook interface')).toBeInTheDocument()
72+
expect(screen.getByAltText('Next.js application')).toBeInTheDocument()
73+
})
74+
75+
it('uses title as alt text when imageAlt is not provided', () => {
76+
const itemsWithoutAlt = [
77+
{
78+
title: 'Test Item',
79+
description: 'Test description',
80+
image: '/images/test.webp'
81+
}
82+
]
83+
84+
render(<WhatsIncluded items={itemsWithoutAlt} />)
85+
86+
expect(screen.getByAltText('Test Item')).toBeInTheDocument()
87+
})
88+
89+
it('applies alternating layout classes correctly', () => {
90+
render(<WhatsIncluded items={mockItems} />)
91+
92+
const itemContainers = screen.getAllByText(/Interactive Jupyter Notebook|Next.js Application/).map(
93+
el => el.closest('.flex')
94+
)
95+
96+
// First item should have lg:flex-row
97+
expect(itemContainers[0]).toHaveClass('lg:flex-row')
98+
99+
// Second item should have lg:flex-row-reverse
100+
expect(itemContainers[1]).toHaveClass('lg:flex-row-reverse')
101+
})
102+
})

src/content/blog/rag-pipeline-tutorial/page.mdx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import Link from 'next/link';
33

44
import ragPipelineTutorialLogo from '@/images/rag-chatbot.webp';
55
import ragProfileImage from '@/images/rag-chatbot-profile.webp';
6-
import customRagChat from '@/images/custom-rag-chat-screenshot.webp'
7-
import customRagFlowchart from '@/images/chat-with-blog-flowchart.webp';
8-
import googleColabSecrets from '@/images/rag-tutorial-colab-secrets.webp';
6+
import customRagChat from '@/images/rag-pipeline-ui.webp'
7+
import customRagFlowchart from '@/images/rag-pipeline-tutorial.webp';
8+
import googleColabSecrets from '@/images/rag-pipeline-jupyter.webp';
99
import cloneExampleSite from '@/images/rag-pipeline-tutorial-clone-example-site.webp';
1010
import docsSanity from '@/images/rag-tutorial-docs-sanity.webp';
1111
import queryIndex from '@/images/rag-pipeline-tutorial-query-index.webp';
@@ -23,12 +23,12 @@ export const metadata = createMetadata({
2323
image: ragPipelineTutorialLogo,
2424
commerce: {
2525
isPaid: true,
26-
price: 49,
26+
price: 149,
2727
previewLength: 450,
2828
previewElements: 32,
2929
paywallHeader: "Master RAG Development: The Complete Package",
3030
paywallBody: "Get everything you need to build production-ready RAG applications: a step-by-step tutorial, ready-to-use Jupyter notebook for data processing, and a complete Next.js example site. Perfect for developers who want to add the most in-demand Gen AI skill to their toolkit.",
31-
buttonText: "Get the complete package ($49)",
31+
buttonText: "Get the complete package ($149)",
3232
miniPaywallTitle: "Master RAG pipeline creation",
3333
miniPaywallDescription: "Includes a Jupyter Notebook, a Next.js example site, and a step-by-step tutorial.",
3434
paywallImage: ragPipelineElements,
@@ -58,6 +58,26 @@ export const metadata = createMetadata({
5858
description: "Learn how to ground AI responses in your actual content for accurate, trustworthy results"
5959
}
6060
],
61+
whatsIncluded: [
62+
{
63+
title: "Interactive Jupyter Notebook",
64+
description: "A complete, ready-to-run notebook that walks you through data processing, embedding creation, and vector database setup. Simply add your API keys and run each cell to process your own content. Includes detailed explanations of chunking strategies, embedding techniques, and Pinecone integration.",
65+
image: googleColabSecrets,
66+
imageAlt: "Jupyter Notebook interface showing data processing and vector database creation"
67+
},
68+
{
69+
title: "Production-Ready Next.js Application",
70+
description: "Full source code for a modern web application built with the Vercel AI SDK. Features streaming responses, citation support, related content suggestions, and a beautiful chat interface. Deploy it to Vercel with one click or run it locally for development.",
71+
image: customRagChat,
72+
imageAlt: "Next.js chat application with streaming responses and citations"
73+
},
74+
{
75+
title: "Comprehensive Step-by-Step Tutorial",
76+
description: "Detailed written guide that explains every concept, from RAG fundamentals to advanced implementation patterns. Learn not just how to build it, but why each piece works and how to customize it for your specific use case. Perfect for developers who want to truly understand the technology.",
77+
image: customRagFlowchart,
78+
imageAlt: "RAG pipeline architecture diagram showing the complete system flow"
79+
}
80+
],
6181
contentSections: [
6282
{
6383
title: "Getting Started",
53.7 KB
Loading
95.3 KB
Loading

src/images/rag-pipeline-ui.webp

59 KB
Loading

src/types/metadata.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export interface ExtendedMetadata extends Metadata {
3535
description: string
3636
icon?: string
3737
}>
38+
whatsIncluded?: Array<{
39+
title: string
40+
description: string
41+
image: string | StaticImageData
42+
imageAlt?: string
43+
}>
3844
contentSections?: Array<{
3945
title: string
4046
subsections?: Array<string>

0 commit comments

Comments
 (0)