Imagine content could live on the internet, completely decoupled from the

platform someone chooses to consume it on. Well, guess what, the future is here!

The nostr protocol is perfect for sharing content because it is client-agnostic.

It even comes with a special event type for long-form content,

taxonomy, markdown support, etc.

You can use nostr as your personal headless content management platform

and weave it into your NestJS website.

In this blog post, I'll explain how.

## Requirements

Before we get started lets talk about what needs to happen

in order to display your nostr posts on your personal website.

We are going to use a couple of libraries as well as NextJS.

### Getting content from the nostr network

First, we gotta connect to some nostr relays and query them for your blog posts.

We are going to use `nostr-tools` for that.

Nostr-tools is like a Swiss army knife for anything nostr-related

### Extracting metadata from the posts

SEO is important for any web blog. So we are going to use the native support

for taxonomy in nostr long-form content to generate metadata like tags and OG data.

We are going to use NextJS inbuilt metadata functionality.

### Rendering markdown

Finally, we need to take the markdown string we got from the relay and

convert it into HTML to return it from our server.

There are many libraries to render Markdown in React, but only

a few support React Server Components. We are going to use

`next-mdx-remote` for this.

## Creating our blog post component

Our BlogPost component will be responsible for retrieving a specific blog post

from the nostr network and then parsing its content into HTML.

This component is going to be an asynchronous server-rendered component.

Here is a basic code example.

```ts

import { MDXRemote } from "next-mdx-remote/rsc";

async function BlogPost({ nevent }: {nevent: `nevent1${string}`}) {

const event = await getCachedSinglePost(nevent);

if (!event || event.pubkey !== authorData.pubkey) {

notFound();

}

return (

source={event.content}

/>

);

}

export default BlogPost;

```

So what is going on here? BlogPost is an asynchronous component that

receives a `nevent` entity as prop.

It then proceeds to look up the event in question.

If none is found, it will return a 404 using NextJS' notFound function.

Finally it passes the event's content on to `MDXRemote` which takes care of the parsing.

### Getting a post from nostr

Lets take a look at our `getCachedSinglePost` function, shall we?

```ts

import { Event, SimplePool, nip19 } from "nostr-tools";

import { cache } from "react";

const pool = new SimplePool();

async function getSinglePost(nevent: Nevent): Promise {

const {

data: { id, relays },

} = nip19.decode(nevent);

const relayList =

relays && relays.length > 0 ? relays : ["wss://relay.damus.io"];

const event = await pool.get(

relayList,

{

ids: [id],

},

{ maxWait: 2500 },

);

if (!event) {

return undefined;

}

return event;

}

export const getCachedSinglePost = cache(getSinglePost);

```

This function is responsible for parsing our nevent string,

connecting to the nostr network

and receiving the event in question.

Here is a quick rundown:

1. Parsing the nevent entity for the event id and relay hints

2. If no relay hints are present, we fall back to a default

3. We are using nostr-tools `SimplePool` to connect to a pool of relays

and look up the id in question

4. If after 2.5 no event is found we return `undefined`, otherwise the event

5. Optional: In order to avoid unnecessary duplications we use react's cache function.

It will make sure that even if called multiple times in a single request,

it is only executed once.

### Extracting additional info

Nostr long-form content comes with standardized tags for

featured-images, taxonomy, summary, title, and so on.

All that information can be found in an events tags array.

What follows is a utility function to extract these tags from an event:

```ts

export function getTagValue(e: Event, tagName: string, position: number) {

for (let i = 0; i < e.tags.length; i++) {

if (e.tags[i][0] === tagName) {

return e.tags[i][position];

}

}

}

const title = getTagValue(event, "title", 1);

const summary = getTagValue(event, "summary", 1);

```

This can be used to add a H1 heading to our BlogPost component:

```ts

async function BlogPost({ nevent }: {nevent: `nevent1${string}`}) {

const event = await getCachedSinglePost(nevent);

if (!event || event.pubkey !== authorData.pubkey) {

notFound();

}

const title = getTagValue(event, "title", 1);

return (

{title}

source={event.content}

/>

);

}

export default BlogPost;

```

### Styling our Markdown

`MDXRemote` takes care of parsing our Markdown into HTML.

If you have global styles setup for all the required HTML tags,

then you are good to go, but if you rely on classes,

you will need to add custom components to MDXRemote.

To do so, create an object `components` with a function representing each

Tag you wish to replace and pass it to MDXRemote.

Our final might look something like this:

