Hosting on IPFS without running a server
Pin your site to IPFS
I looked at the available options, and only one makes it easy to pin a whole folder/site: Pinata.
Make a free key and save it to PINATA_JWT. Then start writing a GitHub workflow
like this:
name: Deploy
on:
push:
branches: [main]
schedule:
- cron: '0 0,12 * * *'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
# You'll add environment here later
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with: { node-version: latest }
# Fill this in with your build setup
# - uses: pnpm/action-setup@v4
# with: { version: latest }
# - run: pnpm install
# - run: pnpm build
- name: Upload to IPFS
env:
PINATA_JWT: ${{ secrets.PINATA_JWT }}
run: |
node --input-type=module -e "
import { readFileSync, readdirSync, writeFileSync } from 'fs';
import { join, relative } from 'path';
const files = [];
function walk(dir) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) walk(p); else files.push(p);
}
}
walk('dist');
const form = new FormData();
const folder = 'deploy-' + Date.now() + '/';
for (const f of files) {
form.append('file', new Blob([readFileSync(f)]), folder + relative('dist', f));
}
form.append('pinataMetadata', JSON.stringify({ name: 'deploy-' + Date.now() }));
const res = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PINATA_JWT },
body: form,
});
const json = await res.json();
if (!json.IpfsHash) { console.error(JSON.stringify(json)); process.exit(1); }
writeFileSync('/tmp/cid', json.IpfsHash);
"
# You'll add IPNS here later Give it a stable URL
Changing content changes its content ID (CID), but you probably don't want its URL to also
change. The simplest fix is to use IPNS. Set IPNS_KEY to 32 random hex bytes and add this step:
- name: Point IPNS
id: point
env:
IPNS_KEY: ${{ secrets.IPNS_KEY }}
run: |
echo "url=$(npx point-ipns $(cat /tmp/cid))" >> $GITHUB_OUTPUT And to show where it was deployed, add this right before steps:
environment:
name: ipfs
url: ${{ steps.point.outputs.url }} Other options involve changing your README to always include the latest CID, or using DNSLink (changing your DNS to always point to the latest CID).
Access it
If you didn't notice, your content is available at https://ipfs.io/ipns/[key hash or domain] if you used IPNS or DNSLink, or https://ipfs.io/ipfs/[CID] if you didn't.
But there are more gateways than ipfs.io. This page lists 20, and at least one likely works for you.
