Content Creation

Learn how to create publications on Lens.

We'll primarily use Posts as examples to demonstrate the creation of a Lens Publication. However, many of the concepts discussed here are also applicable to Comments and Quotes. For details on other types of publications, please refer to the Referencing Content guide.

Creating a Post

Lens Publication content, including text, images, videos, and more, is stored in what's known as metadata. This metadata is a JSON file linked to the Lens Publication via its public URI.

To create a Post on Lens, follow these steps:

  1. Create the Publication Metadata object.

  2. Upload this object to a publicly accessible URI.

  3. Use this metadata URI to create the Lens Post.


Create Publication Metadata

The Publication Metadata Standard is a specification that outlines the structure of Lens Publication Metadata objects.

This is a self-describing specification, meaning the data itself includes all the information necessary for its validation.

You can construct Publication Metadata in two ways:

  • By utilizing the @lens-protocol/metadata package

  • Manually, with the help of a dedicated JSON Schema

Install the @lens-protocol/metadata package with its required peer dependencies:

npm install zod @lens-protocol/metadata@latest

The Publication Metadata Standards encompass various types of content. Below is a list of the most common ones.

Used to describe content that is text-only, such as a message or a comment.

import { textOnly } from '@lens-protocol/metadata';
const metadata = textOnly({  content: `GM! GM!`,});

See textOnly(input): TextOnlyMetadata reference doc.


Upload Metadata

You can host Publication Metadata anywhere, as long as it's publicly accessible via a URI and served with the appropriate Content-Type: application/json header.

Commonly, integrators use solutions like IPFS or Arweave for hosting Publication Metadata.

In the upcoming examples, we'll assume you have a function named uploadJson. This function takes a JavaScript object, uploads it, and returns the public URI of the uploaded file.

import { textOnly } from "@lens-protocol/metadata";import { uploadJson } from "./my-upload-lib";
const metadata = textOnly({  content: `GM! GM!`,});
const metadataURI = await uploadJson(metadata);

You can also upload media files to IPFS or Arweave, then reference their URIs in the Publication Metadata prior to uploading it.


Create the Post

Once you've uploaded the Publication Metadata, you can create a Lens Post.

You must be authenticated with the Profile that will author the Lens Publication. See Profile Login for more information.

You can use the useCreatePost hook to create a Lens Post.

Available in @lens-protocol/react-web and @lens-protocol/react-native

Sponsored and Signless

The hook choose the Signless Experience when possible; otherwise, it will fall back to a signed experience.

