Read your notion pages as Markdown in Laravel applications
Example:
use Redberry\MdNotion\Facades\MdNotion;
$pageId = '263d9316605a806f9e95e1377a46ff3e';
// Get page content as markdown
$markdown = MdNotion::make($pageId)->content()->read();
// Get complete recursive content
$fullContent = MdNotion::make($pageId)->full();Don't forget to star the repo ⭐
- Installation
- Configuration
- Features
- Usage
- Page Size & Pagination
- Error Handling
- Page and Database Objects API
- Customization
- Testing
- Security Vulnerabilities
- Credits
- License
You can install the package via composer:
composer require redberry/md-notionYou can publish the config file with:
php artisan vendor:publish --tag="md-notion-config"This is the contents of the published config file:
return [
/**
* The Notion API key used for authentication with the Notion API.
*/
'notion_api_key' => env('NOTION_API_KEY', ''),
/**
* Defines the maximum block number that can be fetched in a single request.
*/
'default_page_size' => env('NOTION_DEFAULT_PAGE_SIZE', 100),
/**
* Blade templates for markdown rendering
*/
'templates' => [
'page_markdown' => 'md-notion::page-md',
'full_markdown' => 'md-notion::full-md',
],
/**
* Block type to adapter class mappings.
* Customize these to use your own adapters.
*/
'adapters' => [
'paragraph' => \Redberry\MdNotion\Adapters\ParagraphAdapter::class,
'heading_1' => \Redberry\MdNotion\Adapters\HeadingAdapter::class,
// ... many more block adapters
],
];Optionally, you can publish the views:
php artisan vendor:publish --tag="md-notion-views"Set your Notion API key in your .env file:
NOTION_API_KEY=your_notion_api_key_hereTo get your Notion API key:
- Go to Notion Developers
- Create a new integration
- Copy the API key
- Add the integration to your Notion pages you want to read
🔄 Fluent API - Chain methods for intuitive content fetching
📄 Page Reading - Extract Notion pages as clean markdown
🗃️ Database Support - Convert Notion databases to markdown tables
🌲 Recursive Fetching - Get all nested pages and databases in one call
🎨 Customizable Templates - Use Blade templates for markdown output
🧩 Custom Adapters - Extend block adapters for specialized content
⚡ Laravel Integration - Seamless service provider and facade support
🛠️ Configurable - Easy configuration via Laravel config files
📊 Pagination Support - Automatic pagination for large pages (100+ blocks)
🚨 Error Handling - Typed exceptions for Notion API errors
Get the content of a single page as markdown:
use Redberry\MdNotion\Facades\MdNotion;
$pageId = '263d9316605a806f9e95e1377a46ff3e';
$content = MdNotion::make($pageId)->content()->read();
// Returns: "# Page Title\n\nPage content as markdown..."Get collection of child pages:
$pages = MdNotion::make($pageId)->pages();
// Returns: Collection of Page objectsnote: Each page has: id, title, content, created_time, last_edited_time, etc. Check API reference
Get collection of child databases:
$databases = MdNotion::make($pageId)->databases();
// Returns: Collection of Database objectsEach database has: id, title, description, properties, and table content, etc. Check API reference
Get page content including child pages and databases:
// Include child pages in Markdown content
$content = MdNotion::make($pageId)
->content()
->withPages()
->read();
// Include child databases as tables in Markdown content
$content = MdNotion::make($pageId)
->content()
->withDatabases()
->read();
// Include both child pages and databases in Markdown content
$content = MdNotion::make($pageId)
->content()
->withPages()
->withDatabases()
->read();
// Returns: Formatted markdown with main content + child content sectionsGet everything recursively (current page + all nested pages and databases):
$fullContent = MdNotion::make($pageId)->full();
// Returns: Complete markdown with all nested contentfull is the only method which by default returns database item's content as well. It performs minimum one request per page, so pages with nested content or big databases can hit the memory limits, can be slow or hit limits of notion API.
Set page ID dynamically:
$mdNotion = MdNotion::make(); // Empty initially
$content = $mdNotion->setPage($pageId)->full();Access the raw Page object with all data:
$page = MdNotion::make($pageId)
->content()
->withPages()
->withDatabases()
->get();
// Returns: Page object with loaded child content
// Access: $page->getTitle(), $page->getContent(), $page->getChildPages(), etc.The Notion API limits responses to 100 blocks per request. This package handles pagination automatically, allowing you to fetch more blocks seamlessly.
Set the default page size in your .env file:
NOTION_DEFAULT_PAGE_SIZE=100Or in the config file:
// config/md-notion.php
return [
'default_page_size' => env('NOTION_DEFAULT_PAGE_SIZE', 100),
// ...
];You can override the default page size per request:
use Redberry\MdNotion\Facades\MdNotion;
// Fetch up to 50 blocks
$content = MdNotion::make($pageId)->content()->read(50);
// Fetch up to 200 blocks (automatically paginated)
$content = MdNotion::make($pageId)->content()->read(200);
// Use default from config
$content = MdNotion::make($pageId)->content()->read();- Page size ≤ 100: Single API request
- Page size > 100: Automatic pagination with multiple requests
The returned data always has a consistent structure:
[
'results' => [...], // Array of blocks
'has_more' => bool, // Whether more items exist
'next_cursor' => ?string // Cursor for manual continuation (null if results were trimmed)
]Note: When results are trimmed to meet your requested limit,
next_cursoris set tonullto prevent accidentally skipping items. Thehas_moreflag will still indicate if more items exist.
Page size must be a positive integer. Invalid values will throw an exception:
// These will throw InvalidArgumentException:
MdNotion::make($pageId)->content()->read(0); // Zero not allowed
MdNotion::make($pageId)->content()->read(-5); // Negative not allowedThe package provides a dedicated NotionApiException for handling Notion API errors with detailed information.
use Redberry\MdNotion\Facades\MdNotion;
use Redberry\MdNotion\SDK\Exceptions\NotionApiException;
try {
$content = MdNotion::make($pageId)->content()->read();
} catch (NotionApiException $e) {
// Get error details
echo $e->getMessage(); // "Notion API Error [404] object_not_found: Could not find page..."
echo $e->getNotionCode(); // "object_not_found"
echo $e->getNotionMessage(); // "Could not find page with ID: ..."
// Access the original response
$response = $e->getResponse();
$statusCode = $response->status(); // 404
}The exception provides convenient methods to check error types:
try {
$content = MdNotion::make($pageId)->content()->read();
} catch (NotionApiException $e) {
if ($e->isNotFound()) {
// Page doesn't exist or not shared with integration
}
if ($e->isUnauthorized()) {
// Invalid API key
}
if ($e->isForbidden()) {
// Integration doesn't have access to this resource
}
if ($e->isRateLimited()) {
// Too many requests, implement backoff
}
if ($e->isValidationError()) {
// Invalid request parameters
}
if ($e->isServerError()) {
// Notion server error (5xx)
}
if ($e->isRetryable()) {
// Safe to retry (rate limits, server errors, conflicts)
}
}The getNotionCode() method returns one of these values:
| Code | HTTP Status | Description |
|---|---|---|
invalid_json |
400 | Request body is not valid JSON |
invalid_request_url |
400 | Invalid request URL |
invalid_request |
400 | Invalid request parameters |
validation_error |
400 | Request validation failed |
missing_version |
400 | Missing Notion-Version header |
unauthorized |
401 | Invalid API key |
restricted_resource |
403 | No access to resource |
object_not_found |
404 | Resource not found |
conflict_error |
409 | Transaction conflict |
rate_limited |
429 | Too many requests |
internal_server_error |
500 | Notion server error |
bad_gateway |
502 | Bad gateway |
service_unavailable |
503 | Service temporarily unavailable |
gateway_timeout |
504 | Gateway timeout |
use Redberry\MdNotion\SDK\Exceptions\NotionApiException;
function fetchWithRetry(string $pageId, int $maxRetries = 3): string
{
$attempts = 0;
while ($attempts < $maxRetries) {
try {
return MdNotion::make($pageId)->content()->read();
} catch (NotionApiException $e) {
if (!$e->isRetryable()) {
throw $e; // Don't retry non-retryable errors
}
$attempts++;
if ($attempts >= $maxRetries) {
throw $e;
}
// Exponential backoff
$delay = $e->isRateLimited() ? 1000 : 500;
usleep($delay * $attempts * 1000);
}
}
}The MdNotion package provides rich object models for working with Notion pages and databases. Both Page and Database objects extend BaseObject and use several traits to provide comprehensive functionality.
use Redberry\MdNotion\Objects\Page;
// Create from data
$page = Page::from([
'id' => 'page-id-123',
'title' => 'My Page Title',
'content' => '# Page content...',
'has_children' => true
]);
// Core properties
$page->getId(); // string - Page ID
$page->getTitle(); // string - Page title
$page->getContent(); // ?string - Page content
$page->hasContent(); // bool - Whether page has content
$page->hasChildren(); // bool - Whether page has child pages// Content operations
$page->setContent('# New content');
$page->getContent(); // Returns MD string: '# New content'
$page->hasContent(); // Returns: true
// Child database
$databases = $page->getChildDatabases(); // Collection<Database>When page is accessed as child page of another, it may not contain all information you need, including child pages, markdown content and etc. To get the needed data, you can use ID with MdNotion again or use fetch method on page instance.
// Fetch latest data from Notion API
$page->fetch(); // Updates current instance with fresh data
// The fetch method preserves object identity
$originalPage = Page::from(['id' => 'page-123']);
$updatedPage = $originalPage->fetch();
// $originalPage === $updatedPage (same page, updated data)use Redberry\MdNotion\Objects\Database;
// Create from data
$database = Database::from([
'id' => 'db-id-123',
'title' => 'My Database',
'tableContent' => '| Name | Status |\n|------|--------|\n| Task 1 | Done |'
]);
// Core properties
$database->getId(); // string - Database ID
$database->getTitle(); // string - Database title
$database->getTableContent(); // ?string - Database as markdown table
$database->hasTableContent(); // bool - Whether has table content// Table content operations
$database->getTableContent(); // Returns markdown table string
$database->hasTableContent(); // Returns true if table content exists
// Read items content (populate child pages with content)
$database->readItemsContent();// Fetch latest data from Notion API
$database->fetch(); // Updates current instance with fresh dataBoth Page and Database objects share these APIs through inheritance and traits:
// Title operations
$object->getTitle(); // string - Get title
$object->setTitle('New Title'); // Set title
$object->renderTitle(1); // string - Render as markdown heading (# Title)
$object->renderTitle(2); // string - Render as level 2 heading (## Title)
$object->renderTitle(3); // string - Render as level 3 heading (### Title)// Icon operations
$object->getIcon(); // ?array - Get icon data
$object->setIcon($iconData); // Set icon data
$object->hasIcon(); // bool - Whether has icon
$object->processIcon(); // string - Get icon as emoji/markdown
// Icon types supported:
// - Emoji: Returns emoji character
// - External: Returns [IconName](url) markdown link
// - File: Returns [🔗](url) markdown link// Timestamps
$object->getCreatedTime(); // string - ISO timestamp
$object->getLastEditedTime(); // string - ISO timestamp
// User information
$object->getCreatedBy(); // array - User data who created
$object->getLastEditedBy(); // array - User data who last edited
// Status flags
$object->isArchived(); // bool - Whether archived
$object->isTrashed(); // bool - Alias for isInTrash()// Parent operations
$object->getParent(); // array - Full parent data
$object->hasParent(); // bool - Whether has parent
$object->getParentType(); // ?string - Parent type (page_id, workspace, etc.)
$object->getParentId(); // ?string - Parent ID based on type// Child pages operations
$object->getChildPages(); // Collection<Page> - Child pages collection
$object->hasChildPages(); // bool - Whether has child pages// URL management
$object->getUrl(); // ?string - Notion URL
$object->hasUrl(); // bool - Whether has URL
$object->getPublicUrl(); // ?string - Public sharing URL
$object->hasPublicUrl(); // bool - Whether has public URL
// Properties management
$object->getProperties(); // array - All Notion properties
$object->hasProperties(); // bool - Whether has properties
$object->getProperty('title'); // mixed - Get specific property// Convert to array
$data = $object->toArray(); // Complete object data as array
// Fill from array (merge new data)
$object->fill($newData); // Updates object with new data, preserves existing
// Static creation
$object = Page::from($data); // Create new instance from data array
$object = Database::from($data); // Create new instance from data array// Objects maintain identity during fetch operations
$page = Page::from(['id' => 'page-123']);
$samePageReference = $page->fetch();
// $page === $samePageReference (same object instance)
// But $page now has updated content from Notion API// Page with child databases
$childDbs = $page->getChildDatabases();
// Database with child pages
$childPages = $database->getChildPages();
// Read all child page content
$database->readItemsContent();$page = Page::from([
'id' => 'page-123',
'title' => 'Original Title',
'content' => 'Original content'
]);
// Partial update - only updates specified fields
$page->fill([
'title' => 'Updated Title'
// content remains "Original content"
]);
echo $page->getTitle(); // "Updated Title"
echo $page->getContent(); // "Original content" (preserved)You can customize how markdown is rendered by creating your own Blade templates:
To change the layout or logic how read or full methods render the markdown, you should create a new view and replace them in config:
// config/md-notion.php
return [
'templates' => [
'page_markdown' => 'custom.page-template', // For content()->read()
'full_markdown' => 'custom.full-template', // For full()
],
];When customizing templates, you have access to these variables:
$current_page- Array withtitle,content,hasContent$child_databases- Array of database data withtitle,table_content,hasTableContent$child_pages- Array of page data withtitle,content,hasContent$withDatabases- Boolean flag$withPages- Boolean flag$hasChildDatabases- Boolean flag$hasChildPages- Boolean flag
- Complete recursive data structure with nested
current_page,child_databases,child_pages - Each level includes
hasChildDatabases,hasChildPages,levelfor depth tracking
You can check current blade templates here:
read()Method: resources\views\page-md.blade.phpfull()Method: resources\views\full-md.blade.php
Create custom adapters to handle specific Notion block types:
You will need adapter class extending src\Adapters\BaseBlockAdapter.php and custom blade template rendering the data.
// app/Adapters/CustomCodeAdapter.php
<?php
namespace App\Adapters;
use Redberry\MdNotion\Adapters\BaseBlockAdapter;
class CustomCodeAdapter extends BaseBlockAdapter
{
public function getType(): string
{
return 'code'; // Set the type
}
public function getTemplate(): string
{
return 'notion.blocks.code'; // Set blade view
}
// Main method which prepares data to pass to view
protected function prepareData(array $block): array
{
$code = $block['code'];
$content = $this->processRichText($code['richText']);
$content = str_replace('\\n', "\n", $content);
// You will have access to this variables in your blade template:
return [
'content' => $content,
'language' => $code['language'],
'caption' => $this->processRichText($dto->caption),
'block' => $code,
];
}
}Check example: src\Adapters\ParagraphAdapter.php
// config/md-notion.php
return [
'adapters' => [
'callout' => \App\Adapters\CustomCalloutAdapter::class,
// Keep existing adapters...
'paragraph' => \Redberry\MdNotion\Adapters\ParagraphAdapter::class,
'heading_1' => \Redberry\MdNotion\Adapters\HeadingAdapter::class,
// ... other adapters
],
];composer testPlease review our security policy on how to report security vulnerabilities.
Thanks to following people and projects:
The MIT License (MIT). Please see License File for more information.