This guide is for migrating an external theme (for example Astro/Vite themes) into NotionNext's Next.js + Notion data architecture.
- Keep the original theme's visual language (layout, spacing, cards, motion).
- Follow NotionNext's data flow and feature conventions.
- Expose behavior through
themes/<theme>/config.jsswitches instead of hardcoding.
Create a dedicated theme folder:
themes/<theme>/index.jsthemes/<theme>/style.jsthemes/<theme>/config.jsthemes/<theme>/components/*
Rules:
- Do not import UI components from other theme folders.
- Keep cross-theme shared components only from global
@/components/*when needed (for exampleNotionPage,Comment,ShareBar,FlipCard, ads widgets). - Keep theme-specific rendering and style under the theme folder.
Common props available in theme layouts/components:
siteInfo: site metadata, cover, title, descriptionposts,post,archivePostslatestPosts,categoryOptions,tagOptionsnoticepostCountprev,nextcustomNav,customMenurightAreaSlot
Typical post fields used by themes:
title,slug,href,summarypublishDay,lastEditedDaypageCover,pageCoverThumbnailcategory,tagItemstoc
When migrating a new theme, verify all of these:
-
Data-driven menu
- Support default menu items.
- Support
customNav. - Support
CUSTOM_MENUoverriding withcustomMenu.
-
Notice/announcement block
- Render Notion content using
NotionPage. - Switchable in theme config.
- Render Notion content using
-
Notion cover as Hero
- Use
siteInfo.pageCoveras first priority for home hero background. - Keep fallback image config.
- Use
-
Dark mode support
- Use global context (
useGlobal) andtoggleDarkMode. - Avoid isolated theme-only dark state.
- Use global context (
-
Article module compatibility
- TOC panel switch
- Share module switch
- Comment module switch
- Copyright block switch
- Adjacent posts switch
-
Sidebar modularity
- Latest posts, categories, tags
- Contact card (optional flip card)
- Analytics card
- Ads card
- Plugin slot (
rightAreaSlot)
-
Float tools
- Back to top
- Jump to comment
- Dark mode quick switch
Use siteConfig('<KEY>', <default>, CONFIG) consistently.
Recommended key groups:
THEME_MENU_*THEME_HERO_*THEME_POST_LIST_*THEME_WIDGET_*THEME_ARTICLE_*
Do not scatter constants in component bodies.
- Build minimum runnable skeleton (
LayoutBase,LayoutIndex,LayoutSlug, etc.). - Split large
index.jsinto focused components. - Port original style details (cards, banner, metadata density, transitions).
- Integrate NotionNext feature modules and config switches.
- Add docs for all theme config keys and default values.
- Run lint and verify:
- Home/list/search/archive/category/tag/article/404
- Light/dark mode
- Menu behaviors (
customNav,CUSTOM_MENU) - Notice, ads, plugin slot, contact card
The NEXT_PUBLIC_CONTACT_EMAIL environment variable is compiled into conf/contact.config.js and stored in siteConfig('CONTACT_EMAIL') as a Base64-encoded payload (UTF-8), so plain addresses do not appear verbatim in static HTML. When migrating or adding theme UI, pick the right helper or you will see garbled mailto: targets or wrong Gravatar hashes.
| Scenario | Do | Don't |
|---|---|---|
| Icon/link opens the system mail client | Use handleEmailClick (below) |
href={\mailto:${siteConfig('CONTACT_EMAIL')}`}` |
| Footer or copy shows the address | resolveContactEmail(siteConfig('CONTACT_EMAIL')) |
Render siteConfig('CONTACT_EMAIL') directly |
| md5 for Gravatar | Hash lowercased plaintext from resolveContactEmail |
md5 the encrypted string |
Server-generated text (e.g. security.txt) |
resolveContactEmail before mailto: |
Write the encrypted blob into the file |
Click-to-mail pattern (match themes/next/components/SocialButton.js):
import { useRef } from 'react'
import { handleEmailClick } from '@/lib/plugins/mailEncrypt'
import { siteConfig } from '@/lib/config'
const emailIcon = useRef(null)
const CONTACT_EMAIL = siteConfig('CONTACT_EMAIL')
// ...
{CONTACT_EMAIL && (
<a
onClick={e => handleEmailClick(e, emailIcon, CONTACT_EMAIL)}
title='email'
className='cursor-pointer'
ref={emailIcon}>
{/* icon */}
</a>
)}Helpers live in lib/plugins/mailEncrypt.js: handleEmailClick, decryptEmail, resolveContactEmail.
You can keep a custom theme under themes/<theme-id>/ locally or in your fork without opening a PR.
If you want the theme merged into the official NotionNext repository, you must also ship assets and manifest entries used by the theme switcher when THEME_SWITCH / NEXT_PUBLIC_THEME_SWITCH is enabled: cover previews plus human-readable name and summary.
Commit static previews under:
public/images/themes-preview/
| File | Purpose |
|---|---|
<theme-id>.png |
Baseline image; must match the folder name under themes/ (lowercase), e.g. endspace.png. Used as LazyImage fallbackSrc. |
<theme-id>.webp |
Strongly recommended; convert from PNG (cwebp, Squoosh, ImageMagick, etc.). Used as the preferred src for smaller payloads. |
By default, getThemeSwitchMeta() resolves /images/themes-preview/<id>.webp and .png. Submit both in PRs when possible.
Edit conf/themeSwitch.manifest.js and add an entry under THEME_SWITCH_MANIFEST keyed by the theme id (same as the directory under themes/):
name(optional): label in the switcher; if omitted, the id is auto-formatted.summary(recommended): one-line blurb under the card title.cover/coverWebp(optional): custom image URLs. If omitted, the paths in §8.1 apply. If you only ship PNG for now, setcoverWebpto''for that theme so the UI uses PNG only.tier(optional):'free'or'paid'; defaults to'free', which shows a Free-style badge in the switcher. For future paid themes, settierto'paid'to show the paid label.
components/ThemeSwitch.js reads metadata via getThemeSwitchMeta(); you do not duplicate this inside the theme folder.
- Long-form theme notes still belong under
docs/developer/themes/(see the visual fidelity checklist for doc placement). - In the PR description, list preview files and any new or updated manifest entries.
This subsection is for theme authors: how we collaborate in the open repo today, and possible future options for paid themes. Final policies and timelines will be announced officially.
- Acknowledging the work: shipping and maintaining a theme (layout, UX, breakpoints, and ongoing compatibility) is real effort. Contributing redistributable themes to the public NotionNext repo remains highly valued.
- Current path: we still encourage free-to-use themes via PRs to the main GitHub repository, following §8.1–§8.3 for previews and manifest. Local or private-fork use is unrestricted.
- Future direction (not live yet): as the ecosystem matures, we plan to allow paid theme submissions so authors can be compensated while users get maintained, high-quality work.
- Planned shape (illustrative only): a separate private Git repository may be introduced for authors to publish and version themes; themes could be priced, and after purchase, buyers receive what they need to privately deploy the theme on their own sites (exact license, updates, and support will be defined at launch).
- Relation to code: the manifest
tierfield (free/paid) is for UI labeling in the theme switcher. Billing, entitlements, delivery, and private deployment will not rely on manifest alone; dedicated docs and tooling will ship when the program goes live.
For themes/fuwari, these specifics are already applied:
- Upstream style reference source: saicaca/fuwari
- Notion cover hero support
- Data-driven menu with
customNav/customMenucompatibility - Independent TOC, sidebar widgets, and right-float actions
- Flip contact card support via global
FlipCard
- Hardcoded menu paths without
customMenusupport. - Reusing another theme's UI components directly.
- Local-only dark mode toggle that ignores global context.
- Missing
post?.tocandnotice?.blockMapguards. - Forgetting to expose new behaviors in theme config.
- Contact email: raw
mailto:with encrypted config or missinghandleEmailClick/resolveContactEmail(see section 7).
- Layout orientation: desktop default should be left functional sidebar + right content feed.
- Hero full width: avoid
calc(50% - 50vw)scrollbar offset drift; use stable center transform strategy. - Post card variants:
- with cover: text left + cover right
- without cover: keep a right-side action rail to maintain visual rhythm
- Readmore affordance: right action rail and icon should keep consistent card height alignment.
- Profile card actions: include social icon row under avatar/bio if source theme has it.
- Theme color picker UX:
- trigger from top-right palette button
- use floating panel, not sidebar block
- real-time preview + persisted local setting
- expose copied hue/hex for operators to write back into
config.js
- Theme docs placement:
- avoid putting markdown docs under
themes/<theme>/if build pipeline treats theme dirs as runtime modules - place theme docs under
docs/developer/themes/instead
- avoid putting markdown docs under
- Route transition feel: add lightweight page/card transition to mimic source theme interaction rhythm.