This is NoStrut, a client on Ubuntu Touch nostr:npub1qlpdj0c69dkyz82qecnrg6gjm3wmggr43p392xr7cylgy9gnun6s565lcp 
For all the Dutch out there! I'm co-hosting this workshop to make your computer free of bigtech helping installing Linux Mint as your operating system.
#Dutch: Zaterdagmiddag 27 januari organiseer ik met de coöperatie Nieuw West-Brabant en stichting KeuzeVrijBijMij een workshop 'maak je computer bigtech-vrij'. Als deelnemer aan deze workshop ga je deze middag het free & open source besturingssysteem Linux Mint installeren op jouw computer. Doe je mee?
#FOSS #LinuxMint #BigTechFree #GoodbyBigTech #opensource
This is cool!
- Drupal Association secures $300,000 in funding from Sovereign Tech Fund
#Drupal #OpenWeb #FreedomTech
Ah yes on mobile it’s perfect! 👍🏻
Any plans on creating / posting a blogpost from your website (client)? I'm building something similar with Drupal (check my long-form articles about this for more info).
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 (
);
}
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)
Neat!!!
Looks like the codeblocks are not rendered correctly by YakiHonne. Something they need to fix I think.
Anyone interested in helping bring nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft's Oauth-like login experience to many more apps?!
I've started a simple drop-in library that will enable any web client to trigger the creation of a new account (via nsecbunker) when a user without a NIP-07 extension tries to log in.
I should integrate this in Nuxstr 🥸
Somewhat related perhaps? 🤓
https://sebastix.nl/blog/nostr-integration-for-cchs-social-drupal-cms/
Wha about module / plugin which provides a zap button (for zapping the content on the page)?
I could build that for Drupal.
Interesting! 👀 Why too early?
Devs: want to make a yuge impact for #nostr?
Wordpress runs over 40% or about 800 million websites.
Per discussion with nostr:npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac and nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s there is alignment that a nostr comment box plugin for Wordpress could be a useful way to do marketing for nostr, and drive new users.
Announcing a name your own #bounty for a dev or team of devs to create a Wordpress nostr comment plugin. I hereby volunteer nostr:npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac for design. Tag nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s for the bounty size in sats you would like.
cc nostr:npub1a7n2h5y3gt90y00mwrknhx74fyzzjqw25ehkscje58x9tfyhqd5snyvfnu nostr:npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev nostr:npub1j8y6tcdfw3q3f3h794s6un0gyc5742s0k5h5s2yqj0r70cpklqeqjavrvg
Where are the WordPress developers on Nostr? I know WordPress, but I don’t like it 😅
I’m tinkering around with #Drupal: https://sebastix.nl/blog/how-could-drupal-adopt-nostr/
"Now let’s get on with 2024. Once we’ve finished the full release of the HEY Calendar this week, we’ll be focused on routing around all this app store nonsense with our new ONCE series of products that we’ve bet exclusively on PWA technology with. Can’t wait to share!"
#PWAsAreWinning
https://world.hey.com/dhh/apple-approves-the-hey-calendar-c76ebd9c
Coracle is giving me a hard time on tagging here nostr:nprofile1qqs04xzt6ldm9qhs0ctw0t58kf4z57umjzmjg6jywu0seadwtqqc75sppemhxue69uhkummn9ekx7mp0qyghwumn8ghj7vf5xqhxvdm69e5k7tcpzdmhxue69uhhyetvv9ujue3h0ghxjme0mantpa , please don't miss out this ;-)
#m=image%2Fjpeg&dim=1920x1080&blurhash=ipDJ%23X%24xRObdWXWAj%3FofofyGn%23oHWXazj%3Faxj%5BofOHRjozs-j%3Dj%3Fn%7EWVa%23R-bIf%2Cf6oIjYayjsjZV%3FkDbcf5oLjYjsayax&x=251022dacbec95d6c0b8b8510748726b5ed1269bf36bde791227c55ae10f197a
#m=image%2Fjpeg&dim=1920x1080&blurhash=i*Dc%5D%7Ct7ayWVWBj%5Bn%23oIWByGa%7Dayjtaya%7DaeayjsJFWBoJoJoej%5BWXWWoejcj%3Fayaya%7DfPofj%5DbHs8j%5BWVaya%23aykDj%5DWW&x=fb89fb4942949a83667e07a3a0348ecf5194bd3a43167496ef22250c6d47073d
#m=image%2Fjpeg&dim=1920x1080&blurhash=i%24EppRIUxuWAe-aej%5Bj%40aeyZa%23oeaeaeaej%5Bayjsx%5EflV%40j%5Dazj%5Bayj%5BayM%7CogWBkCogj%5DfQaykCaKkCfkkCkDkCayj%5Bj%5D&x=fc9a15ca5d079f2f7adff81b61d9a74f4db7e848fbbadf611fca3d03b68e1108

