feat: docs support premium content

This commit is contained in:
javayhu 2025-09-03 01:09:01 +08:00
parent 794c18a7e6
commit 5d5eb82013
8 changed files with 116 additions and 87 deletions

View File

@ -2,6 +2,7 @@
title: What is Fumadocs
description: Introducing Fumadocs, a docs framework that you can break.
icon: CircleHelp
premium: true
---
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**.
@ -18,6 +19,8 @@ You are still using features of Next.js App Router, like **Static Site Generatio
**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.
<PremiumContent>
## Why Fumadocs
Fumadocs is designed with flexibility in mind.
@ -56,3 +59,5 @@ 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!
</PremiumContent>

View File

@ -2,6 +2,7 @@
title: 什么是 Fumadocs
description: 介绍 Fumadocs一个可以打破常规的文档框架
icon: CircleHelp
premium: true
---
Fumadocs 的创建是因为我想要一种更加可定制化的文档构建体验,一个不固执己见的文档框架,**一个你可以"打破"的"框架"**。
@ -18,6 +19,8 @@ Fumadocs 的创建是因为我想要一种更加可定制化的文档构建体
**对 UI 有自己的看法:** Fumadocs UI默认主题提供的唯一东西是**用户界面**。UI 的设计理念是提供更好的移动响应性和用户体验。
相反,我们使用受 Shadcn UI 启发的更灵活的方法 — [Fumadocs CLI](/docs/cli),这样我们可以快速迭代设计,并欢迎更多关于 UI 的反馈。
<PremiumContent>
## 为什么选择 Fumadocs
Fumadocs 的设计考虑了灵活性。
@ -54,3 +57,5 @@ Fumadocs 为 Next.js 提供了额外的工具,包括语法高亮、文档搜
Fumadocs 由 Fuma 和许多贡献者维护,关注代码库的可维护性。
虽然我们不打算提供人们想要的每一项功能,但我们更专注于使基本功能完美且维护良好。
您也可以通过贡献来帮助 Fumadocs 变得更加有用!
</PremiumContent>

View File

