Skip to content

Creating and modifying pages

Creating a new page

To create a new page, we first create a session and retrieve a root page, which will then hold our new page:

import ultimate_notion as uno

ROOT_PAGE = 'Tests'  # page with connected Ultimate Notion integration
notion = uno.Session.get_or_create()

root_page = notion.search_page(ROOT_PAGE).item()

Using root_page we can simply create a new page with:

my_page = notion.create_page(parent=root_page, title='Autogenerated page')

Our new page is empty and we can check that with:

assert not my_page.children
# or
assert len(my_page.children) == 0

Adding content to a page

Our new page can now be populated with the typical Notion blocks from the blocks module. Let's create a heading 1 block and append it to our page with:

h1 = uno.Heading1('My new page')
my_page.append(h1)

We can also append several blocks at once:

my_page.append([
    uno.Paragraph('Some text...'),
    uno.Code(
        'SELECT * FROM MY_TABLE',
        language=uno.CodeLang.SQL,
        caption='SQL Query'
        ),
    uno.Quote(
        '"You know, for a mathematician, he did not have enough imagination. '
        'But he has become a poet and now he is fine." - D. Hilbert',
        color=uno.Color.BLUE
        ),
    uno.Callout(
        'Ultimate Notion rocks!',
        color=uno.BGColor.PURPLE,
        icon=uno.Emoji('🚀')
        ),
    uno.Heading2('Showing off some more...'),
    uno.BulletedItem('First bulleted item'),
    uno.BulletedItem('Second bulleted item'),
    uno.NumberedItem('First numbered item', color=uno.Color.RED),
    uno.NumberedItem('Second numbered item', color=uno.Color.RED),
    uno.ToDoItem('First checked Todo', checked=True),
    uno.ToDoItem('Second open Todo'),
    uno.ToggleItem('Toggle item'),
    uno.Divider(),
    uno.Heading3('Some more fancy stuff...'),
    uno.TableOfContents(color=uno.Color.PINK),
    uno.Breadcrumb(),
    uno.Embed('https://www.youtube.com/watch?v=dQw4w9WgXcQ', caption='Rick'),
    uno.Bookmark('https://www.youtube.com/watch?v=dQw4w9WgXcQ'),
    uno.Equation(r'-1 = \exp(i \pi)'),
    ]
)

Let's say we have forgotten one block and want to add it at a specific position, i.e. after another block:

new_paragraph = uno.Paragraph('Some more text, which was added afterwards...')
my_page.append(new_paragraph, after=my_page.children[2])

As the block reference new_paragraph is automatically updated when appended to the page, we can also use it now as a reference to append a new block after it, e.g.:

another_paragraph = uno.Paragraph('Another paragraph...')
my_page.append(another_paragraph, after=new_paragraph)

Another possibility to insert a block after a certain other block is to use the insert_after method of a block itself. Let's say we want to add a divider after the block another_paragraph:

another_paragraph.insert_after(uno.Divider())

To delete a block from a page, we can just call .delete() on it:

another_paragraph.delete()

In some cases, you might rather want to replace a block with another block. For this we can use the .replace method of a block.

first_block = my_page.children[0]
first_block.replace(uno.Heading1('Heading of my new page!'))

Also note that you can even pass a sequence of new blocks to .insert_after and .replace.

Tip

Python's walrus operator, i.e. :=, can be extremely useful when working with blocks that you want to reference later on. So instead of assigning new blocks to variables and passing them to page.append(...), you could just write:

page = notion.create_page(parent=root_page, title='Another autogenerated page')
page.append([
    heading1 := uno.Heading1('My heading'),
    paragraph := uno.Paragraph('My paragraph')
])
print(paragraph.block_url)

Note

For type hinting, use AnyBlock for a general block, e.g.

new_blocks: uno.AnyBlock = [uno.Heading1('New section'), uno.Divider(), uno.Paragraph('My paragraph')]

Modifying blocks

