Pinata
Published in

Pinata

How To Build An NFT Token-Gated Blog with Ghost

Incorporating NFTs into your private blogging community

Setting Up Your Dev Environment

mkdir token-gated-blog && cd token-gated-blog
npx create-next-app blog
mkdir ghost-app
npm install ghost-cli@latest -g
cd ghost-blog
ghost install local

Connecting Ghost to Pinata

cd content
mkdir adapters && cd adapters
mkdir storage && cd storage
git clone https://github.com/PinataCloud/pinata-ghost-storage
cd pinata-ghost-storage && npm i
"storage": {
"active": "pinata-ghost-storage",
"pinata-ghost-storage": {
"gatewayUrl": "https://yourgatewayurl.com",
"pinataKey": "YOUR PINATA API KEY",
"pinataSecret": "YOUR PINATA API SECRET"
}
},
ghost restart
cd ../blog
npm run dev
SUBMARINE_KEY=YOUR SUBMARINE API KEY
DEDICATED_GATEWAY_URL=https://yourgateway.com
CONTRACT_ADDRESS=NFT CONTRACT ADDRESS
BLOCKCHAIN=Ethereum
NETWORK=Mainnet
npm i pinata-submarine
import { Submarine } from "pinata-submarine";const submarine = new Submarine(process.env.SUBMARINE_KEY, process.env.DEDICATED_GATEWAY_URL);export default async function handler(req, res) {
if(req.method === "POST") {
try {
const { post } = req.body;
const { current } = post;
const metadata = {
title: current.title,
feature_image: current.feature_image,
featured: current.featured,
excerpt: current.custom_excerpt,
tags: JSON.stringify(current.tags),
published: "true"
}
await submarine.uploadJson(current, current.id, 1, metadata);

res.send("Success");
} catch (error) {
console.log(error);
res.status(500).json(error);
}
}
}
import { Submarine } from "pinata-submarine";
const submarine = new Submarine(process.env.SUBMARINE_KEY, process.env.DEDICATED_GATEWAY_URL);
export default async function handler(req, res) {
if(req.method === "POST") {
try {
const { post } = req.body;
const { current } = post;
const options = {
name: current.id,
metadata: JSON.stringify({
published: "true"
})
}
const content = await submarine.getSubmarinedContent(options); if(content.length > 0) {
const { id } = content[0];
await submarine.deleteContent(id);
const metadata = {
title: current.title,
feature_image: current.feature_image,
featured: current.featured,
excerpt: current.custom_excerpt,
tags: JSON.stringify(current.tags),
published: "true"
}
await submarine.uploadJson(current, current.id, 1, metadata);
} else {
throw "No content found"
}
res.send("Success");
} catch (error) {
console.log(error);
res.status(500).json(error);
}
}
}
import { Submarine } from "pinata-submarine";
const submarine = new Submarine(process.env.SUBMARINE_KEY, process.env.DEDICATED_GATEWAY_URL);
export default async function handler(req, res) {
if(req.method === "POST") {
try {
const { post } = req.body;
const { previous } = post;
const options = {
name: previous.id,
metadata: JSON.stringify({
published: "true"
})
}
const content = await submarine.getSubmarinedContent(options); if(content.length > 0) {
const { id } = content[0];
await submarine.deleteContent(id);
} else {
throw "Content not found"
}
res.send("Success");
} catch (error) {
console.log(error);
res.status(500).json(error);
}
}
}
import { Submarine } from "pinata-submarine";
const submarine = new Submarine(process.env.SUBMARINE_KEY, process.env.DEDICATED_GATEWAY_URL);

export default async function handler(req, res) {
if(req.method === "GET") {
try {
const { offset, limit } = req.query;
const options = {
metadata: JSON.stringify({
ghostBlog: {
value: "true",
op: "eq"
}
}),
offset,
limit
}
const content = await submarine.getSubmarinedContent(options); if(content.length > 0) {
return res.json(content);
} else {
return res.json([]);
}
} catch (error) {
console.log(error);
res.status(500).json(error);
}
}
}

Build The Token-Gating App

import Link from 'next/link';
import React from 'react';
const Posts = ({ post }) => {
const { metadata, createdAt, cid } = post;
const { title, excerpt } = metadata;
return (
<Link href={`/${cid}`}>
<div className="card cursor">
<h1>{title}</h1>
<p>{createdAt}</p>
<p>{excerpt}</p>
<h3>Read More</h3>
</div>
</Link>
)
}
export default Posts;
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import Authenticate from '../components/Authenticate';
import Post from '../components/Post';
const SinglePost = () => {
const [cid, setCID] = useState("");
const [authenticated, setAuthenticated] = useState(false);
const [postContent, setPostContent] = useState(null);
const router = useRouter(); const { query } = router; useEffect(() => {
if(query && query.cid) {
setCID(query.cid);
}
}, [query]);
if(authenticated) {
return <Post cid={cid} postContent={postContent} />
}
return (
<div>
<Authenticate cid={cid} setAuthenticated={setAuthenticated} setPostContent={setPostContent} />
</div>
)
}
export default SinglePost;
import { Submarine } from "pinata-submarine";
const submarine = new Submarine(process.env.SUBMARINE_KEY, process.env.DEDICATED_GATEWAY_URL);
export default async function handler(req, res) {
if (req.method === "GET") {
try {
const message = await submarine.getEVMMessageToSign(process.env.BLOCKCHAIN, process.env.CONTRACT_ADDRESS);
res.json(message);
} catch (error) {
console.log(error);
res.status(500).json(error);
}
} else {
try {
const { signature, messageId, address } = JSON.parse(req.body);
const ownsNFT = await submarine.verifyEVMNFT(
signature,
address,
messageId,
process.env.BLOCKCHAIN,
process.env.CONTRACT_ADDRESS,
process.env.NETWORK
);
if (ownsNFT) {
const { cid } = req.query;
const postData = await submarine.getSubmarinedContentByCid(cid);
const { id, metadata } = postData.items[0]; const link = await submarine.generateAccessLink(1000, id, cid); return res.json({
link,
id,
metadata,
});
} else {
return res.status(401).send("NFT not owned or invalid signature");
}
} catch (error) {
console.log(error);
const { response: fetchResponse } = error;
return res.status(fetchResponse?.status || 500).json(error.data);
}
}
}
import React from "react";
import Link from "next/link";
const Post = ({ postContent }) => {
return (
<div>
<Link href="/">Back</Link>
<div>
<h1>{postContent.title}</h1>
</div>
<div>
<div dangerouslySetInnerHTML={{ __html: postContent.html }} /
</div>
</div>
);
};
export default Post;

Wrapping Up

--

--

The cloud wasn’t built for this. Pinata was. Managing your NFT media just got easier.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store