diff --git a/content/blog/premium-fumadocs.mdx b/content/blog/premium-fumadocs.mdx new file mode 100644 index 0000000..45c8d3a --- /dev/null +++ b/content/blog/premium-fumadocs.mdx @@ -0,0 +1,67 @@ +--- +title: "Premium: What is Fumadocs" +description: "Introducing Fumadocs, a docs framework that you can break." +date: "2025-08-30" +published: true +premium: true +categories: ["development", "nextjs"] +author: "fox" +image: "/images/blog/post-1.png" +--- + +Fumadocs was created because I wanted a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**. + +## Philosophy + +**Less Abstraction:** Fumadocs expects you to write code and cooperate with the rest of your software. +While most frameworks are configured with a configuration file, they usually lack flexibility when you hope to tune its details. +You can't control how they render the page nor the internal logic. Fumadocs shows you how the app works, instead of a single configuration file. + +**Next.js Fundamentals:** It gives you the utilities and a good-looking UI. +You are still using features of Next.js App Router, like **Static Site Generation**. There is nothing new for Next.js developers, so you can use it with confidence. + + + +**Opinionated on UI:** The only thing Fumadocs UI (the default theme) offers is **User Interface**. The UI is opinionated for bringing better mobile responsiveness and user experience. +Instead, we use a much more flexible approach inspired by Shadcn UI — [Fumadocs CLI](/docs/cli), so we can iterate our design quick, and welcome for more feedback about the UI. + +## Why Fumadocs + +Fumadocs is designed with flexibility in mind. + +You can use `fumadocs-core` as a headless UI library and bring your own styles. +Fumadocs MDX is also a useful library to handle MDX content in Next.js. It also includes: + +- Many built-in components. +- Typescript Twoslash, OpenAPI, and Math (KaTeX) integrations. +- Fast and optimized by default, natively built on App Router. +- Tight integration with Next.js, you can add it to an existing Next.js project easily. + +You can read [Comparisons](/docs/comparisons) if you're interested. + +### Documentation + +Fumadocs focuses on **authoring experience**, it provides a beautiful theme and many docs automation tools. + +It helps you to iterate your codebase faster while never leaving your docs behind. +You can take this site as an example of docs site built with Fumadocs. + +### Blog sites + +Since Next.js is already a powerful framework, most features can be implemented with **just Next.js**. + +Fumadocs provides additional tooling for Next.js, including syntax highlighting, document search, and a default theme (Fumadocs UI). +It helps you to avoid reinventing the wheels. + +## When to use Fumadocs + +For most of the web applications, vanilla React.js is no longer enough. +Nowadays, we also wish to have a blog, a showcase page, a FAQ page, etc. With a +fancy UI that's breathtaking, in these cases, Fumadocs can help you build the +docs easier, with less boilerplate. + +Fumadocs is maintained by Fuma and many contributors, with care on the maintainability of codebase. +While we don't aim to offer every functionality people wanted, we're more focused on making basic features perfect and well-maintained. +You can also help Fumadocs to be more useful by contributing! + + diff --git a/messages/en.json b/messages/en.json index cbd0f4d..9399fc2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -281,7 +281,20 @@ "all": "All", "noPostsFound": "No posts found", "allPosts": "All Posts", - "morePosts": "More Posts" + "morePosts": "More Posts", + "premiumContent": { + "title": "Unlock Premium Content", + "description": "Subscribe to our Pro plan to access all premium articles and exclusive content.", + "upgradeCta": "Upgrade Now", + "benefit1": "All premium articles", + "benefit2": "Exclusive content", + "benefit3": "Cancel anytime", + "signIn": "Sign In", + "loginRequired": "Sign in to continue reading", + "loginDescription": "This is a premium article. Sign in to your account to access the full content.", + "checkingAccess": "Checking access...", + "loadingContent": "Loading full content..." + } }, "DocsPage": { "toc": "Table of Contents", diff --git a/source.config.ts b/source.config.ts index 7917344..b2feabf 100644 --- a/source.config.ts +++ b/source.config.ts @@ -94,6 +94,7 @@ export const blog = defineCollections({ image: z.string(), date: z.string().date(), published: z.boolean().default(true), + premium: z.boolean().optional(), categories: z.array(z.string()), author: z.string(), }), diff --git a/src/components/blog/blog-card.tsx b/src/components/blog/blog-card.tsx index 7d587c9..23eff59 100644 --- a/src/components/blog/blog-card.tsx +++ b/src/components/blog/blog-card.tsx @@ -4,6 +4,7 @@ import { formatDate } from '@/lib/formatter'; import { type BlogType, authorSource, categorySource } from '@/lib/source'; import Image from 'next/image'; import BlogImage from './blog-image'; +import { PremiumBadge } from './premium-badge'; interface BlogCardProps { locale: string; @@ -30,6 +31,13 @@ export default function BlogCard({ locale, post }: BlogCardProps) { title={title || 'image for blog post'} /> + {/* Premium badge - top right */} + {post.data.premium && ( +
+ +
+ )} + {/* categories */} {blogCategories && blogCategories.length > 0 && (
diff --git a/src/components/blog/premium-badge.tsx b/src/components/blog/premium-badge.tsx new file mode 100644 index 0000000..51772df --- /dev/null +++ b/src/components/blog/premium-badge.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import { CrownIcon } from 'lucide-react'; + +interface PremiumBadgeProps { + className?: string; + variant?: 'default' | 'outline' | 'secondary'; + size?: 'sm' | 'default' | 'lg'; +} + +export function PremiumBadge({ + className, + variant = 'default', + size = 'default', +}: PremiumBadgeProps) { + const sizeClasses = { + sm: 'text-xs h-5', + default: 'text-xs h-6', + lg: 'text-sm h-7', + }; + + const iconSizes = { + sm: 'size-3', + default: 'size-3', + lg: 'size-4', + }; + + return ( + + + Premium + + ); +} diff --git a/src/components/blog/premium-content.tsx b/src/components/blog/premium-content.tsx new file mode 100644 index 0000000..e3b9ca8 --- /dev/null +++ b/src/components/blog/premium-content.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useCurrentUser } from '@/hooks/use-current-user'; +import { useCurrentPlan } from '@/hooks/use-payment'; +import type { ReactNode } from 'react'; + +interface PremiumContentProps { + children: ReactNode; +} + +/** + * This component will now rely on server-side filtering + * The tags will be removed server-side for non-premium users + * This component only serves as a marker for premium sections + */ +export function PremiumContent({ children }: PremiumContentProps) { + return
{children}
; +} diff --git a/src/components/blog/premium-guard.tsx b/src/components/blog/premium-guard.tsx new file mode 100644 index 0000000..f9f6109 --- /dev/null +++ b/src/components/blog/premium-guard.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { LoginWrapper } from '@/components/auth/login-wrapper'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { useCurrentUser } from '@/hooks/use-current-user'; +import { useCurrentPlan } from '@/hooks/use-payment'; +import { LocaleLink, useLocalePathname } from '@/i18n/navigation'; +import { + ArrowRightIcon, + CheckCircleIcon, + CrownIcon, + Loader2Icon, + LockIcon, +} from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import type { ReactNode } from 'react'; + +interface PremiumGuardProps { + children: ReactNode; + isPremium: boolean; + canAccess?: boolean; + className?: string; +} + +export function PremiumGuard({ + children, + isPremium, + canAccess, + className, +}: PremiumGuardProps) { + // For non-premium articles, show content immediately with no extra processing + if (!isPremium) { + return ( +
+
+ {children} +
+
+ ); + } + + const t = useTranslations('BlogPage'); + const pathname = useLocalePathname(); + + // If server-side check has already determined access, use that + if (canAccess !== undefined) { + // Server has determined the user has access + if (canAccess) { + return ( +
+
+ {children} +
+
+ ); + } + + // Server determined no access, show appropriate message + const currentUser = useCurrentUser(); + if (!currentUser) { + return ( +
+
+ {/* Show partial content before protection */} + {children} +
+ + {/* Enhanced login prompt for server-side blocked content */} +
+
+
+
+ +
+ +
+