Most properties of a block also provide setter, so you can just assign a new value to the property to modify it. Let's assume, we first have to retrieve the page we created above and want to modify the first heading:

my_page = notion.get_page(my_page.id)  # or `notion.search_page(...).item()`

heading = my_page.children[0]
assert heading.color == uno.Color.DEFAULT
heading.color = uno.BGColor.RED
heading.rich_text = 'Red heading of my new page!'

Check the blocks module to see which properties can also be set. Ultimate Notion allows updating all properties that could also be udpated via the update-a-block endpoint of the official Notion API.

Our page now looks like this so far:

Block page

Bug

Sometimes setting certain attributes, for instance the color property with a foreground color, i.e. uno.Color, can result in the default black text although the property is set correctly when checked in the Notion app. This seems to be a bug within the Notion backend itself.

Nested blocks

A really useful feature of Notion blocks is that most of them can be nested. Therefore some blocks, like paragaphs, quotes, toggable headings and so on, can have children and thus also have an append method, just like a page.

Let's create a new page where we store various file types under a toggable Headning:

file_page = notion.create_page(parent=root_page, title='Page with nested blocks')

heading = uno.Heading1('My files', toggleable=True)
file_page.append(heading)

heading.append([
    uno.File(
        'robots.txt',
        'https://www.google.de/robots.txt',
        caption='Google Robots'
        ),
    uno.Image(
        'https://cdn.pixabay.com/photo/2019/08/06/09/16/flowers-4387827_1280.jpg',
        caption='Path on meadow'
        ),
    uno.Video(
        'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
        caption='Rick Roll'
        ),
    uno.PDF(
        'https://www.iktz-hd.de/fileadmin/user_upload/dummy.pdf',
        caption='Dummy PDF'
        ),
])

Info

We can only add children blocks to a block that is already in notion, i.e. was already appended to a page. You can also programmatically check that with the in_notion property of a block.

Columns & tables

Assume that we want to structure our page a bit more using columns. We define a set of columns using the Columns block. This block now behaves like a list of where each element presents a column. We can now just append to a column, e.g.:

page = notion.create_page(parent=root_page, title='Page with advanced blocks')

cols = uno.Columns(2)
page.append(cols)

cols[0].append(uno.Paragraph('Column 1'))
cols[1].append(uno.Paragraph('Column 2'))

Using the some concept, we can also create a table but this time we specify the cells instead of columns. Creating a 4x6 table with 4 rows and 6 columns where the first row is a header row and thus we set header_row to true. We can use indexing to assign either whole rows or specific cells:

table = uno.Table(4, 3, header_row=True, header_col=False)
table[0] = ('First name', 'Last name', 'Sports')
table[1, 0] = 'Florian'
table[1, 1] = 'Wilhelm'
table[1, 2] = 'Running and bicycling'

page.append(table)

Note

All cells of a table are initialized with an empty string. Therefore, to delete the content of a cell, you need to assign an empty string, i.e. ''. For instance, to delete the first row after the header:

table[1] = ('', '', '')

To access the all rows, i.e. children of the table, we can use children:

rows = table.children
assert isinstance(rows[0], uno.TableRow)
assert isinstance(rows[0], tuple)

In order to access a single row or even a cell, we can use indexing like:

row = table[0]
cell = table[0, 0]
assert cell == 'First name'

When using only a row index, a TableRow object will be returned that is a subclass of tuple with special functionality. For instance, to delete a row from a table you can call delete, e.g.:

row.delete()

Tables can be easily modified in various other ways using the insert_row, append_row methods as well as assigning to the has_header_col and has_header_row properties.

Advanced blocks

There are some additional more advanced blocks like link to a page and synced block. Let's take a look at those.

Assume we have our "Getting Started"-page and want to link to it from the page, we just created above.

target_page = notion.search_page('Getting Started').item()

page.append(uno.LinkToPage(target_page))

