shipd 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base-package/app/globals.css +126 -0
- package/base-package/app/layout.tsx +53 -0
- package/base-package/app/page.tsx +15 -0
- package/base-package/base.config.json +57 -0
- package/base-package/components/ui/avatar.tsx +53 -0
- package/base-package/components/ui/badge.tsx +46 -0
- package/base-package/components/ui/button.tsx +59 -0
- package/base-package/components/ui/card.tsx +92 -0
- package/base-package/components/ui/chart.tsx +353 -0
- package/base-package/components/ui/checkbox.tsx +32 -0
- package/base-package/components/ui/dialog.tsx +135 -0
- package/base-package/components/ui/dropdown-menu.tsx +257 -0
- package/base-package/components/ui/form.tsx +167 -0
- package/base-package/components/ui/input.tsx +21 -0
- package/base-package/components/ui/label.tsx +24 -0
- package/base-package/components/ui/progress.tsx +31 -0
- package/base-package/components/ui/resizable.tsx +56 -0
- package/base-package/components/ui/select.tsx +185 -0
- package/base-package/components/ui/separator.tsx +28 -0
- package/base-package/components/ui/sheet.tsx +139 -0
- package/base-package/components/ui/skeleton.tsx +13 -0
- package/base-package/components/ui/sonner.tsx +25 -0
- package/base-package/components/ui/switch.tsx +31 -0
- package/base-package/components/ui/tabs.tsx +66 -0
- package/base-package/components/ui/textarea.tsx +18 -0
- package/base-package/components/ui/toggle-group.tsx +73 -0
- package/base-package/components/ui/toggle.tsx +47 -0
- package/base-package/components/ui/tooltip.tsx +61 -0
- package/base-package/components.json +21 -0
- package/base-package/eslint.config.mjs +16 -0
- package/base-package/lib/utils.ts +6 -0
- package/base-package/middleware.ts +12 -0
- package/base-package/next.config.ts +27 -0
- package/base-package/package.json +49 -0
- package/base-package/postcss.config.mjs +5 -0
- package/base-package/public/favicon.svg +4 -0
- package/base-package/tailwind.config.ts +89 -0
- package/base-package/tsconfig.json +27 -0
- package/dist/index.js +1858 -956
- package/docs-template/README.md +74 -0
- package/features/ai-chat/README.md +316 -0
- package/features/ai-chat/app/api/chat/route.ts +16 -0
- package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
- package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
- package/features/ai-chat/feature.config.json +22 -0
- package/features/analytics/README.md +364 -0
- package/features/analytics/feature.config.json +20 -0
- package/features/analytics/lib/posthog.ts +36 -0
- package/features/auth/README.md +409 -0
- package/features/auth/app/api/auth/[...all]/route.ts +4 -0
- package/features/auth/app/dashboard/layout.tsx +15 -0
- package/features/auth/app/dashboard/page.tsx +140 -0
- package/features/auth/app/sign-in/page.tsx +228 -0
- package/features/auth/app/sign-up/page.tsx +243 -0
- package/features/auth/auth-schema.ts +47 -0
- package/features/auth/components/auth/setup-instructions.tsx +123 -0
- package/features/auth/feature.config.json +33 -0
- package/features/auth/lib/auth-client.ts +8 -0
- package/features/auth/lib/auth.ts +295 -0
- package/features/auth/lib/email-stub.ts +55 -0
- package/features/auth/lib/email.ts +47 -0
- package/features/auth/middleware.patch.ts +43 -0
- package/features/database/README.md +312 -0
- package/features/database/db/drizzle.ts +48 -0
- package/features/database/db/schema.ts +21 -0
- package/features/database/drizzle.config.ts +13 -0
- package/features/database/feature.config.json +30 -0
- package/features/email/README.md +341 -0
- package/features/email/emails/components/layout.tsx +181 -0
- package/features/email/emails/password-reset.tsx +67 -0
- package/features/email/emails/payment-failed.tsx +167 -0
- package/features/email/emails/subscription-confirmation.tsx +129 -0
- package/features/email/emails/welcome.tsx +100 -0
- package/features/email/feature.config.json +22 -0
- package/features/email/lib/email.ts +118 -0
- package/features/file-upload/README.md +329 -0
- package/features/file-upload/app/api/upload-image/route.ts +64 -0
- package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
- package/features/file-upload/feature.config.json +23 -0
- package/features/file-upload/lib/upload-image.ts +28 -0
- package/features/marketing-landing/README.md +333 -0
- package/features/marketing-landing/app/page.tsx +25 -0
- package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
- package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
- package/features/marketing-landing/components/homepage/footer.tsx +53 -0
- package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
- package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
- package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
- package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
- package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
- package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
- package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
- package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
- package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
- package/features/marketing-landing/components/logos/Polar.tsx +7 -0
- package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
- package/features/marketing-landing/components/logos/index.ts +6 -0
- package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
- package/features/marketing-landing/feature.config.json +23 -0
- package/features/payments/README.md +375 -0
- package/features/payments/app/api/subscription/route.ts +25 -0
- package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/features/payments/app/dashboard/payment/page.tsx +126 -0
- package/features/payments/app/success/page.tsx +123 -0
- package/features/payments/feature.config.json +31 -0
- package/features/payments/lib/polar-products.ts +49 -0
- package/features/payments/lib/subscription.ts +148 -0
- package/features/payments/payments-schema.ts +30 -0
- package/features/seo/README.md +302 -0
- package/features/seo/app/blog/[slug]/page.tsx +314 -0
- package/features/seo/app/blog/page.tsx +107 -0
- package/features/seo/app/robots.txt +13 -0
- package/features/seo/app/sitemap.ts +70 -0
- package/features/seo/feature.config.json +19 -0
- package/features/seo/lib/seo-utils.ts +163 -0
- package/package.json +3 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payments",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Polar.sh subscription management and payment processing",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@polar-sh/sdk": "^0.42.1"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {},
|
|
9
|
+
"envVars": [
|
|
10
|
+
"POLAR_ACCESS_TOKEN",
|
|
11
|
+
"POLAR_SUCCESS_URL",
|
|
12
|
+
"POLAR_WEBHOOK_SECRET",
|
|
13
|
+
"NEXT_PUBLIC_STARTER_TIER",
|
|
14
|
+
"NEXT_PUBLIC_STARTER_SLUG",
|
|
15
|
+
"NEXT_PUBLIC_APP_URL"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"app/dashboard/payment/**/*",
|
|
19
|
+
"app/success/**/*",
|
|
20
|
+
"app/api/subscription/**/*",
|
|
21
|
+
"lib/subscription.ts",
|
|
22
|
+
"lib/polar-products.ts",
|
|
23
|
+
"payments-schema.ts"
|
|
24
|
+
],
|
|
25
|
+
"requires": [
|
|
26
|
+
"auth",
|
|
27
|
+
"database"
|
|
28
|
+
],
|
|
29
|
+
"conflicts": []
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Polar } from '@polar-sh/sdk';
|
|
2
|
+
|
|
3
|
+
const polarClient = new Polar({
|
|
4
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export interface ProductDetails {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string | null;
|
|
11
|
+
prices: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
amount: number;
|
|
14
|
+
currency: string;
|
|
15
|
+
recurring_interval: 'month' | 'year';
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch product details from Polar
|
|
21
|
+
*/
|
|
22
|
+
export async function getProductDetails(
|
|
23
|
+
productId: string
|
|
24
|
+
): Promise<ProductDetails | null> {
|
|
25
|
+
try {
|
|
26
|
+
const product = await polarClient.products.get({
|
|
27
|
+
id: productId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!product) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: product.id,
|
|
36
|
+
name: product.name,
|
|
37
|
+
description: product.description || null,
|
|
38
|
+
prices: (product.prices || []).map((price) => ({
|
|
39
|
+
id: price.id,
|
|
40
|
+
amount: price.priceAmount || 0,
|
|
41
|
+
currency: price.priceCurrency || 'usd',
|
|
42
|
+
recurring_interval: (price.recurringInterval as 'month' | 'year') || 'month',
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error fetching product from Polar:', error);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
import { db } from "@/db/drizzle";
|
|
3
|
+
import { subscription } from "@/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { headers } from "next/headers";
|
|
6
|
+
|
|
7
|
+
export type SubscriptionDetails = {
|
|
8
|
+
id: string;
|
|
9
|
+
productId: string;
|
|
10
|
+
status: string;
|
|
11
|
+
amount: number;
|
|
12
|
+
currency: string;
|
|
13
|
+
recurringInterval: string;
|
|
14
|
+
currentPeriodStart: Date;
|
|
15
|
+
currentPeriodEnd: Date;
|
|
16
|
+
cancelAtPeriodEnd: boolean;
|
|
17
|
+
canceledAt: Date | null;
|
|
18
|
+
organizationId: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SubscriptionDetailsResult = {
|
|
22
|
+
hasSubscription: boolean;
|
|
23
|
+
subscription?: SubscriptionDetails;
|
|
24
|
+
error?: string;
|
|
25
|
+
errorType?: "CANCELED" | "EXPIRED" | "GENERAL";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function getSubscriptionDetails(): Promise<SubscriptionDetailsResult> {
|
|
29
|
+
try {
|
|
30
|
+
const session = await auth.api.getSession({
|
|
31
|
+
headers: await headers(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!session?.user?.id) {
|
|
35
|
+
return { hasSubscription: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const userSubscriptions = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(subscription)
|
|
41
|
+
.where(eq(subscription.userId, session.user.id));
|
|
42
|
+
|
|
43
|
+
if (!userSubscriptions.length) {
|
|
44
|
+
return { hasSubscription: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get the most recent active subscription
|
|
48
|
+
const activeSubscription = userSubscriptions
|
|
49
|
+
.filter((sub) => sub.status === "active")
|
|
50
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
51
|
+
|
|
52
|
+
if (!activeSubscription) {
|
|
53
|
+
// Check for canceled or expired subscriptions
|
|
54
|
+
const latestSubscription = userSubscriptions
|
|
55
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
56
|
+
|
|
57
|
+
if (latestSubscription) {
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const isExpired = new Date(latestSubscription.currentPeriodEnd) < now;
|
|
60
|
+
const isCanceled = latestSubscription.status === "canceled";
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
hasSubscription: true,
|
|
64
|
+
subscription: {
|
|
65
|
+
id: latestSubscription.id,
|
|
66
|
+
productId: latestSubscription.productId,
|
|
67
|
+
status: latestSubscription.status,
|
|
68
|
+
amount: latestSubscription.amount,
|
|
69
|
+
currency: latestSubscription.currency,
|
|
70
|
+
recurringInterval: latestSubscription.recurringInterval,
|
|
71
|
+
currentPeriodStart: latestSubscription.currentPeriodStart,
|
|
72
|
+
currentPeriodEnd: latestSubscription.currentPeriodEnd,
|
|
73
|
+
cancelAtPeriodEnd: latestSubscription.cancelAtPeriodEnd,
|
|
74
|
+
canceledAt: latestSubscription.canceledAt,
|
|
75
|
+
organizationId: null,
|
|
76
|
+
},
|
|
77
|
+
error: isCanceled ? "Subscription has been canceled" : isExpired ? "Subscription has expired" : "Subscription is not active",
|
|
78
|
+
errorType: isCanceled ? "CANCELED" : isExpired ? "EXPIRED" : "GENERAL",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { hasSubscription: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
hasSubscription: true,
|
|
87
|
+
subscription: {
|
|
88
|
+
id: activeSubscription.id,
|
|
89
|
+
productId: activeSubscription.productId,
|
|
90
|
+
status: activeSubscription.status,
|
|
91
|
+
amount: activeSubscription.amount,
|
|
92
|
+
currency: activeSubscription.currency,
|
|
93
|
+
recurringInterval: activeSubscription.recurringInterval,
|
|
94
|
+
currentPeriodStart: activeSubscription.currentPeriodStart,
|
|
95
|
+
currentPeriodEnd: activeSubscription.currentPeriodEnd,
|
|
96
|
+
cancelAtPeriodEnd: activeSubscription.cancelAtPeriodEnd,
|
|
97
|
+
canceledAt: activeSubscription.canceledAt,
|
|
98
|
+
organizationId: null,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error fetching subscription details:", error);
|
|
103
|
+
return {
|
|
104
|
+
hasSubscription: false,
|
|
105
|
+
error: "Failed to load subscription details",
|
|
106
|
+
errorType: "GENERAL",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Simple helper to check if user has an active subscription
|
|
112
|
+
export async function isUserSubscribed(): Promise<boolean> {
|
|
113
|
+
const result = await getSubscriptionDetails();
|
|
114
|
+
return result.hasSubscription && result.subscription?.status === "active";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Helper to check if user has access to a specific product/tier
|
|
118
|
+
export async function hasAccessToProduct(productId: string): Promise<boolean> {
|
|
119
|
+
const result = await getSubscriptionDetails();
|
|
120
|
+
return (
|
|
121
|
+
result.hasSubscription &&
|
|
122
|
+
result.subscription?.status === "active" &&
|
|
123
|
+
result.subscription?.productId === productId
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Helper to get user's current subscription status
|
|
128
|
+
export async function getUserSubscriptionStatus(): Promise<"active" | "canceled" | "expired" | "none"> {
|
|
129
|
+
const result = await getSubscriptionDetails();
|
|
130
|
+
|
|
131
|
+
if (!result.hasSubscription) {
|
|
132
|
+
return "none";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result.subscription?.status === "active") {
|
|
136
|
+
return "active";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.errorType === "CANCELED") {
|
|
140
|
+
return "canceled";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (result.errorType === "EXPIRED") {
|
|
144
|
+
return "expired";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return "none";
|
|
148
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
// Subscription table for Polar webhook data
|
|
4
|
+
// Note: userId references user.id from db/schema.ts (user table is added by auth module)
|
|
5
|
+
export const subscription = pgTable("subscription", {
|
|
6
|
+
id: text("id").primaryKey(),
|
|
7
|
+
createdAt: timestamp("createdAt").notNull(),
|
|
8
|
+
modifiedAt: timestamp("modifiedAt"),
|
|
9
|
+
amount: integer("amount").notNull(),
|
|
10
|
+
currency: text("currency").notNull(),
|
|
11
|
+
recurringInterval: text("recurringInterval").notNull(),
|
|
12
|
+
status: text("status").notNull(),
|
|
13
|
+
currentPeriodStart: timestamp("currentPeriodStart").notNull(),
|
|
14
|
+
currentPeriodEnd: timestamp("currentPeriodEnd").notNull(),
|
|
15
|
+
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd").notNull().default(false),
|
|
16
|
+
canceledAt: timestamp("canceledAt"),
|
|
17
|
+
startedAt: timestamp("startedAt").notNull(),
|
|
18
|
+
endsAt: timestamp("endsAt"),
|
|
19
|
+
endedAt: timestamp("endedAt"),
|
|
20
|
+
customerId: text("customerId").notNull(),
|
|
21
|
+
productId: text("productId").notNull(),
|
|
22
|
+
discountId: text("discountId"),
|
|
23
|
+
checkoutId: text("checkoutId").notNull(),
|
|
24
|
+
customerCancellationReason: text("customerCancellationReason"),
|
|
25
|
+
customerCancellationComment: text("customerCancellationComment"),
|
|
26
|
+
metadata: text("metadata"), // JSON string
|
|
27
|
+
customFieldData: text("customFieldData"), // JSON string
|
|
28
|
+
userId: text("userId"), // References user.id (user table from auth module)
|
|
29
|
+
});
|
|
30
|
+
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# SEO Module - Integration Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The SEO module provides a complete SEO foundation for your Next.js application, including automated sitemap generation, robots.txt, blog structure, and structured data utilities.
|
|
6
|
+
|
|
7
|
+
## What's Included
|
|
8
|
+
|
|
9
|
+
### Files Added
|
|
10
|
+
|
|
11
|
+
- `app/sitemap.ts` - Automated sitemap generation
|
|
12
|
+
- `app/robots.txt` - Search engine crawler instructions
|
|
13
|
+
- `app/blog/` - Complete blog structure with example posts
|
|
14
|
+
- `lib/seo-utils.ts` - Helper functions for structured data and metadata
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
✅ **Automated Sitemap Generation**
|
|
19
|
+
- Dynamically generates sitemap.xml
|
|
20
|
+
- Includes all major routes (home, blog, pricing, docs, etc.)
|
|
21
|
+
- Supports blog posts with proper priorities and change frequencies
|
|
22
|
+
|
|
23
|
+
✅ **Robots.txt**
|
|
24
|
+
- Allows all crawlers by default
|
|
25
|
+
- Blocks sensitive routes (API, dashboard, auth pages)
|
|
26
|
+
- Points to sitemap location
|
|
27
|
+
|
|
28
|
+
✅ **Blog Structure**
|
|
29
|
+
- Blog listing page (`/blog`)
|
|
30
|
+
- Individual blog post pages (`/blog/[slug]`)
|
|
31
|
+
- Markdown content support via react-markdown
|
|
32
|
+
- SEO-optimized metadata for each post
|
|
33
|
+
|
|
34
|
+
✅ **Structured Data Utilities**
|
|
35
|
+
- JSON-LD generation for WebApplication
|
|
36
|
+
- BlogPosting structured data
|
|
37
|
+
- Organization schema
|
|
38
|
+
- OpenGraph and Twitter Card helpers
|
|
39
|
+
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
This module requires:
|
|
43
|
+
- `react-markdown` (for blog content)
|
|
44
|
+
- `remark-gfm` (for GitHub Flavored Markdown)
|
|
45
|
+
|
|
46
|
+
These are automatically added to your `package.json` when you append this module.
|
|
47
|
+
|
|
48
|
+
## Environment Variables
|
|
49
|
+
|
|
50
|
+
Add to your `.env.local`:
|
|
51
|
+
|
|
52
|
+
```env
|
|
53
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For production, set this to your actual domain:
|
|
57
|
+
```env
|
|
58
|
+
NEXT_PUBLIC_APP_URL=https://yourdomain.com
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Manual Integration Steps
|
|
62
|
+
|
|
63
|
+
### 1. Verify Files Were Copied
|
|
64
|
+
|
|
65
|
+
After appending, check that these files exist:
|
|
66
|
+
- `app/sitemap.ts`
|
|
67
|
+
- `app/robots.txt`
|
|
68
|
+
- `app/blog/page.tsx`
|
|
69
|
+
- `app/blog/[slug]/page.tsx`
|
|
70
|
+
- `lib/seo-utils.ts`
|
|
71
|
+
|
|
72
|
+
### 2. Update Sitemap with Your Routes
|
|
73
|
+
|
|
74
|
+
Edit `app/sitemap.ts` to include all your application routes:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Add your custom routes
|
|
78
|
+
{
|
|
79
|
+
url: `${baseUrl}/your-custom-page`,
|
|
80
|
+
lastModified: new Date(),
|
|
81
|
+
changeFrequency: 'monthly',
|
|
82
|
+
priority: 0.8,
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Update Blog Posts
|
|
87
|
+
|
|
88
|
+
Edit `app/blog/page.tsx` and `app/blog/[slug]/page.tsx` to use your actual blog content. In production, you might:
|
|
89
|
+
- Fetch from a CMS (Contentful, Sanity, etc.)
|
|
90
|
+
- Read from markdown files
|
|
91
|
+
- Query from a database
|
|
92
|
+
|
|
93
|
+
### 4. Add Structured Data to Layout
|
|
94
|
+
|
|
95
|
+
In your `app/layout.tsx`, add structured data:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { generateWebAppStructuredData } from '@/lib/seo-utils';
|
|
99
|
+
|
|
100
|
+
export default function RootLayout({ children }) {
|
|
101
|
+
const jsonLd = generateWebAppStructuredData(
|
|
102
|
+
'Your App Name',
|
|
103
|
+
'Your app description',
|
|
104
|
+
'https://yourdomain.com'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<html>
|
|
109
|
+
<head>
|
|
110
|
+
<script
|
|
111
|
+
type="application/ld+json"
|
|
112
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
113
|
+
/>
|
|
114
|
+
</head>
|
|
115
|
+
<body>{children}</body>
|
|
116
|
+
</html>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 5. Add Structured Data to Blog Posts
|
|
122
|
+
|
|
123
|
+
In `app/blog/[slug]/page.tsx`, add blog post structured data:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { generateBlogPostStructuredData } from '@/lib/seo-utils';
|
|
127
|
+
|
|
128
|
+
export default function BlogPost({ params }) {
|
|
129
|
+
const post = getPost(params.slug);
|
|
130
|
+
|
|
131
|
+
const jsonLd = generateBlogPostStructuredData(
|
|
132
|
+
post.title,
|
|
133
|
+
post.description,
|
|
134
|
+
`https://yourdomain.com/blog/${post.slug}`,
|
|
135
|
+
post.date,
|
|
136
|
+
post.dateModified,
|
|
137
|
+
post.author,
|
|
138
|
+
post.image
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
<script
|
|
144
|
+
type="application/ld+json"
|
|
145
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
146
|
+
/>
|
|
147
|
+
{/* Blog post content */}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Usage Examples
|
|
154
|
+
|
|
155
|
+
### Generate OpenGraph Metadata
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { generateOpenGraphMetadata } from '@/lib/seo-utils';
|
|
159
|
+
|
|
160
|
+
export const metadata = {
|
|
161
|
+
...generateOpenGraphMetadata(
|
|
162
|
+
'Page Title',
|
|
163
|
+
'Page description',
|
|
164
|
+
'https://yourdomain.com/page',
|
|
165
|
+
'https://yourdomain.com/og-image.png'
|
|
166
|
+
),
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Generate Twitter Card Metadata
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { generateTwitterCardMetadata } from '@/lib/seo-utils';
|
|
174
|
+
|
|
175
|
+
export const metadata = {
|
|
176
|
+
twitter: generateTwitterCardMetadata(
|
|
177
|
+
'Page Title',
|
|
178
|
+
'Page description',
|
|
179
|
+
'https://yourdomain.com/twitter-image.png',
|
|
180
|
+
'yourhandle'
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Testing
|
|
186
|
+
|
|
187
|
+
1. **Test Sitemap**: Visit `http://localhost:3000/sitemap.xml`
|
|
188
|
+
2. **Test Robots.txt**: Visit `http://localhost:3000/robots.txt`
|
|
189
|
+
3. **Test Blog**: Visit `http://localhost:3000/blog`
|
|
190
|
+
4. **Test Blog Post**: Visit `http://localhost:3000/blog/getting-started-with-saas`
|
|
191
|
+
|
|
192
|
+
## Customization
|
|
193
|
+
|
|
194
|
+
### Update Robots.txt Rules
|
|
195
|
+
|
|
196
|
+
Edit `app/robots.txt` to customize crawler access:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
# Disallow specific paths
|
|
200
|
+
Disallow: /admin/
|
|
201
|
+
Disallow: /private/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Add More Routes to Sitemap
|
|
205
|
+
|
|
206
|
+
Edit `app/sitemap.ts` and add your routes to the return array.
|
|
207
|
+
|
|
208
|
+
### Customize Blog Structure
|
|
209
|
+
|
|
210
|
+
The blog pages are fully customizable. Update:
|
|
211
|
+
- `app/blog/page.tsx` - Blog listing page
|
|
212
|
+
- `app/blog/[slug]/page.tsx` - Individual blog post page
|
|
213
|
+
|
|
214
|
+
## Related Modules
|
|
215
|
+
|
|
216
|
+
- **Marketing Landing Page** - Works great with SEO for discoverability
|
|
217
|
+
- **Documentation Pages** - Can be included in sitemap
|
|
218
|
+
|
|
219
|
+
## Module Status
|
|
220
|
+
|
|
221
|
+
✅ **Ready to Use** - Works immediately after append
|
|
222
|
+
✅ **Template Variables** - Uses {{APP_URL}} in robots.txt
|
|
223
|
+
✅ **No Conflicts** - Safe to append to any project
|
|
224
|
+
|
|
225
|
+
## Troubleshooting
|
|
226
|
+
|
|
227
|
+
### Sitemap Not Generating
|
|
228
|
+
|
|
229
|
+
- Ensure `NEXT_PUBLIC_APP_URL` is set in `.env.local`
|
|
230
|
+
- Check that `app/sitemap.ts` exists
|
|
231
|
+
- Verify Next.js version supports `MetadataRoute.Sitemap`
|
|
232
|
+
|
|
233
|
+
### Blog Posts Not Showing
|
|
234
|
+
|
|
235
|
+
- Check that blog posts array is populated in `app/blog/page.tsx`
|
|
236
|
+
- Verify markdown content exists in `app/blog/[slug]/page.tsx`
|
|
237
|
+
- Ensure `react-markdown` and `remark-gfm` are installed
|
|
238
|
+
|
|
239
|
+
### Structured Data Not Validating
|
|
240
|
+
|
|
241
|
+
- Use [Google's Rich Results Test](https://search.google.com/test/rich-results)
|
|
242
|
+
- Verify JSON-LD syntax is correct
|
|
243
|
+
- Check that required fields are present
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## AI Integration Prompt
|
|
248
|
+
|
|
249
|
+
**For AI Assistants:** Use this prompt to understand and integrate the SEO module into an existing Next.js application.
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
The SEO module has been appended to this Next.js project. Your task is to ensure it's properly integrated and functional.
|
|
253
|
+
|
|
254
|
+
**Installed Packages:**
|
|
255
|
+
- react-markdown@^10.1.0 - Markdown rendering for blog posts
|
|
256
|
+
- remark-gfm@^4.0.1 - GitHub Flavored Markdown support
|
|
257
|
+
|
|
258
|
+
**Files Added:**
|
|
259
|
+
- app/sitemap.ts - Automated sitemap generation (Next.js MetadataRoute)
|
|
260
|
+
- app/robots.txt - Search engine crawler instructions
|
|
261
|
+
- app/blog/page.tsx - Blog listing page with example posts
|
|
262
|
+
- app/blog/[slug]/page.tsx - Individual blog post page with markdown support
|
|
263
|
+
- lib/seo-utils.ts - Structured data helpers (JSON-LD, OpenGraph, Twitter Cards)
|
|
264
|
+
|
|
265
|
+
**Environment Variables Required:**
|
|
266
|
+
- NEXT_PUBLIC_APP_URL - Application URL (e.g., http://localhost:3000 or https://yourdomain.com)
|
|
267
|
+
|
|
268
|
+
**Integration Steps:**
|
|
269
|
+
1. Verify app/sitemap.ts exists and exports default sitemap() function
|
|
270
|
+
2. Check that app/robots.txt exists (Next.js serves this automatically)
|
|
271
|
+
3. Verify blog structure exists: app/blog/page.tsx and app/blog/[slug]/page.tsx
|
|
272
|
+
4. Check that lib/seo-utils.ts exists with structured data helper functions
|
|
273
|
+
5. Ensure NEXT_PUBLIC_APP_URL is set in .env.local
|
|
274
|
+
6. Test sitemap: Visit /sitemap.xml (should return XML)
|
|
275
|
+
7. Test robots.txt: Visit /robots.txt (should return text)
|
|
276
|
+
8. Test blog: Visit /blog (should show blog listing)
|
|
277
|
+
|
|
278
|
+
**Key Integration Points:**
|
|
279
|
+
- Sitemap generation: app/sitemap.ts uses Next.js MetadataRoute.Sitemap API
|
|
280
|
+
- Robots.txt: Next.js automatically serves app/robots.txt at /robots.txt
|
|
281
|
+
- Blog structure: Example blog posts are hardcoded; replace with CMS/database in production
|
|
282
|
+
- Structured data: Use lib/seo-utils.ts functions to generate JSON-LD for pages
|
|
283
|
+
- Template variables: robots.txt uses {{APP_URL}} which gets replaced during init
|
|
284
|
+
|
|
285
|
+
**Dependencies:**
|
|
286
|
+
- No dependencies on other Shipd modules
|
|
287
|
+
- Can be installed standalone
|
|
288
|
+
|
|
289
|
+
**Common Issues to Check:**
|
|
290
|
+
- If sitemap not generating: Check NEXT_PUBLIC_APP_URL is set, verify app/sitemap.ts exists
|
|
291
|
+
- If blog posts not showing: Check blog posts array in app/blog/page.tsx is populated
|
|
292
|
+
- If markdown not rendering: Ensure react-markdown and remark-gfm are installed
|
|
293
|
+
- If structured data invalid: Use Google Rich Results Test to validate JSON-LD
|
|
294
|
+
|
|
295
|
+
**Next Steps After Integration:**
|
|
296
|
+
- Update sitemap.ts with all your application routes
|
|
297
|
+
- Customize blog posts in app/blog/page.tsx and app/blog/[slug]/page.tsx
|
|
298
|
+
- Add structured data to app/layout.tsx using generateWebAppStructuredData()
|
|
299
|
+
- Add structured data to blog posts using generateBlogPostStructuredData()
|
|
300
|
+
- Update robots.txt rules if needed (currently allows all, blocks /api/, /dashboard/)
|
|
301
|
+
- Replace hardcoded blog posts with CMS/database queries in production
|
|
302
|
+
```
|