+ {t('premiumContent.loginRequired')} +

+

+ {t('premiumContent.loginDescription')} +

+
+ + + + +
+
+
+
+ ); + } + } + + // Fallback to client-side check when server-side check is not available + const currentUser = useCurrentUser(); + const { data: paymentData, isLoading: isLoadingPayment } = useCurrentPlan( + currentUser?.id + ); + + // Determine if user has premium access + const hasPremiumAccess = + paymentData?.currentPlan && + (!paymentData.currentPlan.isFree || paymentData.currentPlan.isLifetime); + + // If user is not logged in + if (!currentUser) { + return ( +
+
+ {children} +
+ + {/* Enhanced login prompt */} +
+
+
+
+ +
+ +
+

+ {t('premiumContent.loginRequired')} +

+

+ {t('premiumContent.loginDescription')} +

+
+ + + + +
+
+
+
+ ); + } + + // If payment data is still loading + if (isLoadingPayment) { + return ( +
+
+ {children} +
+ {isLoadingPayment && ( +
+ + {t('premiumContent.checkingAccess')} +
+ )} +
+ ); + } + + // If user doesn't have premium access + if (!hasPremiumAccess) { + return ( +
+
+ {children} +
+ + {/* Inline subscription banner for logged-in non-members */} +
+ + +
+
+ +
+
+ +

+ {t('premiumContent.title')} +

+ +

+ {t('premiumContent.description')} +

+ +
+ +
+ +
+ + + {t('premiumContent.benefit1')} + + + + {t('premiumContent.benefit2')} + + + + {t('premiumContent.benefit3')} + +
+
+
+
+
+ ); + } + + // Show full content for premium users + return ( +
+
+ {children} +
+
+ ); +} diff --git a/src/components/docs/mdx-components.tsx b/src/components/docs/mdx-components.tsx index c7c2400..adcfe52 100644 --- a/src/components/docs/mdx-components.tsx +++ b/src/components/docs/mdx-components.tsx @@ -1,3 +1,4 @@ +import { PremiumContent } from '@/components/blog/premium-content'; import { ImageWrapper } from '@/components/docs/image-wrapper'; import { Wrapper } from '@/components/docs/wrapper'; import { YoutubeVideo } from '@/components/docs/youtube-video'; @@ -23,6 +24,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { ...LucideIcons, // ...((await import('lucide-react')) as unknown as MDXComponents), YoutubeVideo, + PremiumContent, Tabs, Tab, TypeTable,