A synced block is a different kind of beast. We first have to define an original block, which acts as a container for other blocks that should be synced. Then we append various blocks to it and finally create a synced block, which will be synchronized with the content of the original block.

orig_block = uno.SyncedBlock(uno.Paragraph('This is a synced paragraph'))
page.append(orig_block)

sync_block = orig_block.create_synced()
page.append(sync_block)

Now we have two paragraphs, with the second always reflecting the content of the first.

Working with texts

So far we passed to most blocks basic strings and only provided in some cases a colour for the whole block. Notion allows much fancier formatting for texts, i.e. rich texts, but also inline formulas and mentions of various types. All this is supported via the Text object, which is a subclass of the ordinary str type. For your convience three different constructor functions, i.e. text, math and mention, are provided that construct a Text object. An example illustrates this:

page = notion.create_page(parent=root_page, title='Page with fancy text')

rt = uno.text('This is a ') + uno.text('bold', bold=True) + uno.text(' word.\n')
assert isinstance(rt, str)  # str with special capabilities!
assert rt.to_markdown() == 'This is a **bold** word.\n'

rt += 'We can even add a normal string to it.\n'
rt += uno.text('Now some ') + uno.text('inline formula: ', color=uno.Color.RED)
rt += uno.math(r'-1 = e^{i \pi}') + '\n'
rt += uno.join([
    uno.text('All', italic=True),
    uno.text('possible', underline=True),
    uno.text('formatting, ', color=uno.BGColor.PURPLE),
    uno.text('is', code=True),
    uno.text('it?', strikethrough=True),
    uno.text('it not?', italic=True, bold=True)
    ])

page.append(uno.Paragraph(rt))

To achieve fancy formatting, we have to mix and match Text objects generated by text, mention and math to compose the formatting we want.

Warning

Be cautious with the operator precedence of += and + in the example above. While rt += 'We can even add a normal string to it.\n' works since rt is of type Text, which supports this, rt += 'We can even add a normal string to ' + uno.text('it.\n') would not work as we would first conatenate a str with a Text before we append it to a Text. A str object would ignore the additional formatting information in the Text object while concatenating.

Let's see how we can mention a person, page, database or even a date.

from datetime import datetime

person = notion.search_user('Florian Wilhelm').item()
dummy_db = notion.create_db(parent=root_page)
dummy_db.title = 'Dummy DB'
intro_page = notion.search_page('Getting Started').item()
my_date = datetime(1592, 3, 14) # or better use the pendulum library

rt = uno.join([
    uno.mention(person),
    uno.mention(dummy_db),
    uno.mention(intro_page),
    uno.mention(my_date)
    ], delim=' : ')
page.append(uno.Paragraph(rt))

Let's take a look how our fancy text page looks like in the end.

Fancy text page

Working with comments

In Notion there are two kinds of comments, page comments at the top of the page and inline comments, which are associated to a block within a page. Page comments are organized in a single discussion, i.e. thread of comments whereas inline comments associated to a single block can have several discussions at once. Besides this structural difference also the capabilities provided by the Notion API differ between page and inline comments. While you can read both kinds of comments and also append comments to existing discussions, you can only start new page discussions but not inline discussions.

Page comments

We can get the single discussion thread of a page by calling the comments property of the page. This will return a Discussion container object for the pages Comments, which behaves like a list, so you can use indexing, get the length, etc. To create a new comment we can just use the append method of the Discussion object. Here is an example:

new_page = notion.create_page(parent=root_page, title='Page with page comments')
discussion = page.comments

assert len(discussion) == 0
discussion.append('My first page comment!')

assert len(discussion) == 1
assert discussion[0].text == 'My first page comment!'

Inline comments

Since inline comments are organized in different discussion threads, we use the discussions property of a block to retrieve a list of Discussion container objects. We can now work with these discussions and append new comments. Here is an example:

page = notion.search_page('Comments').item()
heading_block = page.children[0]
discussion = heading_block.discussions[0]
discussion.append('My first appended inline comment!')