@ -282,20 +282,7 @@
"all": "All",
"noPostsFound": "No posts found",
"allPosts": "All 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..."
}
"morePosts": "More Posts"
},
"DocsPage": {
"toc": "Table of Contents",
@ -306,8 +293,20 @@
"nextPage": "Next",
"chooseLanguage": "Select language",
"title": "MkSaaS Docs",
"homepage": "Homepage",
"blog": "Blog"
"homepage": "Homepage"
},
"PremiumContent": {
"title": "Unlock Premium Content",
"description": "Subscribe to our Pro plan to access all premium content and exclusive content.",
"upgradeCta": "Upgrade Now",
"benefit1": "All premium content",
"benefit2": "Exclusive content",
"benefit3": "Cancel anytime",
"signIn": "Sign In",
"loginRequired": "Sign in to continue reading",
"loginDescription": "This is premium content. Sign in to your account to access the full content.",
"checkingAccess": "Checking access...",
"loadingContent": "Loading full content..."
},
"Marketing": {
"navbar": {

View File

@ -282,20 +282,7 @@
"all": "全部",
"noPostsFound": "没有找到文章",
"allPosts": "全部文章",
"morePosts": "更多文章",
"premiumContent": {
"title": "解锁付费内容",
"description": "订阅我们的付费计划,访问所有付费文章和独家内容。",
"upgradeCta": "立即升级",
"benefit1": "所有文章",
"benefit2": "独家内容",
"benefit3": "随时取消",
"signIn": "登录",
"loginRequired": "登录以继续阅读",
"loginDescription": "这是一篇付费文章,请登录您的账户以访问完整内容。",
"checkingAccess": "检查阅读权限...",
"loadingContent": "加载完整内容..."
}
"morePosts": "更多文章"
},
"DocsPage": {
"toc": "目录",
@ -306,8 +293,20 @@
"nextPage": "下一页",
"chooseLanguage": "选择语言",
"title": "MkSaaS文档",
"homepage": "首页",
"blog": "博客"
"homepage": "首页"
},
"PremiumContent": {
"title": "解锁付费内容",
"description": "订阅我们的付费计划,访问所有付费内容和独家内容。",
"upgradeCta": "立即升级",
"benefit1": "所有内容",
"benefit2": "独家内容",
"benefit3": "随时取消",
"signIn": "登录",
"loginRequired": "登录以继续阅读",
"loginDescription": "这是一篇付费内容,请登录您的账户以访问完整内容。",
"checkingAccess": "检查阅读权限...",
"loadingContent": "加载完整内容..."
},
"Marketing": {
"navbar": {

View File

@ -15,6 +15,7 @@ export const docs = defineDocs({
schema: frontmatterSchema.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
premium: z.boolean().optional(),
}),
},
meta: {

View File

@ -1,3 +1,5 @@
import { PremiumBadge } from '@/components/blog/premium-badge';
import { PremiumGuard } from '@/components/blog/premium-guard';
import * as Preview from '@/components/docs';
import { getMDXComponents } from '@/components/docs/mdx-components';
import {
@ -7,6 +9,8 @@ import {
} from '@/components/ui/hover-card';
import { LOCALES } from '@/i18n/routing';
import { constructMetadata } from '@/lib/metadata';
import { checkPremiumAccess } from '@/lib/premium-access';
import { getSession } from '@/lib/server';
import { source } from '@/lib/source';
import { getUrlWithLocale } from '@/lib/urls/urls';
import Link from 'fumadocs-core/link';
@ -86,6 +90,14 @@ export default async function DocPage({ params }: DocPageProps) {
}
const preview = page.data.preview;
const { premium } = page.data;
// Check premium access for premium docs
const session = await getSession();
const hasPremiumAccess =
premium && session?.user?.id
? await checkPremiumAccess(session.user.id)
: !premium; // Non-premium docs are always accessible
const MDX = page.data.body;
@ -98,15 +110,24 @@ export default async function DocPage({ params }: DocPageProps) {
}}
>
<DocsTitle>{page.data.title}</DocsTitle>
{premium && <PremiumBadge size="sm" className="mt-2" />}
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
{/* Preview Rendered Component */}
{preview ? <PreviewRenderer preview={preview} /> : null}
{/* MDX Content */}
<PremiumGuard
isPremium={!!premium}
canAccess={hasPremiumAccess}
className="max-w-none"
>
<MDX
components={getMDXComponents({
a: ({ href, ...props }: { href?: string; [key: string]: any }) => {
a: ({
href,
...props
}: { href?: string; [key: string]: any }) => {
const found = source.getPageByHref(href ?? '', {
dir: page.file.dirname,
});
@ -136,6 +157,7 @@ export default async function DocPage({ params }: DocPageProps) {
},
})}
/>
</PremiumGuard>
</DocsBody>
</DocsPage>
);

View File

@ -35,8 +35,7 @@ export function PremiumBadge({
variant={variant}
className={cn(
'inline-flex items-center gap-1 font-medium',
'bg-gradient-to-r from-amber-500 to-orange-500',
'text-white border-0 hover:from-amber-600 hover:to-orange-600',
'bg-orange-400 text-white border-0',
sizeClasses[size],
className
)}

View File

@ -30,7 +30,7 @@ export function PremiumGuard({
className,
}: PremiumGuardProps) {
// All hooks must be called unconditionally at the top
const t = useTranslations('BlogPage');
const t = useTranslations('PremiumContent');
const pathname = useLocalePathname();
const currentUser = useCurrentUser();
const { data: paymentData, isLoading: isLoadingPayment } = useCurrentPlan(
@ -76,7 +76,7 @@ export function PremiumGuard({
</div>
{/* Enhanced login prompt for server-side blocked content */}
<div className="mt-16">
<div className="mt-8">
<div className="w-full p-12 rounded-lg bg-gradient-to-br from-primary/5 via-primary/10 to-secondary/5 border border-primary/20">
<div className="flex flex-col items-center justify-center gap-6 text-center">
<div className="p-4 rounded-full bg-primary/10">
@ -85,17 +85,17 @@ export function PremiumGuard({
<div className="space-y-2">
<h3 className="text-xl font-semibold">
{t('premiumContent.loginRequired')}
{t('loginRequired')}
</h3>
<p className="text-muted-foreground max-w-md">
{t('premiumContent.loginDescription')}
{t('loginDescription')}
</p>
</div>
<LoginWrapper mode="modal" asChild callbackUrl={pathname}>
<Button size="lg" className="min-w-[160px] cursor-pointer">
<LockIcon className="mr-2 size-4" />
{t('premiumContent.signIn')}
{t('signIn')}
</Button>
</LoginWrapper>
</div>
@ -115,7 +115,7 @@ export function PremiumGuard({
</div>
{/* Enhanced login prompt */}
<div className="mt-16">
<div className="mt-8">
<div className="w-full p-12 rounded-lg bg-gradient-to-br from-primary/5 via-primary/10 to-secondary/5 border border-primary/20">
<div className="flex flex-col items-center justify-center gap-6 text-center">
<div className="p-4 rounded-full bg-primary/10">
@ -123,18 +123,16 @@ export function PremiumGuard({
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold">
{t('premiumContent.loginRequired')}
</h3>
<h3 className="text-xl font-semibold">{t('loginRequired')}</h3>
<p className="text-muted-foreground max-w-md">
{t('premiumContent.loginDescription')}
{t('loginDescription')}
</p>
</div>
<LoginWrapper mode="modal" asChild callbackUrl={pathname}>
<Button size="lg" className="min-w-[160px] cursor-pointer">
<LockIcon className="mr-2 size-4" />
{t('premiumContent.signIn')}
{t('signIn')}
</Button>
</LoginWrapper>
</div>
@ -154,7 +152,7 @@ export function PremiumGuard({
{isLoadingPayment && (
<div className="mt-8 flex items-center justify-center text-primary font-semibold">
<Loader2Icon className="size-5 animate-spin mr-2" />
<span>{t('premiumContent.checkingAccess')}</span>
<span>{t('checkingAccess')}</span>
</div>
)}
</div>
@ -170,7 +168,7 @@ export function PremiumGuard({
</div>
{/* Inline subscription banner for logged-in non-members */}
<div className="mt-16">
<div className="mt-8">
<Card className="bg-gradient-to-br from-primary/5 via-primary/10 to-secondary/5 border border-primary/20">
<CardContent className="p-12 text-center">
<div className="flex justify-center mb-6">
@ -179,18 +177,19 @@ export function PremiumGuard({
</div>
</div>
<h3 className="text-xl font-semibold mb-2">
{t('premiumContent.title')}
</h3>
<h3 className="text-xl font-semibold mb-2">{t('title')}</h3>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
{t('premiumContent.description')}
{t('description')}
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center items-center">
<Button asChild size="lg" className="min-w-[160px]">
<LocaleLink href="/pricing">
{t('premiumContent.upgradeCta')}
<LocaleLink
href="/pricing"
className="text-white no-underline hover:text-white/90"
>
{t('upgradeCta')}
<ArrowRightIcon className="ml-2 size-4" />
</LocaleLink>
</Button>
@ -199,15 +198,15 @@ export function PremiumGuard({
<div className="mt-8 flex items-center justify-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-2">
<CheckCircleIcon className="size-4 text-primary" />
{t('premiumContent.benefit1')}
{t('benefit1')}
</span>
<span className="flex items-center gap-2">
<CheckCircleIcon className="size-4 text-primary" />
{t('premiumContent.benefit2')}
{t('benefit2')}
</span>
<span className="flex items-center gap-2">
<CheckCircleIcon className="size-4 text-primary" />
{t('premiumContent.benefit3')}
{t('benefit3')}
</span>
</div>
</CardContent>