# Example: add a post


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

### Version 5

This version features the ability to import the module without problems
with the app having to be created during the load. This allows it to be
deployed easily with a python app file and to change from a Jupyter
server to a more straightforward uvicorn server

Developments still to add in: 1. Add ability to delete posts from the UI
if admin 2. Add ability to edit posts in situ 3. Check if there is a way
to enable image layout in Obscidian

Sets the default export module for nbdev. All cells marked `#| export`
will be written to `my_blog/core_v5.py`.

------------------------------------------------------------------------

### AppState

``` python

def AppState(
    pdb:Database, posts_t:Table, tags_t:Table, post_tags_t:Table, auth:AuthManager, db:Database
)->None:

```

------------------------------------------------------------------------

### create_database_tables

``` python

def create_database_tables(
    pdb:Database, # database to save posts and all associated tag tables
):

```

*Call self as a function.*

------------------------------------------------------------------------

### create_post_database

``` python

def create_post_database(
    db_path:str, # path to posts database
)->Database: # Creates all the required database tables if they don't already exist

```

*Call self as a function.*

Convenience wrapper that creates a database connection and initializes
all tables.

------------------------------------------------------------------------

### create_app

``` python

def create_app(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

### route

``` python

def route(
    path:NoneType=None
):

```

*Decorator to collect routes without registering them immediately.* Use
@route(‘/path’) or @route() for function-name-based paths.

------------------------------------------------------------------------

### register_routes

``` python

def register_routes(
    app
):

```

*Register all collected routes with the app.*

------------------------------------------------------------------------

### logout

``` python

def logout(
    sess
):

```

*Overwrite logout route from fasthtml_auth to return users to home page
after logging out*

------------------------------------------------------------------------

### intro

``` python