```ts

const components = {

p: (props: any) => (

{props.children}

),

};

async function BlogPost({ nevent }: {nevent: `nevent1${string}`}) {

const event = await getCachedSinglePost(nevent);

if (!event || event.pubkey !== authorData.pubkey) {

notFound();

}

const title = getTagValue(event, "title", 1);

return (

{title}

source={event.content}

components={components}

/>

);

}

export default BlogPost;

```

## Creating a blog post page with metadata

In the next step, we will create a dynamic route segment

and within render the component we just created.

For my blog, I went with `domain.com/blog/[nevent]`.

To achieve this path with NextJS' app router we create a file

with this path: `src/app/blog/[nevent]/page.tsx`.

The brackets will tell NextJS that this is a dynamic route segment

and will automatically pass the parameter to the page component

as props.

Here is our starting point:

```ts

function BlogEntry({ params }: {params: {nevent: `nevent1${string}`}}) {

return (

Loading...

}>

);

}

export default BlogEntry;

```

In this page component, we use the BlogPost component we created in the first step.

We pass to it the nevent that we receive from the router.

Because this component is asynchronously rendered, we wrap it in a Suspense boundary,

in order to render a fallback, while the BlogPost component is still fetching data.

### Adding metadata

Of course, our blog posts need metadata for SEO and sharing.

NextJS comes with an inbuilt function that lets you dynamically

generate a metadata object, that will be sent along with each request.

We simply add the following code to our `page.tsx` file

(but outside the component itself).

```ts

export async function generateMetadata({

params,

}: BlogEntryProps): Promise {

const event = await getSinglePost(params.nevent);

if (!event) {

return {};

}

const title = getTagValue(event, "title", 1);

const image = getTagValue(event, "image", 1);

return {

title,

openGraph: {

title,

images: [image || ""],

},

twitter: {

title,

images: [image || ""],

},

};

}

```

Notice that we called `getSinglePost` a second time, but because we wrapped

its functionality in `cache` it will only get executed once.

We then use our `getTagValue` utility to extract metadata from our event and

return a Metadata object as required by NextJS.

### Statically prerendering your posts

Dynamic route segments are rendered on request by default, but for performance

reasons it might be wise to render them beforehand. NextJS allows you to

specify a list of route ids that you wish to render at build time using the

`generateStaticParams` function.

Simply add this function outside the page component but in the same file.

```ts

export async function generateStaticParams() {

const [events, relays] = await getCachedAllPosts("YOUR PUBKEY HERE");

return events.map((event) => ({

nevent: nip19.neventEncode({ id: event.id, relays }),

}));

}

```

`getAllPosts` is another cached function that looks like this:

```ts

const relays = ["wss://relay.damus.io"];

async function getPostsByAuthor(pubkey: string): Promise<[Event[], string[]]> {

const events = await pool.querySync(relays, {

kinds: [30023],

authors: [pubkey],

});

return [events, relays];

}

export const getCachedAllPosts = cache(getPostsByAuthor);

```

This function will get all long-form posts on a list of relays

and then add their nevent entity ids to an array as expected by NextJS.

Next will then use this array to generate these paths at build time.

## Wrapping Up

We have now successfully created a BlogPost component that looks up an event on nostr

and parses its Markdown contents into HTML.

We also created a dynamic route segment that receives and nevent entity as a parameter,

looks up metadata for it and renders our BlogPost component.

I used the exact same approaches to build my personal blog [my2sats](https://my2sats.space),

so if you are interested in a real-life code example take a look at its [repository](https://github.com/Egge21M/my2sats)

(and let me know if you find any bugs ^^).

If you have any questions about this guide please contact me on

[nostr](nostr:npub1mhcr4j594hsrnen594d7700n2t03n8gdx83zhxzculk6sh9nhwlq7uc226)

or on [Twitter](https://twitter.com/Egge21M)

Reply to this note

Please Login to reply.

Discussion

Neat!!!

Looks like the codeblocks are not rendered correctly by YakiHonne. Something they need to fix I think.

Yeah I noticed that as well. You can view the full event with all the code blocks and syntax highlighting on my blog though:

https://my2sats.space/blog/nevent1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsqgr4f4zsxehsukp4ucv4320gvpq7qmgmc0t28j9p0kywgrqj4rhkmvssrp9j

Thanks Sebastian we are on it yes πŸ™. However they are rendered perfectly on the mobile app πŸ‘Œ.

Ah yes on mobile it’s perfect! πŸ‘πŸ»