Concept Introduction
Pagination dan enhanced content rendering adalah dua konsep fundamental dalam pengembangan modern web applications yang sering diabaikan hingga menjadi bottleneck dalam production. Dalam konteks Next.js blog yang terintegrasi dengan external APIs seperti Notion, kedua konsep ini memiliki implementasi yang spesifik dan tantangan yang unik.
Pagination bukan hanya tentang membagi data menjadi pages untuk user interface, tetapi juga tentang efficient data fetching dan memory management. Enhanced content rendering, di sisi lain, berkaitan dengan transformasi raw data menjadi rich, interactive content yang memberikan value kepada end users.
Technical Definition
Pagination dalam Context API Integration
Pagination adalah systematic approach untuk menangani large datasets dengan membagi mereka menjadi smaller, manageable chunks. Dalam context Notion API, pagination menggunakan cursor-based system:
interface PaginationResponse {
results: NotionBlock[];
next_cursor: string | null;
has_more: boolean;
type: 'block' | 'page' | 'database';
}
interface PaginationRequest {
block_id: string;
start_cursor?: string;
page_size: number; // Maximum 100 untuk Notion API
}
Cursor-based pagination memiliki keuntungan dibanding offset-based pagination karena:
Enhanced Content Rendering Architecture
Enhanced content rendering merujuk pada proses multi-stage transformation dari raw API data menjadi formatted, interactive HTML. Architecture ini terdiri dari beberapa layers:
interface ContentRenderingPipeline {
fetch: (id: string) => Promise;
convert: (raw: RawBlock[]) => TypedBlock[];
process: (typed: TypedBlock[]) => ProcessedContent;
render: (content: ProcessedContent) => string;
optimize: (html: string) => OptimizedContent;
}
Block Type System
Notion API mendukung berbagai block types yang masing-masing memiliki structure dan behavior yang berbeda:
interface NotionBlockTypes {
// Text-based blocks
paragraph: { rich_text: RichText[] };
heading_1: { rich_text: RichText[] };
heading_2: { rich_text: RichText[] };
heading_3: { rich_text: RichText[] };
// List blocks
bulleted_list_item: { rich_text: RichText[] };
numbered_list_item: { rich_text: RichText[] };
// Interactive blocks
to_do: { rich_text: RichText[]; checked: boolean };
// Code blocks
code: { rich_text: RichText[]; language: string };
// Media blocks
image: {
type: 'file' | 'external';
file?: { url: string; expiry_time?: string };
external?: { url: string };
};
// Structural blocks
divider: {};
quote: { rich_text: RichText[] };
callout: { rich_text: RichText[]; icon?: { emoji: string } };
}
Use Cases
1. Large Article Handling
Ketika artikel memiliki lebih dari 100 blocks (limitation Notion API), pagination menjadi critical:
// Scenario: Technical tutorial dengan 250 blocks
async function handleLargeArticle(articleId: string): Promise {
let allBlocks: NotionBlock[] = [];
let hasMore = true;
let nextCursor: string | undefined;
let requestCount = 0;
while (hasMore) {
// Rate limiting protection
if (requestCount > 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}
const response = await notion.blocks.children.list({
block_id: articleId,
start_cursor: nextCursor,
page_size: 100,
});
allBlocks = allBlocks.concat(response.results);
hasMore = response.has_more;
nextCursor = response.next_cursor || undefined;
requestCount++;
}
// Process all blocks untuk complete article
return processCompleteArticle(allBlocks);
}
2. Rich Content Experience
Support untuk various block types memungkinkan rich content yang interactive:
// Example: Processing different block types untuk rich experience
function renderRichContent(blocks: NotionBlock[]): string {
return blocks.map(block => {
switch (block.type) {
case 'to_do':
return renderInteractiveCheckbox(block);
case 'code':
return renderSyntaxHighlightedCode(block);
case 'heading_1':
return renderNavigableHeading(block);
case 'image':
return renderOptimizedImage(block);
case 'quote':
return renderStyledQuote(block);
default:
return renderBasicBlock(block);
}
}).join('\\n\\n');
}
function renderInteractiveCheckbox(block: ToDoBlock): string {
const checked = block.to_do.checked ? 'ā' : 'ā';
const emoji = block.to_do.checked ? 'ā
' : 'ā¬';
const text = extractRichText(block.to_do.rich_text);
return `
${emoji}
${text}
`;
}
3. Performance-Optimized Rendering
Balanced approach untuk performance vs. freshness:
// Context-aware revalidation strategy
function getOptimalRevalidationTime(contentMetadata: ContentMetadata): number {
const { type, lastModified, viewCount, updateFrequency } = contentMetadata;
// High-traffic content needs faster updates
if (viewCount > 1000) {
return 180; // 3 minutes
}
// Frequently updated content
if (updateFrequency === 'high') {
return 300; // 5 minutes
}
// Static content can be cached longer
if (type === 'reference' || type === 'documentation') {
return 3600; // 1 hour
}
// Default balanced approach
return 300; // 5 minutes
}
// Dynamic revalidation based on content characteristics
export const revalidate = getOptimalRevalidationTime(contentMetadata);
Implementation Examples
Example 1: Complete Pagination System
class NotionPaginationManager {
private notion: Client;
private rateLimiter: RateLimiter;
constructor(apiKey: string) {
this.notion = new Client({ auth: apiKey });
this.rateLimiter = new RateLimiter(3, 1000); // 3 requests per second
}
async fetchAllBlocks(blockId: string): Promise {
const allBlocks: NotionBlock[] = [];
let hasMore = true;
let nextCursor: string | undefined;
let page = 1;
try {
while (hasMore) {
// Rate limiting
await this.rateLimiter.wait();
console.log(`Fetching page ${page} for block ${blockId}`);
const response = await this.notion.blocks.children.list({
block_id: blockId,
start_cursor: nextCursor,
page_size: 100,
});
allBlocks.push(...response.results);
hasMore = response.has_more;
nextCursor = response.next_cursor || undefined;
page++;
// Safety check untuk prevent infinite loops
if (page > 50) {
console.warn('Reached maximum pages limit');
break;
}
}
console.log(`Successfully fetched ${allBlocks.length} blocks in ${page - 1} pages`);
return allBlocks;
} catch (error) {
console.error('Error in pagination:', error);
throw new Error(`Failed to fetch blocks: ${error.message}`);
}
}
}
Example 2: Advanced Content Processing Pipeline
class ContentProcessor {
private processors: ProcessingStage[];
constructor() {
this.processors = [
new CodeBlockProcessor(),
new HeadingProcessor(),
new ListProcessor(),
new InteractiveElementProcessor(),
new LinkProcessor(),
new FormattingProcessor()
];
}
async processContent(blocks: NotionBlock[]): Promise {
let content = this.blocksToString(blocks);
// Apply all processors in sequence
for (const processor of this.processors) {
content = await processor.process(content);
}
return {
html: content,
metadata: this.extractMetadata(blocks),
wordCount: this.countWords(content),
estimatedReadTime: this.calculateReadTime(content)
};
}
private blocksToString(blocks: NotionBlock[]): string {
return blocks.map(block => {
switch (block.type) {
case 'paragraph':
return this.processParagraph(block);
case 'heading_1':
return `# ${this.extractRichText(block.heading_1.rich_text)}`;
case 'heading_2':
return `## ${this.extractRichText(block.heading_2.rich_text)}`;
case 'heading_3':
return `### ${this.extractRichText(block.heading_3.rich_text)}`;
case 'bulleted_list_item':
return `⢠${this.extractRichText(block.bulleted_list_item.rich_text)}`;
case 'numbered_list_item':
return `1. ${this.extractRichText(block.numbered_list_item.rich_text)}`;
case 'to_do':
const checked = block.to_do.checked ? 'ā' : 'ā';
return `${checked} ${this.extractRichText(block.to_do.rich_text)}`;
case 'code':
const language = block.code.language || 'text';
const code = this.extractRichText(block.code.rich_text);
return `\\`\\`\\`${language}\\n${code}\\n\\`\\`\\``;
case 'divider':
return '---';
case 'quote':
return `> ${this.extractRichText(block.quote.rich_text)}`;
default:
return '';
}
}).filter(Boolean).join('\\n\\n');
}
}
Example 3: Multi-Stage Text Processing
class TextProcessingPipeline {
private stages: ProcessingStage[];
constructor() {
this.stages = [
new CodeBlockStage(),
new HeadingStage(),
new QuoteStage(),
new ListStage(),
new InlineFormattingStage(),
new InteractiveElementStage(),
new LinkStage(),
new ParagraphStage()
];
}
process(text: string): string {
let processed = text;
for (const stage of this.stages) {
processed = stage.process(processed);
}
return this.cleanupOutput(processed);
}
private cleanupOutput(text: string): string {
return text
.replace(/\\n{3,}/g, '\\n\\n') // Remove excessive newlines
.replace(/^\\s+|\\s+$/g, '') // Trim whitespace
.replace(/(<[^>]+>)\\s+(<[^>]+>)/g, '$1$2'); // Remove whitespace between tags
}
}
// Individual processing stages
class CodeBlockStage implements ProcessingStage {
process(text: string): string {
return text.replace(/```([^\\n]*?)\\n([\\s\\S]*?)\\n```/g, (match, language, code) => {
const lang = language?.trim() || 'text';
const cleanCode = code.trim();
return `
${this.escapeHtml(cleanCode)}
`;
});
}
private escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/ .replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
class InteractiveElementStage implements ProcessingStage {
process(text: string): string {
return text
.replace(/^ā (.*$)/gm, this.renderCheckedTodo)
.replace(/^ā (.*$)/gm, this.renderUncheckedTodo);
}
private renderCheckedTodo(match: string, content: string): string {
return `
${content}
`;
}
private renderUncheckedTodo(match: string, content: string): string {
return `
${content}
`;
}
}
Best Practices
1. Pagination Best Practices
// Rate limiting dan error handling
class RobustPaginationManager {
private static readonly MAX_RETRIES = 3;
private static readonly RETRY_DELAY = 1000;
async fetchWithRetry(
blockId: string,
cursor?: string,
retryCount = 0
): Promise {
try {
return await this.notion.blocks.children.list({
block_id: blockId,
start_cursor: cursor,
page_size: 100,
});
} catch (error) {
if (retryCount < RobustPaginationManager.MAX_RETRIES) {
console.log(`Retry ${retryCount + 1} for block ${blockId}`);
await this.delay(RobustPaginationManager.RETRY_DELAY * (retryCount + 1));
return this.fetchWithRetry(blockId, cursor, retryCount + 1);
}
throw error;
}
}
private delay(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
2. Content Validation
// Validate content untuk prevent XSS dan ensure quality
class ContentValidator {
static validateNotionBlock(block: any): block is NotionBlock {
return (
block &&
typeof block.type === 'string' &&
block.id &&
typeof block.id === 'string' &&
this.isValidBlockType(block.type)
);
}
static isValidBlockType(type: string): boolean {
const validTypes = [
'paragraph', 'heading_1', 'heading_2', 'heading_3',
'bulleted_list_item', 'numbered_list_item', 'to_do',
'code', 'quote', 'divider', 'image', 'callout'
];
return validTypes.includes(type);
}
static sanitizeContent(content: string): string {
// Remove potentially dangerous content
return content
.replace(/