def intro(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

### hx_link

``` python

def hx_link(
    txt, href, cls:str='text-primary underline', target:str='#main-content', kw:VAR_KEYWORD
):

```

*Call self as a function.*

------------------------------------------------------------------------

### hx_attrs

``` python

def hx_attrs(
    target:str='#main-content'
):

```

*Call self as a function.*

This dialogue is part of an nbdev project. When deployed to github the
actions setup for publishing to github pages is resulting in two entries
for this dialogue (see: https://fromlittleacorns.github.io/my-blog/).
Can you use dialogue tools to explore why this might be happening

Yes please create the sidebar file. The pages looks like this:

<figure>
<img
src="05_blog_v5_files/figure-commonmark/e0443ce1-1-8fca75ad-5587-433c-a5a9-354f8590f2bf.png"
alt="image.png" />
<figcaption aria-hidden="true">image.png</figcaption>
</figure>

You can see the duplication there

------------------------------------------------------------------------

### is_logged_in

``` python

def is_logged_in(
    req
):

```

*Call self as a function.*

------------------------------------------------------------------------

### navbar

``` python

def navbar(
    req:Request
):

```

*Call self as a function.*

------------------------------------------------------------------------

### x_icon

``` python

def x_icon(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

### footer

``` python

def footer(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

### social_link

``` python

def social_link(
    k, v
):

```

*Call self as a function.*

------------------------------------------------------------------------

### social_link

``` python

def social_link(
    k, v
):

```

*Call self as a function.*

------------------------------------------------------------------------

### layout

``` python

def layout(
    req, content:VAR_POSITIONAL, htmx, title:NoneType=None
):

```

\*title here is used to set the browser tab label in the
<head>

section and will not be visible on the page. The article title is
appended into the main content and shows below the navbar\*

------------------------------------------------------------------------

### slug_exists

``` python

def slug_exists(
    slug
):

```

*Call self as a function.*

``` python
from datetime import datetime
```

------------------------------------------------------------------------

### get_slug

``` python

def get_slug(
    title
):

```

*Call self as a function.*

------------------------------------------------------------------------

### blogpost

``` python

def blogpost(
    htmx, req, slug:str
):

```

*Call self as a function.*

------------------------------------------------------------------------

### index

``` python

def index(
    req, htmx
):

```

*Call self as a function.*

------------------------------------------------------------------------

### get_tags

``` python

def get_tags(
    tags_tbl
):

```

*Call self as a function.*

------------------------------------------------------------------------

### get_post_tags

``` python

def get_post_tags(
    post_id:int
):

```

*Call self as a function.*

def get_posts(n: Union\[int, None\]=None, tags: Union\[List, None\] =
None, logged_in: bool=False): if tags: place_holders = ‘,’.join(‘?’ \*
len(tags)) private_posts_query = ’’ if logged_in else ‘AND p.private =
False’ query = f”“” SELECT DISTINCT p.\* FROM posts p JOIN post_tags pt
ON p.id = pt.post_id JOIN tags t ON pt.tag_id = t.id WHERE t.name IN
({place_holders}) AND p.published = True {private_posts_query} ORDER BY
p.created DESC ““” if n: query += f” LIMIT {n}” posts =
state.pdb.q(query, tags) else: posts =
list(state.posts_t.rows_where(“published = ? AND (private = False OR
?)”, \[True, logged_in\], order_by=“created DESC”, limit=n)) posts =
\[dict(r) for r in posts\]

    for p in posts:
        p['created'] = datetime.fromisoformat(p['created']) if isinstance(p['created'], str) else p['created']
        p['tags'] = get_post_tags(p['id'])
    return posts

------------------------------------------------------------------------

### get_posts

``` python

def get_posts(
    n:Optional=None, # number of posts to load
    tags:Optional=None, # list of tags
    logged_in:bool=False, # is user logged in
    offset:Optional=None, # where to start to load posts once ordered
)->list: # list of posts

```

*Call self as a function.*

------------------------------------------------------------------------

### tag_pill

``` python

def tag_pill(
    tag_name, selected_tags
):

```

*Call self as a function.*

------------------------------------------------------------------------

### tag_filter

``` python

def tag_filter(
    selected
):

```

*Call self as a function.*

------------------------------------------------------------------------

### tag_badge

``` python

def tag_badge(
    name
):

```

*Call self as a function.*

------------------------------------------------------------------------

### decode_tag_str

``` python

def decode_tag_str(
    tag_str:str
):

```

*Call self as a function.*

``` python
test_tags_decode_str = 'cycling, motorhome, coding'
decode_tag_str(test_tags_decode_str)
assert decode_tag_str(test_tags_decode_str) == {'cycling', 'motorhome', 'coding'}
```

------------------------------------------------------------------------

### sentinal

``` python

def sentinal(
    n:int, # number of posts to load
    offset:int=None, # where to start the load
    tags:str=None, # a list of active tags separated by commas
)->functools.partial(<function ft_hx at 0x7fb9c1b385e0>, 'div'): # Div to add to the end of the list of posts to act as a sentinal

```

*Create a div to add to the end of the posts on the blog. When it
becomes visible then it activates the load of more posts*

@route def blog(htmx, req, tags:str=None): logged_in = is_logged_in(req)
posts_to_load = 10 \# selected is a SET of the name of the selected tags
selected = {unquote(t.strip()) for t in (tags or ’‘).split(’,’) if
t.strip()} filtered = get_posts(n=posts_to_load, tags=selected,
logged_in=logged_in) tag_filter_div = tag_filter(selected) items =
\[post_card(p, req) for p in filtered\] if len(items) == posts_to_load:
items.append(sentinal(offset=0, n=posts_to_load, tags=tags))

    post_content = Div(*items, cls="space-y-2", id="posts-list") if items else P("No posts yet.", cls="text-muted-foreground", id="posts-list")
    if htmx and htmx.target == "posts-list":
        tag_filter_div.attrs['hx-swap-oob'] = 'true'
        return post_content, tag_filter_div
    return layout(req, H2("Blog"), tag_filter_div, Divider(cls=('my-2')), post_content, title="Blog", htmx=htmx)

------------------------------------------------------------------------

### blog

``` python

def blog(
    htmx, req, tags:str=None
):

```

*Call self as a function.*

------------------------------------------------------------------------

### get_more_posts

``` python

def get_more_posts(
    req, n:int, offset:int, tags:str=None
):

```

*Call self as a function.*

I am inplementing an infinte scroll using a sentinal and the /loadmore
route. When I first load /blog it loads 10 posts and has the sentinal at
the end with the following:

<div hx-get="/loadmore?offset=10&amp;n=10&amp;tags=None"
hx-trigger="intersect once" hx-swap="outerHTML" hx-target="this">

</div>

But when the sentinal is activated no more posts are loaded, when I know
there should be more (13 in total)? Can you see why this might happen?

I have updated sentinal, does this look ok def sentinal(n:int,
offset:int=None, tags: str=None): \# n: int posts to load \# offset: int
where to load from \# tags: str string containing selected tags with
comma separation get_str = f”/loadmore?offset={offset}&n={n}” if tags:
get_str += f”&tags={tags}” sentinal = Div( hx_get=get_str,  
hx_trigger=“intersect once”, hx_swap=“outerHTML”, hx_target=“this” )
return sentinal

I guess I can check the tags passed in and if there are none, then not
call the tags parameter so that it will default to None in the loadposts
function.

In the htmx online guidebook they use a htmx trigger “revealled”. Is
this available in fastHTML and if so why use the intersect once. The
full code they have is: {% if contacts|length == 10 %}
<tr>

<td colspan="5" style="text-align: center">

<span hx-target="closest tr" hx-trigger="revealed" hx-swap="outerHTML"
hx-select="tbody &gt; tr" hx-get="/contacts?page={{ page + 1 }}">Loading
More…</span>
</td>

</tr>

{% endif %}

I am happy to use the intersect once, I just wanted to understand the
difference. Can you elaborate on the selection. I understand the filter
they use just results in new rows being selected. In your example you
have a target of “this”, what is that and how do you avoid getting
headers etc?

Ok that makes sense and I think our approach is more efficient. Do I
need to start by setting up pagination within get_posts, or should we
create a new way to retrieve the pages?

Ok I have updated get_posts (I created a new version after the original)

given that the call to get_more_posts is to load more posts and that it
uses outerHTML as a swop, I was returning additional posts and if
necessary a new sentinal, what is the correct syntax for that?

I am confused by your third comment above, I am setting the sentinal
offet so that it will load posts after the first load is complete, is
that not correct?

I can see that when a call is made to /blog/more, ir is going to
/blog/{slug}. how would you recomment i change the routes to avoid this.
its a pain to change the route registration order, i guess i could just
rename it to /load_more ir similar?

------------------------------------------------------------------------

### get_post_image

``` python

def get_post_image(
    p
):

```

*Call self as a function.*

------------------------------------------------------------------------

### check_if_admin

``` python

def check_if_admin(
    req
):

```

*Call self as a function.*

------------------------------------------------------------------------

### post_card

``` python

def post_card(
    p, req
):

```

*Post summary card with optional admin controls*

------------------------------------------------------------------------

### add_post

``` python

def add_post(
    title, content, excerpt:str='', tags:NoneType=None, published:bool=True, created:NoneType=None,
    updated:NoneType=None, slug:str=None, private:bool=False
):

```

*Call self as a function.*

``` python
def post_exists(slug: str) -> bool:
    return bool(_state.posts_t(where='slug=?', vals=[slug]))
```

------------------------------------------------------------------------

### process_upload

``` python

def process_upload(
    content:bytes, filename:str, slug:str=None, overwrite:bool=False
):

```

*Call self as a function.*

------------------------------------------------------------------------

### get

``` python

def get(
    htmx
):

```

*Call self as a function.*

------------------------------------------------------------------------

### save_pending

``` python

def save_pending(
    slug:str, md_name:str, md_content:bytes, images:list
):

```

*Save md content and images to temp storage keyed by slug.* images is a
list of (filename, bytes) tuples.

------------------------------------------------------------------------

### load_pending

``` python

def load_pending(
    slug:str
):

```

*Returns (md_content, images) or None if not found.*

------------------------------------------------------------------------

### clear_pending

``` python

def clear_pending(
    slug:str
):

```

*Call self as a function.*

``` python
@route('/admin/upload')    
def post(upload2: list[UploadFile]):
    files = [(f.filename, f.file.read()) for f in upload2]
    md_files = [(n, c) for n, c in files if n.endswith('.md')]
    img_files = [(n, c) for n, c in files if not n.endswith('.md')]
    results = []
    slug = None
    if img_files and len(md_files)==0:
        return Div(Alert("Please upload a post (.md file) with images, or upload images separately", cls=AlertT.warning))
    for name, content in md_files:
        success, message, slug = process_upload(content, name)
        results.append((name, success, message))
    for name, content in img_files:
        success, message, _ = process_upload(content, name, slug=slug)
        results.append((name, success, message))
    header = ["Name", 'Success', 'Message']
    body = [[r[0], r[1], r[2]] for r in results]
    return Div(H2("Post upload results"), TableFromLists(header, body))
```

------------------------------------------------------------------------

### do_upload

``` python

def do_upload(
    md_files, img_files, slug:str=None, overwrite:bool=False
):

```

*Call self as a function.*

------------------------------------------------------------------------

### post

``` python

def post(
    upload2:list
):

```

*Call self as a function.*

------------------------------------------------------------------------

### post

``` python

def post(
    slug:str
):

```

*Call self as a function.*

###### end-section

I don’t think this is correct as the filename of the md file is not
necessarily going to be slug + ‘.md’. The filename surely needs to come
from the originally supplied name? I think that this flaw is built into
the load and save pending functions as well. Do you agree or have I
misunderstood?

------------------------------------------------------------------------

### rewrite_image_paths

``` python

def rewrite_image_paths(
    content:str, slug:str
)->str:

```

*Call self as a function.*

------------------------------------------------------------------------

### load_md_file

``` python

def load_md_file(
    path:str, image_base:str=None
)->str:

```

*Load markdown file, optionally converting Obsidian image syntax*

------------------------------------------------------------------------

### convert_obsidian_images

``` python

def convert_obsidian_images(
    content:str, image_base:str='/static/image/about'
)->str:

```

*Convert Obsidian \![\[image.ext\]\] syntax to standard markdown
![](./path/image.ext)*

``` python
test_img_1_content = '''test string with obscidian markdown, first without a table:
![[IMG_3932.jpeg]]
next with size:
![[IMG_3932.jpeg|250]]
and with size and position:
![[IMG_3932.jpeg|250|right]]
Then some rubbish
'''
base_path = "/static/image/post_images/post_1"
converted = convert_obsidian_images(test_img_1_content, base_path)
converted
```

    'test string with obscidian markdown, first without a table:\n![](/static/image/post_images/post_1/IMG_3932.jpeg)\nnext with size:\n![](/static/image/post_images/post_1/IMG_3932.jpeg)\nand with size and position:\n![](/static/image/post_images/post_1/IMG_3932.jpeg)\nThen some rubbish\n'

------------------------------------------------------------------------

### about_content

``` python

def about_content(
    
):

```

*Call self as a function.*

------------------------------------------------------------------------

### about

``` python

def about(
    req, htmx
):

```

*Call self as a function.*

------------------------------------------------------------------------

### strava_embed

``` python

def strava_embed(
    activity_id:str
):

```

*Call self as a function.*

``` python
# In your markdown, use:
# {{strava:12345678}}
# This will be replaced with an embedded Strava activity
```

Returns a Strava embed div with the given activity ID. The Strava
embed.js script (loaded in headers) will transform this into a full
embed.

------------------------------------------------------------------------

### process_strava_embeddings

``` python

def process_strava_embeddings(
    page:NotStr
):

```

*Call self as a function.*

Post-processes rendered HTML to find `{strava:ID}` placeholders and
replace them with actual Strava embed divs.

------------------------------------------------------------------------

### process_obsidian_images

``` python

def process_obsidian_images(
    page:NotStr, image_base:str
)->NotStr:

```

*Call self as a function.*

``` python
# Obsidian image syntax examples:
# ![[image.jpg|300]]           - width 300px
# ![[image.jpg|300|right]]     - float right, text wraps
# ![[image.jpg|300x200|center]] - fixed size, centered
```

Post-processes rendered HTML to find Obsidian image syntax
`![[image.jpg|width|position]]` and replace with styled `<img>` tags.
Supports width, height, and positioning (left/right/center).

Additional imports for the custom markdown renderer.

------------------------------------------------------------------------

### EnhancedRenderer

``` python

def EnhancedRenderer(
    args:VAR_POSITIONAL, img_dir:NoneType=None, kwargs:VAR_KEYWORD
):

```

*Custom renderer for Franken UI that handles image paths*

``` python
@route('/googleada316577537ad2b.html')
def get(): return 'google-site-verification: googleada316577537ad2b.html'
```

------------------------------------------------------------------------

### sitemap

``` python

def sitemap(
    
):

```

*Call self as a function.*

Custom markdown renderer that extends `FrankenRenderer`. Automatically
adds `target="_blank"` and `rel="noopener noreferrer"` to external links
(http/https).

### Export

``` python
nbdev.nbdev_export()
```

Exports all cells marked with `#| export` to the `my_blog/core_v5.py`
module file.
