Implementing Token-Gated Pages and Sections of Website
Token-gated pages restrict access to entire sections of a website for users without required tokens. Implementation includes both server-side route protection and client-side UI — blur effect on content, overlay prompting wallet connection.
Implementation Strategies
Hard gate — content doesn't load at all without token. Server returns 403 or redirect.
Soft gate (blur gate) — content is visible but blurred, overlay prompts you to get access. Used as a marketing tool.
Progressive disclosure — part of content is open, rest is behind token.
Next.js: Server-Side Route Protection
// app/members/page.tsx (Next.js App Router)
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { verifyTokenGate } from '@/lib/token-gate';
export default async function MembersPage() {
const cookieStore = cookies();
const token = cookieStore.get('auth_token')?.value;
if (!token) {
redirect('/connect-wallet?redirect=/members');
}
const { walletAddress } = verifyJwt(token);
const hasAccess = await verifyTokenGate(walletAddress, {
contractAddress: process.env.NFT_CONTRACT,
type: 'ERC721',
minBalance: 1
});
if (!hasAccess) {
redirect('/token-required?contract=' + process.env.NFT_CONTRACT);
}
return <MembersContent />;
}
React: Token Gate Component
// components/TokenGate.tsx
import { useAccount, useReadContract } from 'wagmi';
import { erc721Abi } from 'viem';
interface TokenGateProps {
contractAddress: `0x${string}`;
tokenType: 'ERC721' | 'ERC20';
minBalance?: bigint;
lockedContent: React.ReactNode; // displayed if no token
children: React.ReactNode;
}
export function TokenGate({
contractAddress, tokenType, minBalance = 1n, lockedContent, children
}: TokenGateProps) {
const { address, isConnected } = useAccount();
const { data: balance, isLoading } = useReadContract({
address: contractAddress,
abi: erc721Abi,
functionName: 'balanceOf',
args: [address!],
query: { enabled: isConnected && !!address }
});
if (!isConnected) {
return <WalletConnectPrompt redirectAfter={window.location.pathname} />;
}
if (isLoading) {
return <div className="token-gate-loading">Checking access...</div>;
}
const hasAccess = (balance ?? 0n) >= minBalance;
if (!hasAccess) {
return <>{lockedContent}</>;
}
return <>{children}</>;
}
// Usage
function PremiumSection() {
return (
<TokenGate
contractAddress="0xYourNFTContract"
tokenType="ERC721"
lockedContent={
<div className="token-gate-overlay">
<h3>NFT Holders Only</h3>
<p>Buy NFT to access exclusive content</p>
<a href="https://opensea.io/collection/your-nft">Buy on OpenSea</a>
</div>
}
>
<ExclusiveContent />
</TokenGate>
);
}
Blur-Gate Effect
// Blurred preview with overlay
function BlurGate({ hasAccess, children, contractAddress }) {
return (
<div className="relative">
<div className={hasAccess ? '' : 'blur-sm select-none pointer-events-none'}>
{children}
</div>
{!hasAccess && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30 backdrop-blur-sm">
<div className="bg-white rounded-xl p-8 text-center shadow-xl max-w-sm">
<LockIcon className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<h3 className="text-xl font-bold mb-2">Club Member Content</h3>
<p className="text-gray-600 mb-4">
Get NFT to access this section
</p>
<BuyNFTButton contractAddress={contractAddress} />
</div>
</div>
)}
</div>
);
}
Multi-Token Access
// Access if user owns at least one of several tokens
async function checkMultiTokenAccess(walletAddress: string): Promise<{
hasAccess: boolean;
grantedBy?: string;
}> {
const gates = [
{ contract: PREMIUM_NFT, name: 'Premium NFT', type: 'ERC721' as const },
{ contract: GOVERNANCE_TOKEN, name: 'Governance Token', type: 'ERC20' as const, min: 1000n * 10n**18n }
];
for (const gate of gates) {
const has = gate.type === 'ERC721'
? await checkNFTOwnership(walletAddress, gate.contract)
: await checkERC20Balance(walletAddress, gate.contract, gate.min ?? 1n);
if (has) return { hasAccess: true, grantedBy: gate.name };
}
return { hasAccess: false };
}
Timeline
Token-gated pages with server protection + React component + blur-gate — 4–6 days.







