Skip to content

Commit f653161

Browse files
committed
Fix purchase recording
1 parent 583cd09 commit f653161

File tree

6 files changed

+46
-60
lines changed

6 files changed

+46
-60
lines changed

src/app/api/webhooks/stripe/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { sendReceiptEmail, SendReceiptEmailInput } from '@/lib/postmark'
55
import { getContentItemByDirectorySlug } from '@/lib/content-handlers'
66
import { COURSES_DISABLED } from '@/types'
77
import { stripeLogger as logger } from '@/utils/logger' // Import centralized logger
8-
import { getContentUrl } from '@/lib/content-url'
8+
import { getContentUrlFromObject } from '@/lib/content-url'
99
import { normalizeRouteOrFileSlug } from '@/lib/content-handlers'
1010

1111
// Initialize Stripe and Prisma
@@ -196,7 +196,12 @@ export async function POST(req: Request) {
196196
})
197197
logger.info('WEBHOOK: Existing email notifications found:', existingNotification ? 1 : 0)
198198
// Always use directorySlug for URLs to avoid double-prepending
199-
const productUrl = `${process.env.NEXT_PUBLIC_SITE_URL}${getContentUrl(type, directorySlug)}`;
199+
let productUrl = '';
200+
if (loadedContent) {
201+
productUrl = `${process.env.NEXT_PUBLIC_SITE_URL}${getContentUrlFromObject(loadedContent)}`;
202+
} else {
203+
productUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/${type}/${directorySlug}`;
204+
}
200205
logger.info('WEBHOOK: Preparing to send email')
201206
const emailInput: SendReceiptEmailInput = {
202207

src/app/checkout/result/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ContentCard } from '@/components/ContentCard';
88
import { Blog } from '@/types';
99
import { useSession, signIn } from 'next-auth/react';
1010
import { sendGTMEvent } from '@next/third-parties/google';
11-
import { getContentUrl } from '@/lib/content-url';
11+
import { getContentUrlFromObject } from '@/lib/content-url';
1212

1313
interface PurchasedContent {
1414
content: Blog;
@@ -68,7 +68,7 @@ function CheckoutResultContent() {
6868
}
6969

7070
if (authStatus === 'authenticated') {
71-
const contentUrl = getContentUrl(data.content.type || 'blog', data.content.directorySlug || '', true);
71+
const contentUrl = getContentUrlFromObject(data.content, true);
7272
setTimeout(() => router.push(contentUrl), 1000);
7373
}
7474
} catch (err) {
@@ -166,7 +166,7 @@ function CheckoutResultContent() {
166166
);
167167
}
168168

169-
const contentUrl = getContentUrl(content.content.type || 'blog', content.content.directorySlug || '', true);
169+
const contentUrl = getContentUrlFromObject(content.content, true);
170170

171171
return (
172172
<Container>

src/lib/content-handlers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// use server directive might be needed depending on your Next.js setup,
21
// Next.js 15.3 often benefits from it for RSCs involving file system/db
3-
import 'server-only';
2+
// import 'server-only';
43

54
import { Metadata } from 'next';
65
import fs from 'fs';

src/lib/content-url.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
1+
import type { Content } from '@/types/content';
2+
13
/**
2-
* Get the content URL based on content type and slug
3-
* @param contentType The content type (e.g., 'article', 'blog', 'course')
4-
* @param slug The content slug
5-
* @param keepLeadingSlash Whether to keep the leading slash (default: true)
6-
* @returns The URL path for the content
4+
* Returns the canonical relative URL path for a given Content object.
5+
* Throws if required fields are missing. Never prefixes with baseUrl.
6+
* @param content The Content object
7+
* @returns The canonical relative URL path for the content
78
*/
8-
export function getContentUrl(contentType: string, slug: string, keepLeadingSlash = true) {
9+
export function getContentUrlFromObject(content: Content, keepLeadingSlash = true): string {
10+
if (!content) throw new Error('No content object provided');
11+
const { type, directorySlug } = content as any;
12+
if (!type) throw new Error('Content object missing type');
13+
if (!directorySlug) throw new Error('Content object missing directorySlug');
914
// Remove any leading slashes from the slug
10-
const cleanSlug = slug.replace(/^\/+/, '');
11-
12-
// Generate the URL path based on content type
15+
const cleanSlug = directorySlug.replace(/^\/+/,'');
1316
let url = '';
14-
15-
if (contentType === 'article' || contentType === 'blog') {
17+
if (type === 'article' || type === 'blog') {
1618
url = `/blog/${cleanSlug}`;
17-
} else if (contentType === 'course') {
19+
} else if (type === 'course') {
1820
url = `/learn/courses/${cleanSlug}/0`;
1921
} else {
20-
// Fallback to a generic path
21-
url = `/${contentType}/${cleanSlug}`;
22+
url = `/${type}/${cleanSlug}`;
2223
}
23-
24-
// Remove leading slash if needed
2524
return (keepLeadingSlash || !url.startsWith('/')) ? url : url.substring(1);
2625
}

src/lib/postmark.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ServerClient } from 'postmark'
33
import { getContentWithComponentByDirectorySlug } from "./content-handlers"; // Use this instead of loadContent
44
import { ExtendedMetadata } from '@/types'
55
import { emailLogger as logger } from '@/utils/logger' // Import centralized email logger
6+
import { getContentUrlFromObject } from './content-url'
67

78
// Define the MessageSendingResponse interface based on Postmark API response
89
interface MessageSendingResponse {
@@ -100,15 +101,6 @@ interface SendFreeChaptersEmailInput {
100101
}[];
101102
}
102103

103-
// Function to get the content URL based on content type
104-
const getContentUrl = (type: string, slug: string) => {
105-
// Remove any leading slashes from the slug
106-
const cleanSlug = slug.replace(/^\/+/, '');
107-
108-
// For all content types, use the /blog/ path since we're only selling blog content
109-
return `/blog/${cleanSlug}`;
110-
};
111-
112104
/**
113105
* Extract preview content from a product's MDX content using server-side approach
114106
* @param productSlug The slug of the product
@@ -121,12 +113,10 @@ const getContentUrl = (type: string, slug: string) => {
121113
*/
122114
async function extractPreviewContent(productSlug: string): Promise<PreviewContentResult | null> {
123115
// Remove any leading slashes from the productSlug
124-
const normalizedSlug = productSlug.replace(/^\/+/, '');
125-
116+
const normalizedSlug = productSlug.replace(/^\/+/,'');
126117
// For blog posts, the content type is 'blog'
127118
// This assumes all products are blog posts - adjust if you have different content types
128119
const contentType = 'blog';
129-
130120
logger.info(`Extracting preview content for: ${contentType}/${normalizedSlug}`);
131121
// Use getContentWithComponentByDirectorySlug to load content + metadata
132122
const result = await getContentWithComponentByDirectorySlug(contentType, normalizedSlug);
@@ -136,13 +126,11 @@ async function extractPreviewContent(productSlug: string): Promise<PreviewConten
136126
}
137127
// Access the metadata via result.content
138128
const { content: metadata } = result;
139-
140-
// Create a simple HTML preview with a link to the content
141129
logger.info(`Creating preview content...`);
142130
try {
143131
// Get the base URL
144132
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
145-
const productUrl = `${baseUrl}${getContentUrl(contentType, normalizedSlug)}`;
133+
const productUrl = `${baseUrl}${getContentUrlFromObject(metadata)}`;
146134

147135
// Create a simple HTML preview with the description and a link
148136
const title = typeof metadata.title === 'string'
@@ -201,22 +189,29 @@ const sendFreeChaptersEmail = async (
201189
logger.debug(`[CONFIG] Base URL: ${baseUrl}`);
202190

203191
// Normalize the slug and construct the product URL
204-
const normalizedSlug = input.ProductSlug.replace(/^\/+/, '');
205-
const productUrl = `${baseUrl}${getContentUrl('article', normalizedSlug)}`;
192+
const normalizedSlug = input.ProductSlug.replace(/^\/+/,'');
193+
let productUrl = '';
194+
const result = await getContentWithComponentByDirectorySlug('blog', normalizedSlug);
195+
if (result && result.content) {
196+
productUrl = `${baseUrl}${getContentUrlFromObject(result.content)}`;
197+
} else {
198+
logger.warn(`Could not load content for slug: ${input.ProductSlug} to generate product URL.`);
199+
productUrl = `${baseUrl}/blog/${normalizedSlug}`; // fallback
200+
}
206201
logger.debug(`[CONFIG] Product URL: ${productUrl}`);
207202

208203
// Extract preview content from the actual product
209204
logger.info(`[PROCESS] Starting content extraction...`);
210205

211-
const result = await extractPreviewContent(normalizedSlug);
206+
const previewResult = await extractPreviewContent(normalizedSlug);
212207

213-
if (!result) {
208+
if (!previewResult) {
214209
logger.warn(`No content found for slug: ${input.ProductSlug}`);
215210
return null;
216211
}
217212

218213
// The result from extractPreviewContent already contains the correct metadata
219-
const { previewContent, metadata } = result;
214+
const { previewContent, metadata } = previewResult;
220215

221216
logger.debug(`[DEBUG] Result extracted content length: ${previewContent.length} characters`);
222217

src/utils/createMetadata.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { generateOgUrl } from '@/utils/ogUrl'
33
import path from 'path'
44
import { Metadata } from 'next'
55
import { logger } from './logger'
6+
import { getContentUrlFromObject } from '@/lib/content-url'
67

78
// Create a specialized logger for metadata operations
89
const metaLogger = logger.forCategory('metadata');
@@ -115,22 +116,6 @@ function getTypeFromPath(filePath: string): 'blog' | 'course' | 'video' | 'demo'
115116
return 'blog';
116117
}
117118

118-
/**
119-
* Generate a URL for any content type and slug
120-
* @param type Content type
121-
* @param slug Content slug
122-
* @returns Full URL path
123-
*/
124-
function getUrlForContent(type: string, slug: string): string {
125-
const baseDir = TYPE_PATHS[type];
126-
127-
if (!baseDir) {
128-
return `/${type}/${slug}`;
129-
}
130-
131-
return `/${baseDir}/${slug}`;
132-
}
133-
134119
/**
135120
* Creates a fully typed ExtendedMetadata object with all required fields
136121
* @param params Partial metadata parameters
@@ -240,7 +225,10 @@ export function createMetadata(params: MetadataParams): ExtendedMetadata {
240225
}
241226

242227
// Generate a URL using the type and slug
243-
const contentUrl = finalSlug ? getUrlForContent(contentType, finalSlug) : undefined;
228+
let contentUrl: string | undefined = undefined;
229+
if (contentType && finalSlug) {
230+
contentUrl = getContentUrlFromObject({ type: contentType, directorySlug: finalSlug } as any);
231+
}
244232

245233
// Fix the type error: pass null for slug parameter to match the expected type
246234
const ogImageUrl = generateOgUrl({

0 commit comments

Comments
 (0)