import { textOnly } from '@lens-protocol/metadata';import { useCreatePost } from '@lens-protocol/react-web';
import { uploadJson } from "./my-upload-lib";
export function Composer() {  const { execute, loading, error } = useCreatePost();
  const submit = async (event: React.FormEvent<HTMLFormElement>) => {    event.preventDefault();    const formData = new FormData(event.currentTarget);
    // create post metadata    const metadata = textOnly({      content: formData.get('content') as string,    });
    // publish post    const result = await execute({      metadata: await uploadJson(metadata),    });
    // detect if an early error occurred    if (result.isFailure()) {      window.alert(result.error.message);      return;    }
    // optional: wait for the transaction to be mined and indexed    const completion = await result.value.waitForCompletion();
    // detect if a minining/indexing error occurred    if (completion.isFailure()) {      window.alert(completion.error.message);      return;    }
    // post was created    const post = completion.value;    window.alert(`Post ID: ${}`);  };
  return (    <form onSubmit={submit}>      <textarea        name="content"        minLength={1}        required        rows={3}        placeholder="What's happening?"        disabled={loading}      ></textarea>
      <button type="submit" disabled={loading}>        Post      </button>
      {!loading && error && <p>{error.message}</p>}    </form>  );}

This example utilizes HTML5 form validation, but it can be modified to use your preferred form state management library.


If you wish you can force the useCreatePost hook to use the Self-funded flow via the sponsored flag.

const { execute, loading, error } = useCreatePost();
// ...
const result = await execute({  metadata: await uploadJson(metadata),  sponsored: false,});

This action will prompt the SDK to utilize the user's wallet for sending the transaction and covering the associated gas fees.

That's it—you've just learned how to create your first Lens Post.

Optimistic UI Updates

This feature, currently in its experimental stage, is exclusive to the Lens React SDK.

To create a Post using this optimistic behavior, the steps are the following:

  1. Create your uploader

  2. Create the Publication Metadata object.

  3. Create the Lens Post passing the whole metadata object.


Create Uploader

An uploader is an implementation of the Uploader class. It encapsulates your strategy for uploading Publication Metadata object and any media files referenced by it.

Available in @lens-protocol/react-web and @lens-protocol/react-native

You can define two types of uploaders:

  • Stateless uploader: Handles each file upload individually.

  • Stateful uploader: Manages multiple file uploads according to a custom batching strategy.

If you can upload media files and the Publication Metadata JSON file independently, consider using a stateless uploader. It's the simplest option.

Define an asynchronous UploadHandler function. This function should take a File as input and return the URI of the uploaded file as a string.

You can define a stateless uploader by passing the UploadHandler function to the Uploader constructor, like so:

import { useMemo } from "react";import { Uploader, useIdentityToken } from "@lens-protocol/react-web";
export function useUploader() {  const idToken = useIdentityToken();
  return useMemo(() => {    return new Uploader(async (file: File) => {      const response = await fetch("/api/file", {        method: "POST",        body: file,        headers: {          Authorization: `Bearer ${idToken}`,        },      });
      if (!response.ok) {        throw new Error("Failed to upload");      }
      return response.headers.get("Location")!;    });  }, [idToken]);}

The above example assumes the existence of an /api/file endpoint that accepts a POST request with a file in the body. It returns a Location header containing the public URI of the uploaded file.

Lens dApps often use the Lens Identity Token to authenticate API requests to their backend, as described here. Therefore, we've included it in the example as an Authorization header. Please adapt this to suit your own authentication strategy.


Create Publication Metadata

Creating the Publication Metadata object for the optimistic approach is similar to the non-optimistic one, with the exception that you can directly reference local files using their local URLs.

In the next step, the React SDK will handle the upload of any referenced media files using the uploader you defined. It will replace the local file URLs with the uploaded URIs and then upload the Publication Metadata JSON file.

Here we will show how to embed local URLs in the metadata object for both React Web and React Native environments.

You can use the fileToUri helper function to convert a File object into a blob URL.

import { image, MediaImageMimeType } from "@lens-protocol/metadata";import { fileToUri } from "@lens-protocol/react-web";
// ...
const submit = async (event: React.FormEvent<HTMLFormElement>) => {  event.preventDefault();  const form = event.currentTarget;  const formData = new FormData(form);
  const file = formData.get('image') as File;
  // create post metadata  const metadata = image({    image: {      item: fileToUri(file),      type: MediaImageMimeType.JPEG,      altTag: "My image",    },  });
  // ...};
return (  <form onSubmit={submit}>    <input type="file" name="image" accept={MediaImageMimeType.JPEG} required />    <button type="submit">Post</button>  </form>);


Create the Post

Next, use the useOptimisticCreatePost hook to optimistically create a Lens Post.

import { useOptimisticCreatePost } from "@lens-protocol/react-web";import { textOnly } from "@lens-protocol/metadata";
import { useUploader } from "./useUploader";
// ...
const uploader = useUploader();const { data, execute, loading, error } = useOptimisticCreatePost(uploader);
const post = (content: string) => {  const metadata = textOnly({ content });
  // invoke the `execute` function to create the post  const result = await execute({    metadata,  });
  // check for failure scenarios  if (result.isFailure()) {    window.alert(result.error.message);  }};

Immediately after the execute call, the data property updates with the optimistic Post object.

Use data to render the Post immediately in your UI. All operational flags (e.g. post.operations) will signal that no operations can be conducted on the Post yet.

The hook enables optimistic behavior only when the Signless Experience is possible. If not, the hook will not return optimistic data.

This limitation is due to the experimental nature of the hook. As its usage expands, the optimistic behavior will accommodate more scenarios.

The optimistic behavior will also be disabled if you use Open Action modules or Reference modules that are not flagged as sponsoredApproved and signlessApproved in their module metadata. Similarly, unverified modules (those with verified: false in their metadata) or unregistered modules will also disable the optimistic behavior, as they are not approved for Signless and Sponsored experiences.

Optionally, you can wait for the full completion of the post creation.

const post = (content: string) => {  const metadata = textOnly({ content });
  // invoke the `execute` function to create the post  const result = await execute({    metadata,  });
  // check for failure scenarios  if (result.isFailure()) {    window.alert(result.error.message);  }
  // wait for full completion  const completion = await result.value.waitForCompletion();
  // check for late failures  if (completion.isFailure()) {    window.alert(completion.error.message);    return;  }

Eventually, the data property will automatically update, making the final Post object available for further interactions. At this stage, you can enable all operations on the Post.

Additional Options

Below are some common options you can use to customize Lens Publications.

App-Specific Publications

You can create and retrieve app-specific publications by embedding your app's identifier in the Publication Metadata. Optionally, you can also cryptographically sign it so to avoid app impersonation.


Specify App ID

Define an App ID that uniquely identifies your application (e.g., Hey, Orb) and include it in the Publication Metadata as shown below.

import { textOnly } from '@lens-protocol/metadata';
const metadata = textOnly({  appId: "<my-app-id>",  content: `Hello world!`,});

While this example uses the textOnly helper, the same principle applies to all other metadata types.


Optional: Sign Metadata

Generate an EVM key pair and communicate the corresponding address to the Lens Protocol team via the Lens Developer Garden Telegram group alongside with your App ID.

Use the signMetadata helper function to sign the Publication Metadata object with the private key.

import { privateKeyToAccount } from "viem/accounts";import { signMetadata } from "@lens-protocol/metadata";
const account = privateKeyToAccount(process.env.APP_PRIVATE_KEY);
const signed = await signMetadata(metadata, (message) =>  account.signMessage({ message }));

This process results in a signed copy of the original Publication Metadata object. Use this signed object to create your publication, as previously explained on this page.

Avoid embedding the private key in any client-side application code. Instead, use a secure backend service to sign the metadata.


Fetch App-Specific Metadata

Next, when requesting publications, include your App ID in the list of apps to fetch publication metadata for alongside any other App IDs you want to include.

Omit this filter to fetch publications for all apps, including yours.

The Lens React SDK allows you to do this on an hook basis. See some examples below.

import { appId, PublicationType, usePublications } from '@lens-protocol/react-web';
// ...
const { data, loading, error } = usePublications({  where: {    publicationTypes: [PublicationType.Post]    metadata: {      publishedOn: [appId('<my-app-id>')],    },  },});


You can specify the language of a publication's content using the locale field in the metadata.

The locale values must follow the <language>-<region> format, where:

You can provide either just the language code, or both the language and country codes. Here are some examples:

  • en represents English in any region

  • en-US represents English as used in the United States

  • en-GB represents English as used in the United Kingdom

If not specified, the locale field in all @lens-protocol/metadata helpers will default to en.

import { textOnly } from '@lens-protocol/metadata';
const metadata = textOnly({  content: `Ciao mondo!`,  locale: 'it',});

While this example uses the textOnly helper, the same principle applies to all other metadata types.

Marketplace Metadata

To ensure maximum interoperability with NFT marketplaces, Lens Publications conform to the ERC-721 metadata standard. Additionally, they incorporate extra properties from the OpenSea Metadata Standard.

If you're creating a collectable publication, or using any Open Action that allows to mint NFTs from your publications, you can specify typical NFT properties via the marketplace property of any @lens-protocol/metadata helpers.

import {  image,  MediaImageMimeType,  MarketplaceMetadataAttributeDisplayType,} from "@lens-protocol/metadata";
const metadata = image({  title: "Bob the starfish",  image: {    item: "ipfs://QmXZzv1Q2",    type: MediaImageMimeType.PNG,    altTag: "Bob mugshot",  },  marketplace: {    name: "Bob",
    description: "Bob is a starfish",
    external_url: "",
    image: "ipfs://QmXZzv1Q2",
    animation_url: "",
    attributes: [      {        display_type: MarketplaceMetadataAttributeDisplayType.STRING,        trait_type: "Base",        value: "Starfish",      },      {        display_type: MarketplaceMetadataAttributeDisplayType.NUMBER,        trait_type: "Level",        value: 5,      },      {        display_type: MarketplaceMetadataAttributeDisplayType.DATE,        trait_type: "dob",        value: "2022-01-01",      },    ],  },});

These properties will be validated and automatically included in the resulting Publication Metadata object.

Alternatively, if you're manually creating a Publication Metadata object, you can specify these properties directly in the JSON, following the corresponding publication type's JSON schema specification.

{  "$schema": "",  "name": "Bob",  "description": "Bob is a starfish",  "external_url": "",  "image": "ipfs://QmXZzv1Q2",  "animation_url": "",  "attributes": [    {      "display_type": "string",      "trait_type": "Base",      "value": "Starfish"    },    {      "display_type": "number",      "trait_type": "Level",      "value": 5    },    {      "display_type": "date",      "trait_type": "dob",      "value": "2022-01-01"    }  ],  "lens": {    "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb",    "locale": "en",    "mainContentFocus": "IMAGE",    "title": "Bob the starfish",    "image": {      "item": "ipfs://QmXZzv1Q2",      "type": "image/png",      "altTag": "Bob mugshot"    }  }}

That's it—you now everything you need to know to create on-chain Lens Post.