A Next.js portfolio website that dynamically pulls content from a Notion database and automatically uploads images to Cloudinary for optimized delivery.
- Content managed through Notion database
- Automatic image upload to Cloudinary
- Automatic revalidation when Notion content changes
Before you begin, ensure you have:
- Node.js 18+ installed
- A Notion account with API access
- A Cloudinary account
- npm or yarn package manager
git clone <your-repo-url>
cd portfolio
npm installCreate a .env.local file in the root directory with the following variables:
# Notion Configuration
NOTION_TOKEN=your_notion_integration_token
NOTION_DB_ID=your_notion_database_id
# Cloudinary Configuration
CLOUDINARY_URL=cloudinary://api_key:api_secret@cloud_name
CLOUDINARY_UPLOAD_FOLDER=portfolio
# Optional: Revalidation Secret (for API route)
REVALIDATION_SECRET=your_secret_key_here
# Optional: Upload external images not on Cloudinary (set to "1" to enable)
UPLOAD_EXTERNALS_NOT_ON_CLOUDINARY=0- Go to https://www.notion.so/my-integrations
- Click "New integration"
- Give it a name (e.g., "Portfolio Integration")
- Select your workspace
- Copy the "Internal Integration Token"
- Paste it as
NOTION_TOKENin your.env.local
- Open your Notion database
- Click "Share" β "Copy link"
- The database ID is the long string between the workspace name and the
?in the URL- Example:
https://www.notion.so/workspace/DATABASE_ID?v=... - Copy
DATABASE_IDand use it asNOTION_DB_ID
- Example:
- Sign up at https://cloudinary.com
- Go to your Dashboard
- Copy the "Cloudinary URL" from the dashboard
- It should look like:
cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyz@your-cloud-name - Use this as
CLOUDINARY_URL
Important: Make sure to share your Notion database with your integration:
- Open your Notion database
- Click the "..." menu in the top right
- Click "Add connections"
- Select your integration
Create a Notion database with the following properties:
| Property Name | Property Type | Description |
|---|---|---|
| Name | Title | The title/name of the portfolio item |
| Group | Multi-select | Category or group for organizing items |
| Description | Text | Rich text description of the item |
| Timeline | Date | Start and/or end date for the item |
| Link | URL | External link to the item |
| Files | Files & media | Images or files associated with the item |
| Publish | Checkbox | Whether to display this item on the portfolio |
| Place | Location | Location associated with the item |
| Status | Status | Status of the item |
| Type | Select | Type of portfolio item (see Type Options below) |
The Type select field should include these options (case-insensitive):
about- About section contentheadline- Headline textpostscript- Postscript contentsummary- Summary textportrait- Portrait imagethumbnail- Thumbnail imageproject- Project entrysocial- Social media linkwriting- Writing/article entrypress- Press/media mentionposition- Work position/roleaward- Award entrycertification- Certification entryeducation- Education entryskill- Skill entry
Note: Only items with Publish checked will appear on your portfolio.
- Sign up for a free account at https://cloudinary.com
- Get your Cloudinary URL from the dashboard
- Set up your upload folder (optional, defaults to "portfolio")
The application will automatically:
- Upload images from Notion to Cloudinary
- Update Notion with Cloudinary URLs
- Optimize images for web delivery
npm run devOpen http://localhost:3000 to see your portfolio.
Before building for production, upload all images from Notion to Cloudinary:
npm run upload-imagesOr manually:
npx tsx lib/uploader.tsThis script will:
- Fetch all pages from your Notion database
- Download images from Notion
- Upload them to Cloudinary
- Update Notion with the new Cloudinary URLs
npm run dev- Start development server (also runs image uploader)npm run build- Build for production (also runs image uploader)npm run start- Start production servernpm run lint- Run ESLintnpm run upload-images- Upload images from Notion to Cloudinary
- Push your code to GitHub
- Import your repository in Vercel
- Add all environment variables in Vercel dashboard
- Deploy!
Important: For production, use NEXT_PUBLIC_ prefix for environment variables that need to be accessible in the browser:
NEXT_PUBLIC_NOTION_TOKENNEXT_PUBLIC_NOTION_DATABASE_IDNEXT_PUBLIC_CLOUDINARY_URLNEXT_PUBLIC_CLOUDINARY_UPLOAD_FOLDER
Set up a Notion webhook to automatically revalidate your site when content changes:
- In Vercel, go to your project settings
- Add a webhook URL:
https://your-domain.com/api/revalidate?secret=your_revalidation_secret - Configure the webhook in Notion to call this URL on database updates
Or manually revalidate by visiting:
https://your-domain.com/api/revalidate?secret=your_revalidation_secret
portfolio/
βββ app/ # Next.js app directory
β βββ api/ # API routes
β βββ page.tsx # Main portfolio page
β βββ layout.tsx # Root layout
βββ components/ # React components
β βββ ui/ # UI components
βββ lib/ # Utility libraries
β βββ notion.ts # Notion API client
β βββ cloudinary.ts # Cloudinary client
β βββ portfolio.ts # Portfolio data fetcher
β βββ uploader.ts # Image upload script
βββ types.d.ts # TypeScript type definitions
- Content Management: All content is stored in a Notion database
- Data Fetching: The app fetches data from Notion using the Notion API
- Image Optimization: Images are automatically uploaded to Cloudinary for optimized delivery
- Rendering: Next.js renders the portfolio page server-side
- Revalidation: The site can be revalidated when Notion content changes
The project uses Tailwind CSS. Modify app/globals.css and component files to customize the design.
Edit lib/constants.ts to change default values like:
- Default thumbnail URL
- Default logo URL
- Default portrait URL
- Site URL
- Default title and summary
- Check that
CLOUDINARY_URLis correctly formatted - Ensure your Notion integration has access to the database
- Check that images in Notion are accessible
- Verify
Publishcheckbox is checked in Notion - Check that
Typefield matches one of the supported types - Ensure
NOTION_TOKENandNOTION_DB_IDare correct
- Make sure all environment variables are set
- Run
npm run upload-imagesbefore building - Check that your Notion database has the correct properties
This project is open source and available under the MIT License.
For issues or questions, please open an issue on GitHub.