Create your first post
This guide will show you different ways to create a post on Lens
When you create a post, the Lens Protocol requires you to upload the so-called Publication Metadata into a public location. The Lens SDK simplify this process by dealing with the actual Publication Metadata structure so the only thing you need to do is define the content of the publication and the rules around who can interact with your post, read it, and/or collect it.
Publication Metadata upload
The first step is to define your Publication Metadata upload handler. You can define this once and for all and use in different part of your application.
The upload handler needs to:
- accept an opaque
data
structure, - serialize it as JSON if necessary (rest assured the data structure the Lens SDK generates does not contain circular references),
- upload it to a publicly accessible data storage,
- return the URL that will serve the file to consumers.
export const uploadJson = (data: unknown): Promise<string> => {
const serialized = JSON.stringify(data);
const url = // upload serialized to a public location
return url;
}
Metadata as JSON
It's important that the data storage solution you choose is able to serve the file as JSON (i.e.
Content: application/json
) so that later on the Lens API background workers can fetch it and index it correctly.It's not mandatory but it's a good custom to upload Publication Metadata in a location that is in-line with the Lens ethos: decentalized and immutable. IPFS and Arweave are two popular choices in this space.
Text-only post
Once you have you Publication Metadata upload handler, let's see what it takes to create a purely textual post. Here is an overview of the metadata values you can pass in to the create function. Here is an overview of the entire metadata standard.
import { ContentFocus, ProfileOwnedByMe, useCreatePost } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
});
};
// ...
}
In the example above we assumed you defined some kind of composer component. For brevity we omitted the actual input fields and just focused on an hypothetical submit handler. Adapt this to your needs.
You might notice the useCreatePost
hook requires a reference to a publisher: ProfileOwnedByMe
, this is a specialized type of Profile
that is associated with the current logged-in wallet. See Profile Management to know what hooks returns this specialized profile type.
Publication Metadata upload - Full Example (Next.js)
Here is an example using a Next.js API route with Bundlr Network, to show you how you might put it all together.
/* Server */
// Next.js Route Handler example at /api/upload/page.ts (or your API implementation)
export async function POST(req: NextRequest) {
const data = await req.json()
const bundlr = new Bundlr("http://node1.bundlr.network", "matic", process.env.BNDLR_KEY)
await bundlr.ready()
const tx = await bundlr.upload(JSON.stringify(data), {
tags: [{ name: 'Content-Type', value: 'application/json' }],
})
return NextResponse.json({ url: `https://arweave.net/${tx.id}` })
}
/* Client */
// useCreatePost hook
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
// Upload function
async function uploadJson(data: unknown) {
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify(data),
})
const json = await response.json()
return json.url
} catch(err) {
console.log({ err })
}
}
// create post function
async function createPost() {
await create({
content: "Hello World",
contentFocus: ContentFocus.TEXT,
locale: 'en',
})
}
// button
<button onClick={createPost}>Create Post</button>
To view the entire codebase including imports and authentication, click here.
App specific posts
Occasionally some apps might have the desire to flag publications as being generated by them. Contextually the same app might have the need to just show publications created using the app itself.
The Lens SDK approaches this problem at the main configuration level.
import { LensConfig, development } from '@lens-protocol/react-web';
import { bindings as wagmiBindings } from '@lens-protocol/wagmi';
const lensConfig: LensConfig = {
bindings: wagmiBindings(),
environment: development,
};
When you create the LenConfig
you can specify an appId
field. The content of this field will be automatically included as part of the Publication Metadata of any publication (post or comment) created via the SDK. See the Getting Started to see how the LensConfig
is then used.
import { appId, LensConfig, development } from '@lens-protocol/react-web';
import { bindings as wagmiBindings } from '@lens-protocol/wagmi';
const lensConfig: LensConfig = {
appId: appId('bob'),
bindings: wagmiBindings(),
environment: development,
};
The appId
helper used in the example assures type-safety and, in the near future, it will validate the given ID.
Without further configuration, an instance of the Lens SDK configured as above will surface ALL Lens ecosystems publications including the ones flagged with the provided appId
.
If you want to filter content returned by all publications related hooks to just the one created by your app you can specify a sources
array like so.
import { appId, LensConfig, development } from '@lens-protocol/react-web';
import { bindings as wagmiBindings } from '@lens-protocol/wagmi';
const lensConfig: LensConfig = {
appId: appId('bob'),
sources: [appId('bob')],
bindings: wagmiBindings(),
environment: development,
};
From now on, all publication hooks will yield publications created by your application.
More than one source
Given the
sources
is an array ofAppId
, you can also provide more than just your own App ID:sources: [appId('bob'), appId('lenster')]
The post language
In the previous example you might have noticed the locale
property set to en
. This is one of the few fields that are required in order to create a new publication.
The locale string is in the format of <language-tag>-<region-tag>
or just <language-tag>
, where:
language-tag
is a two-letter ISO 639-1 language code, e.g.en
orit
region-tag
is a two-letter ISO 3166-1 alpha-2 region code, e.g.US
orIT
You can just pass in the language tag if you do not know the region or don't need to be specific.
By just specifying the language-tag
portion you are saying the post is primarily in that language (e.g. en
english content that should be suitable to most english speakers). With the region-tag
portion you can specify the use of a dialect or customs typical of a specific region (e.g. en-GB
English as spoke in United Kingdom). This might include but not be limited to: formatting of currency values, formatting of dates, units of measurement, specific dialect declensions.
Collect policy
When not specified the default collect policy is that nobody can collect your post.
You can specify the following collect policies:
- No-collect policy
- Free collect policy
- Charge collect policy
No-collect policy
The publication cannot be collected (same as default, you can omit it if need to).
import { ContentFocus, ProfileOwnedByMe, useCreatePost, CollectPolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
collect: {
type: CollectPolicyType.NO_COLLECT
}
});
}
// ...
}
Momoka support
Starting from Lens SDK v1.1.0, all non-collectable post will automatically leverage the Momoka infrastructure.
Collectable policies
The Lens Protocol supports a variety of collect settings that among other things allow to charge a fee for when a publication gets collected. The underlying collect module configuration is a non-trivial task of selecting the correct Collect Module and define its parameters.
The Lens SDK abstracts this complexity away from you and, while retaining the full potential of the underlying Protocol capabilities, it let you focus on the collect constraints you want to set for your post. The Lens SDK will take into consideration the configuration you provided and select the correct collect module for the job.
Collect NFT Metadata
All collectable policies requires you to define extra metadata fields that are eventually relevant for the Collect NFTs. You might be already familiar with some of these directly or indirectly as they are quite common in other ERC-721 implementations. These fields are typically used in the UI of NFT marketplaces like OpenSea.
The Lens SDK accepts type-safe versions of these fields, so that you can focus on the data and the SDK will take care of the correct formatting.
import { NftAttributeDisplayType, NftMetadata } from '@lens-protocol/react-web'
const metadata: NftMetadata = {
name: 'The name of the collect NFT', // the NFT title on OpenSea
description: 'A short description for the NFT', // also visible on OpenSea NFT details page
// Visible on OpenSea under traits
attributes: [
{
displayType: NftAttributeDisplayType.Date,
value: new Date(), // actual Data instance
traitType: 'DoB'
},
{
displayType: NftAttributeDisplayType.Number,
value: 42, // an actual JS number
traitType: 'Level'
},
{
displayType: NftAttributeDisplayType.String,
value: '#ababab', // an arbitrary JS string
traitType: 'Color'
},
]
}
Followers only restriction
All collectable policies allow to specify if the profile collecting (the wallet owning the profile to be precise) needs to follow the publication author's profile.
This is possible via the followersOnly
field. See the following collect policies examples to see how this field should be used.
Free collect policy
It's a collect policy where the publication can be collected for free. You can restrict the collect to just the followers of your publisher
profile via the followersOnly
field.
import { ContentFocus, ProfileOwnedByMe, useCreatePost, CollectPolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
collect: {
type: CollectPolicyType.FREE,
metadata, // as defined before
followersOnly: true
}
});
}
// ...
}
Collect fees
The Lens SDK defines all currency amounts via a lightweight abstraction called Amount
. Collect policy fees are defined via a specialized Amount<Erc20>
type. The possible ERC-20 tokens do depend on the underlying Lens Protocol capabilities. With time the Lens Protocol will support more and more ERC-20 tokens.
You can find all ERC-20 currently supported by the Lens Protocol via the useCurrencies
hook. You can use this hook to create a simple currency selector UI like so:
import { useCurrencies, Erc20 } from '@lens-protocol/react-web';
function CurrencySelector({ onChange }: { onChange: (currency: Erc20) => void) {
const { data: currencies, error, loading } = useCurrencies();
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const currency = currencies.find((currency) => currency.symbol === event.target.value);
if (currency) onChange(currency);
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<select onChange={handleChange}>
{currencies.map((currency) => (
<option key={currency.hash} value={currency.symbol}>{currency.name}</option>
))}
</select>
);
}
The example is written for a web application but it can simply be adapted to a React Native app (just remember to import from @lens-protocol/react
instead of @lens-protocol/react-web
).
Mirror reward
The mirror reward represent the percentage of the fee that goes to the mirror profile's owner when the publication gets collected via a mirror.
Collect policies that involve a fee do require you to specify this a mirrorReward
field. The field accepts a percentage value between 0 and 100 and has precision up to 0.01 percentage points (e.g. 0.001 = 0%
, 0.01 = 0.01%
, 0.1 = 0.1%
, 1 = 1%
, 99 = 99%
, 99.99 = 99.99%
, 99.999 = 100%
).
Collect limit (optional)
Collect policies involving a fee allows you to also specify an optional maximum number of Collect NFTs that can be generated from a given publication. This effectively allow you to create scarcity of a given publication Collect NFT.
The collect limit can be specified via the collectLimit
field and it's an integer number between 1
and Number.MAX_SAFE_INTEGER
.
If not provided there is virtually no limit to the amount of Collect NFTs that can be minted from the given publication.
Single recipient collect policy
The single recipient collect policy is a fee-based collect policy that allows to define a single recipient for the collect fee.
You must also provide:
metadata
mirrorReward
- it could be 0followersOnly
restrictiontimeLimited
- a bespoke flag that makes the publication collectable for up to 24 hours from the time the post gets finalized on-chaincollectLimit
import { Amount, ContentFocus, Erc20, ProfileOwnedByMe, useCreatePost, CollectPolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const [currency, setCurrency] = useState<Erc20 | null>(null); // use setCurrency in your <CurrencySelector onChange />
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
if (!currency) return;
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
collect: {
type: CollectPolicyType.CHARGE,
metadata: { /* NftMetadata */ },
followersOnly: true, // only followers can collect
collectLimit: 100, // only 100 available (this is optional)
mirrorReward: 5, // 5% goes to the mirror author if collected via a mirror
fee: Amount.erc20(currency, 1),
recipient: publisher.ownedBy, // or another address the user defines
timeLimited: false
}
});
}
// ...
}
Multiple recipients collect policy
The multiple recipient collect policy is very similar to the single recipient collect policy but with 2 main differences:
- the most obvious one is that you can specify more than one recipient address. It will also requires you to specify the percentage value (number between 0 to 100 with) they would get from each collect fee. The total percentage amount should add up to 100%
- the second difference is that you can specify a time limit as a future predefined deadline via the
endTimestamp
field
Everything else is as per other collect policies: collectLimit
, mirrorReward
, followersOnly
and NFT metadata
.
import { Amount, ContentFocus, Erc20, ProfileOwnedByMe, useCreatePost, CollectPolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const [currency, setCurrency] = useState<Erc20 | null>(null); // use setCurrency in your <CurrencySelector onChange />
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
if (!currency) return;
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
collect: {
type: CollectPolicyType.CHARGE,
metadata: { /* NftMetadata */ },
followersOnly: true, // only followers can collect
collectLimit: 100, // only 100 available (this is optional)
mirrorReward: 5, // 5% goes to the mirror author if collected via a mirror
fee: Amount.erc20(currency, 10),
recipients: [
{
recipient: publisher.ownedBy,
split: 80, // 80%
},
{
recipient: '0x....',
split: 20, // 20%
}
],
endTimestamp: new Date(2023, 11, 25).getTime()
}
});
}
// ...
}
Reference policy
When not specified the the default reference policy is that anybody can comment and/or mirror your post.
You can specify the following reference policies:
- Anybody
- Followers only
- Degrees of separation
Anybody
Anybody can reference the post (same as default, you can omit it if need to)
import { ContentFocus, ProfileOwnedByMe, useCreatePost, ReferencePolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
reference: {
type: ReferencePolicyType.ANYONE
}
});
}
// ...
}
Followers only
Only your followers can comment and/or mirror the post
import { ContentFocus, ProfileOwnedByMe, useCreatePost, ReferencePolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
reference: {
type: ReferencePolicyType.FOLLOWERS_ONLY
}
});
}
// ...
}
Degrees of separation
You can fine tune what reference operations are restricted (comments and/or mirrors) and the maximum distance a profile can be from your profile in your social graph. This distance is called "degree of separation" and is a cardinal number where:
- 1: only your direct followers can reference the post
- 2: up to followers of your followers can reference the post
- N: up to N hops in the followers-of-followers chain can reference the post
import { ContentFocus, ProfileOwnedByMe, useCreatePost, ReferencePolicyType } from '@lens-protocol/react-web';
import { uploadJson } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async (content: string) => {
await create({
content,
contentFocus: ContentFocus.TEXT,
locale: 'en',
reference: {
type: ReferencePolicyType.DEGREES_OF_SEPARATION,
params: {
degreesOfSeparation: 2, // only direct followers and followers of direct followers can comment
commentsRestricted: true, //only comments are restricted
mirrorsRestricted: false, // anybody can mirror
}
}
});
}
// ...
}
The special case for 0 degrees of separation
The careful reader might have noticed that degreesOfSeparation
could technically accept 0
as possible value. This is effectively achieving the same result as blocking all reference activities (according to the commentsRestricted
, mirrorsRestricted
flags) for the given post.
Media post
By media posts we mean publications where the primary objective is to share an image, audio, video file OR a collection of thereof.
In order to define a media post you must specify a contentFocus
that is one between: ContentFocus.AUDIO
, ContentFocus.IMAGE
, ContentFocus.VIDEO
.
You need to first upload the files into a public location then provide the media details to the useCreatePost
callback arguments like so:
import { useState } from 'react';
import { ContentFocus, ImageType, ProfileOwnedByMe, useCreatePost } from '@lens-protocol/react-web';
import { uploadJson, uploadMediaFile } from './upload'
function Composer({ publisher }: { publisher: ProfileOwnedByMe }) {
const [selectedFile, selectFile] = useState<File | null>(null)
const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });
const onSubmit = async () => {
if (!selectedFile) return
const url = await uploadMediaFile(selectedFile);
await create({
contentFocus: ContentFocus.IMAGE,
locale: 'en',
media: [
{
url,
mimeType: ImageType.PNG
}
]
});
}
// ...
}
In the example above we assumed you defined an uploadMediaFile(file: File): string
function that is able to accept a Web API File
instance, upload it to a non-specified public location and return the public URL from where the file will be served from. Given the degree of freedom one have it's difficult to provide an exhaustive example that works for everybody. As reference you can find an example function in the Lens SDK monorepo.
Multiple media files
You can create a publication that contains multiple media files of the same typology (audio, image, or video) by providing more items to the media
field:
await create({
contentFocus: ContentFocus.IMAGE,
locale: 'en',
media: await Promise.all(files.map(async (file) => {
const url = await uploadMediaFile(file);
return {
url,
mimeType: ImageType.PNG
};
}))
});
Whilst there is almost not virtual limit to the number of media files referenced by the same publication, try to be conscious that Lens publications are portable and might be served by other apps in the Lens ecosystem.
Media file upload
As per Publication Metadata, the Lens SDK does not force you to make any choice on where to upload your media files. We do still recommend to follow the Lens ethos and use a decentralized and possibly immutable data storage solution (e.g. IPFS or Arweave). This is even more true if the publication is a collectable posts. The potential collectors will receive collect NFTs that are truly immutable and decentralized starting from the on-chain data, passing through the NFT metadata, all the way to the referenced media files.
Contextual information
Alongside the media
list you can provide contextual informations via the content
field. This is meant to give the consumer some corollary information about the media files. Think of this as an opportunity to add a video description, lyrics for an song, the description of an image gallery.
await create({
contentFocus: ContentFocus.AUDIO,
locale: 'en',
content: `
# Lyrics
Coming out of my cage
And I've been doing just fine
Gotta gotta be down
Because I want it all
It started out with a kiss
How did it end up like this?
It was only a kiss, it was only a kiss
...
`,
media: media: [
{
url,
mimeType: AudioType.MP3,
altTag: 'Mr. Brightside - The Killers (cover by Ronnie)'
}
]
});
Alternative media text
It's good custom to provide a media alternative content in the form of a textual representation of the media to any media item you specify. You can do so via the altTag
field of each media
item.
await create({
contentFocus: ContentFocus.IMAGE,
locale: 'en',
media: media: [
{
url,
mimeType: ImageType.PNG,
altTag: 'A flying blue elephant'
}
]
});
Cover images
A common need when specifying Audio and Video posts is to provide a cover image that could represent the cover picture of an song or can be used as thumbnail for a video.
You can do so by uploading an image using the same facilities you used for the main media file and specify the cover image URL via the cover
field of each media
item.
const songUrl = await uploadMediaFile(songFile);
const coverUrl = await uploadMediaFile(coverImage);
await create({
contentFocus: ContentFocus.AUDIO,
locale: 'en',
media: [
{
url: songUrl,
mimeType: AudioType.MP3,
cover: coverUrl
}
]
})
Updated 13 days ago