# How I built Notion to Site, and how I'd build it today

> How I built Notion to Site in 2022, why it stayed an internal test, what the code taught me, and how AI agents could help turn it into a business today.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-07-25 | Topics: [Business](https://flaviocopes.com/tags/business/) | Canonical: https://flaviocopes.com/how-i-built-notiontosite/

In December 2022 I built an internal project called Notion to Site.

The pitch fit in one line:

> Write in Notion, publish on your site.

I built it for myself and only used it for internal testing. I never released the product or published the code.

I recently found the project again in my old projects folder.

Looking at old code is always interesting.

You remember the general idea, but the repository remembers what you actually did.

Notion to Site had a working Notion renderer, static page generation, nested pages, columns, tables, images, videos, colors, dark mode, and more.

But it was also much less than a product.

There was no user account, no billing, no OAuth connection, no dashboard, and no automated way to deploy a customer's website.

It was a good engine inside an unfinished car.

## Why I built it

The story started a few days earlier with another project called `solocreator.io`.

That was my first attempt at what later became the Solopreneur Masterclass.

And that idea will soon be reborn as [**SOLO LAB**](https://sololab.io). More on that soon.

I wanted to use Notion as the editor for that site.

Notion is a great place to write. But I did not want the website to look like a shared Notion page. I wanted my own domain, my own design, and static HTML I could host anywhere.

So I built the missing layer.

The first commit to Solo Creator was on December 7, 2022. Over the next two days I added support for more Notion blocks, subpages, readable URLs, nested pages, and page layouts.

On December 13 I reused that engine in a separate private project called Notion to Site.

I also built a test landing page to work on the positioning. But the whole project stayed internal.

The landing page promised a few things:

- highly customizable
- developer friendly
- incredibly flexible
- based on a modern and solid tech stack

I had built the technology, but I had not decided exactly what the business was.

Was this a library?

A hosted service?

A starter kit for developers?

A website builder for people who did not want to touch code?

Those are very different products.

## How it worked

The stack was Astro, React, Tailwind CSS, and the official Notion JavaScript client.

This was Astro 1 beta. I was very early in betting on Astro.

Its static-first model was exactly what I wanted for this project: fetch the content at build time, turn it into HTML, and host the result anywhere.

That bet never changed. I still use Astro today, and it became the A in the [AHA Stack](https://ahastack.dev/) I created: Astro, HTMX, and Alpine.js.

The key architectural choice was to fetch Notion at build time.

Astro's `getStaticPaths()` connected to the Notion API, found the child pages, and generated a static route for each one:

```js
export async function getStaticPaths() {
  const notion = new Client({
    auth: import.meta.env.NOTION_API_KEY,
  })

  const page = await notion.pages.retrieve({
    page_id: import.meta.env.NOTION_PAGE_ID,
  })

  const pages = await getAllSubpagesOfPage(page, notion)

  return pages.map((page) => {
    const slug = slugify(getTitle(page))

    return {
      params: { slug },
      props: { page, notion, slug },
    }
  })
}
```

The Notion API token stayed in the build environment. Visitors received plain HTML.

I still like this approach.

The published site was fast, cheap to host, and did not depend on Notion responding every time someone opened a page.

The tradeoff was that an edit in Notion did nothing until the site was rebuilt.

There were no Notion webhooks in the project. There was no background job or deploy queue either. Publishing was still a manual operation.

## I wrote my own Notion block renderer

The project included `notion-to-md`, and the earliest code imported it, but I ended up rendering the Notion blocks myself.

The renderer was a large React component with one component for every supported block.

The central part was a switch:

```jsx
switch (block.type) {
  case 'paragraph':
    return <Paragraph block={block} />
  case 'heading_1':
    return <Heading1 block={block} />
  case 'table':
    return <Table block={block} />
  case 'toggle':
    return <Toggle block={block} />
  case 'image':
    return <Image block={block} />
  case 'video':
    return <Video block={block} />
  default:
    return <UnknownBlock block={block} />
}
```

It supported paragraphs, three heading levels, callouts, quotes, to-do items, bulleted lists, tables, toggles, dividers, images, videos, child pages, and columns.

Rich text was another renderer inside the renderer.

It handled bold, italic, strikethrough, underline, inline code, links, text colors, and background colors.

I even mapped Notion's colors to separate light and dark mode colors.

One detail I like is the unknown block fallback.

If Notion returned something the renderer did not understand, the build did not silently discard it. The page displayed a red warning with the unknown block type.

That is not nice for a production website, but it is very useful while building a renderer.

## Notion's tree made routing interesting

A Notion page can be inside another page. It can also be inside a column block inside another page.

Those cases have different parent objects in the API.

I wrote a `getPageOfBlock()` function that walked up the block tree until it found the page containing it. Then `getPagePath()` kept walking through parent pages to build a URL.

That gave me paths based on the Notion hierarchy instead of a flat collection of page IDs.

The original Solo Creator implementation supported routes three levels deep.

The Notion to Site copy only kept the top-level dynamic route. The path calculation survived, but the route for the next level did not.

This is the kind of half-finished edge you find in a prototype. The pieces exist, but they are not connected all the way through.

## Slugs created another problem

I did not want URLs full of Notion page IDs, so I generated slugs from page titles.

The slug function lowercased the title, removed accents and unsupported characters, changed spaces to hyphens, and collapsed repeated hyphens.

It produced nice URLs.

It also created two problems I had already written in the old README:

- changing a page title changed its URL
- two pages with the same title produced the same slug

A real product needs stable identity behind readable URLs.

I would store the Notion page ID as the permanent key, remember every previous slug, and automatically create redirects after a rename.

## The limitations were already visible

The old code contains a comment next to images uploaded to Notion:

```js
// TODO store local? link expires in 1 hour. see how to do
```

The Notion API returned an S3 URL for each image, but that URL expired after one hour.

If I used that URL directly in the generated HTML, the image would stop working one hour later. A production publishing service must download those images during the build and store its own permanent copies.

There were other limits:

- block requests were not paginated
- nested children were only fetched for a few block types
- list nesting was not handled properly
- numbered lists and code blocks were missing
- some icons were skipped because the extra API calls made builds slower
- a large site would create a lot of API requests during every build
- Notion layout details such as resized columns and images were not available through the API

The current Notion API still returns block children as a paginated list and tells integrations to retrieve nested children recursively. It also applies rate limits. A serious implementation needs a sync engine, not a collection of direct API calls scattered through page components.

## Why it did not become a business

The code was built for one site, then reused in an internal product test.

Those are not the same thing.

I also realized I would become the *anello debole*, the weak link, between the Notion API and every customer's website.

If Notion changed its API, my code had to adapt. If the sync failed, a customer's site would stop updating. If an image disappeared, the customer would blame my product, not Notion.

And they would be right. They would be paying me to make that dependency reliable.

At the time I was not ready to take on that responsibility.

To turn it into a SaaS, I still needed to build:

- Notion OAuth for multiple customers
- account management
- a database for sites, domains, tokens, and settings
- a reliable sync and build pipeline
- previews and publishing controls
- custom domains and SSL
- themes and customization
- image storage
- billing
- support and documentation

The repository has none of those things.

The test landing page included a waiting list form, but I never opened it as a public product.

I also started with the most open-ended part: render as much of Notion as possible.

That creates an endless surface area. Every block, nesting combination, API change, and visual detail becomes something the product must understand forever.

Meanwhile, the landing page did not explain who the product was for or what painful job it solved better than the alternatives.

"Create a website from Notion" is a feature.

It is not yet a business.

The project stopped there. It never went beyond my internal testing.

## How I would restart it today

Four years later, I can see myself trying this idea again.

The big difference is that now I have my agentic shipping "platform" in place: the tools, skills, workflows, and review process I use to go from an idea to tested and deployed software with agents.

That changes what is realistic for me as a solo founder. It does not remove the responsibility of sitting between Notion and my customers, but it makes building and operating that connection much more manageable.

I would not begin by restoring the old repository and updating its dependencies.

I would still build it first and see how people react.

That is how I like to test an idea. A landing page or a conversation can only tell me so much. I want to put a working product in front of people and see what they do with it.

I would keep the first version focused on one kind of user:

> A technical creator or small agency that wants to write in Notion, publish a fast branded website on its own domain, and never maintain the publishing plumbing.

This customer still values customization, but does not want to build OAuth, sync, image caching, deploy hooks, redirects, and previews.

That is the product.

The Notion renderer is only one part of it.

I would release a narrow version. No theme marketplace. No attempt to support every possible Notion workspace.

I would offer two good themes and a clear set of supported blocks.

If a customer uses an unsupported block, the product should tell them before publishing. It should never silently lose content.

The first version would have this flow:

1. Sign in and connect Notion using a public OAuth integration.
2. Choose a root page.
3. Preview the generated website.
4. Pick a theme and change a small set of design tokens.
5. Connect a domain.
6. Publish.

Today Notion provides [public OAuth connections](https://developers.notion.com/guides/get-started/public-connections) for multi-workspace products and [webhooks](https://developers.notion.com/reference/webhooks) for page changes.

An edit could trigger a queued sync. The sync would retrieve the changed page, normalize the blocks into an internal format, copy new media to permanent storage, and rebuild only what changed.

I would keep Notion page IDs as the internal identity. URLs could change without losing old links.

And I would separate the Notion data from the renderer.

The renderer should receive my own stable page format, not raw API responses. That makes API upgrades, tests, imports from other sources, and new frontends much easier.

## What AI agents change

In 2022 I had to do every part myself.

I could use libraries and search for examples, but the long tail of implementation was still mine: every block renderer, fixture, test, migration, UI state, build failure, support reply, and documentation page.

AI agents change the economics of a small product like this.

They do not make the idea good. They do not find customers for me. They do not remove the need to support what I ship.

I would still sit between Notion and my customers. The difference is that agents could continuously test that connection, watch for failures, investigate what changed, and prepare a fix.

But they let one person operate much closer to a small team.

### Agents could help build the renderer

Notion blocks are a good fit for bounded agent work.

I can give an agent one block type, the official API shape, a set of fixtures, the visual rules, and the tests it must pass.

One agent can implement code blocks. Another can handle nested lists. Another can add bookmarks or synced blocks.

The important part is the fixture library and visual regression suite.

Without tests, I would just produce unsupported code faster.

With tests, agents can extend the renderer without changing the shared contracts or breaking existing sites.

### An agent could configure each new site

The setup work for each new site can become an advantage instead of a cost.

An onboarding agent could inspect the selected Notion page tree and propose:

- the navigation structure
- page titles and descriptions
- redirects for duplicate or unsafe slugs
- a theme based on the customer's existing brand
- missing images or broken links
- unsupported blocks that need attention

The customer would review the proposal before publishing.

This is much better than generating an entire arbitrary website from a prompt. The agent works inside a small, predictable system.

### Agents could turn customization into conversation

The old test landing page promised that Notion to Site would be highly customizable and developer friendly.

Normally that means exposing a growing settings panel or asking people to edit code.

An agent offers another interface.

You could say:

> Make the headings smaller, use the logo's blue for links, and add the About page to the top navigation.

The agent would translate that request into validated theme tokens and navigation settings.

It should not edit production code directly. It should change a constrained configuration, show a preview, and wait for approval.

That keeps the flexibility without turning every customer site into a custom fork.

### Agents could help operate the business

A publishing product fails in repetitive ways.

A Notion token expires. A block response changes. An image cannot be downloaded. A build fails. A domain is misconfigured.

An operations agent can inspect the failure, collect the relevant logs, identify the likely cause, and prepare the next action.

It can also answer support questions from the actual site state instead of sending a generic reply.

I would still keep approval around anything destructive or customer-facing. But the agent could remove most of the investigation work.

## The new moat would not be AI

Anyone can call a model.

The value would be the system around it:

- a reliable Notion sync engine
- a well-tested block renderer
- fast websites
- safe customization
- stable URLs
- permanent media
- previews and rollbacks
- excellent onboarding

AI agents would make that system possible for a solo founder to build and operate.

They would not be the reason someone pays.

People would pay because they can write in Notion and get a website they are proud to put on their domain.

## Would I build it again?

Yes.

I would build it, put it online, and see how people react.

The product itself would be the test.

I would keep the scope small enough to ship quickly, then use the reaction to decide what to improve, remove, or build next.

I would use agents to implement new blocks, test edge cases, configure sites, check builds, and help customers.

The old code was not wasted.

It proved that the basic idea worked. It also captured the exact point where a technical project must become a product.

Turning it into a business would still be the hard part.

AI does not guarantee that people will care. It makes the experiment faster to build, cheaper to run, and easier to improve if they do.
