Guides

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 of AppId, 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 or it
  • region-tag is a two-letter ISO 3166-1 alpha-2 region code, e.g. US or IT

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 0
  • followersOnly restriction
  • timeLimited - a bespoke flag that makes the publication collectable for up to 24 hours from the time the post gets finalized on-chain
  • collectLimit
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
    }
  ]
})

Article

An article usually has a fairly large context text and could have some media associated with it.

You can specify this using ContentFocus.ARTICLE as contentFocus.

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 () => {
    await create({
      contentFocus: ContentFocus.ARTICLE,
      locale: 'en',
      content: `
        # The future of Web3 Social Media

        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
        incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 
        nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
        fugiat nulla pariatur.

        ...
			`
    });
  };
  // ...
}

You can add media to it (like they were to be attachments to the article) with the same technique showed for Media posts above.

const url = await uploadMediaFile(selectedFile);
    
await create({
  contentFocus: ContentFocus.ARTICLE,
  locale: 'en',
  content: `
    # The future of Web3 Social Media

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
    incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 
    nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
    Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
    fugiat nulla pariatur.

    ...
  `,
  media: [
    {
      url,
      mimeType: ImageType.PNG
    }
  ]
});

Like for any other publication you can specify Collect Policy via the collect property and Reference Policy via the reference property. See sections above for more details.

Links

Although you can create Text Only or Article posts with links in the content, there is a dedicated type ContentFocus.LINK you should use if the intent of the publication is to just share a URL.

This will help other apps in the Lens ecosystem that might render your Link post to know about the original user intents and maybe show a rich URL preview in place on the raw URL.

import { ContentFocus, ProfileOwnedByMe, useCreatePost } from '@lens-protocol/react-web';
import { uploadJson } from './upload'

function Share({ publisher, url }: { publisher: ProfileOwnedByMe, url: string }) {
  const { execute: create, error, isPending } = useCreatePost({ publisher, upload: uploadJson });

  const share = async () => {
    await create({
      content: url,
      contentFocus: ContentFocus.LINK,
      locale: 'en',
    });
  };
  
  return (
    <button disabled={isPending} onClick={share}>
      Share
    </button>
  );
}