IPFS Directory Listing: Best Practices Without Server-Side Autoindex?

Hi everyone,

I’m running into a limitation with IPFS regarding directory listing.

Unlike traditional web servers such as Nginx or Apache (with autoindex on), IPFS does not provide a built-in way to list directory contents on the server side.

In my case, the “Media” section of my index.html is designed to:

  1. First attempt to load a media.json file (which would explicitly define the content),

  2. If media.json is not present, fall back to parsing the HTML response of the directory in order to infer the file listing.

However, this fallback approach feels unreliable and somewhat hacky, since IPFS gateways are not guaranteed to return a consistent or parseable directory HTML format.

So my questions are:

  • Is there a recommended or standard way to handle directory listings in IPFS-backed websites?

  • Are there best practices for dynamically discovering files in a directory without relying on a pre-generated JSON file?

  • Do any gateways or tools provide a stable API for listing directory contents?

I’d appreciate any guidance or suggestions from people who have dealt with similar use cases.

Thanks!

:white_check_mark:
my solution :

#!/usr/bin/env bash

set -e

OUTPUT="media.json"

echo "[" > $OUTPUT
FIRST=true

add_entry () {
  local FILEPATH="$1"
  local TYPE="$2"

  FILENAME=$(basename "$FILEPATH")
  TITLE="${FILENAME%.*}"

  if [ "$FIRST" = true ]; then
    FIRST=false
  else
    echo "," >> $OUTPUT
  fi

  cat <<EOF >> $OUTPUT
{
  "url": "$FILEPATH",
  "type": "$TYPE",
  "title": "$TITLE"
}
EOF
}

# Videos
for file in video/*; do
  [ -f "$file" ] || continue
  case "$file" in
    *.mp4|*.webm|*.ogg|*.mov)
      add_entry "$file" "video"
      ;;
  esac
done

# PDFs
for file in pdf/*; do
  [ -f "$file" ] || continue
  case "$file" in
    *.pdf)
      add_entry "$file" "pdf"
      ;;
  esac
done

echo "]" >> $OUTPUT

echo "✅ media.json generated"

this script scan for mp4 and pdf to build media.json


cat media.json 
[
{
  "url": "video/Astroport.mp4",
  "type": "video",
  "title": "Web2 / WEB3 __ Deux Voix..."
}
,
{
  "url": "video/Heartbleed___Deux_Voies.mp4",
  "type": "video",
  "title": "Heartbleed___Deux_Voies"
}
,
{
  "url": "pdf/Astroport_Digital_Sovereignty.pdf",
  "type": "pdf",
  "title": "Astroport"
}
,
{
  "url": "pdf/Astroport.ONE_Sovereign_P2P_Swarm.pdf",
  "type": "pdf",
  "title": "SOVEREIGN P2P SWARM"
}
,
{
  "url": "pdf/L_Architecture_Souveraine.pdf",
  "type": "pdf",
  "title": "L_Architecture_Souveraine"
}
,
{
  "url": "pdf/Sovereign_Internet_Manifesto.pdf",
  "type": "pdf",
  "title": "INTERNET MANIFESTO"
}
,
{
  "url": "pdf/UPlanet_Sovereignty_Blueprint.pdf",
  "type": "pdf",
  "title": "UPlanet_Sovereignty_Blueprint"
}
]

that is then found and used by index.html

(async()=>{
  // 1. Essai manifest.json
  try{
    const res=await fetch('media.json');
    if(res.ok){
      const data=await res.json();
      if(Array.isArray(data)&&data.length){
        data.forEach(m=>mediaList.push(m));
        return renderMGrid();
      }
    }
  }catch(e){}
  // 2. Fallback : scan des répertoires (nécessite autoindex nginx/apache)
  await Promise.all([
    scanDir('pdf/','pdf',/\.pdf$/i),
    scanDir('video/','video',/\.(mp4|webm|ogg|mov)$/i),
  ]);
  renderMGrid();
})();

Your approach is fine. Static index.html listings at publish time, or a sibling JSON rendered client-side, are both reasonable.

Yes. The dir listing data is already in IPFS DAG. A UnixFS directory is content-addressed protobuf manifest, and every gateway returns those bytes via ?format=raw as application/vnd.ipld.raw. So you could have single index that works without media.json, and can render the listing from the directory’s own block(s) at view time.

Demo: https://bafybeicj4j2a5mv7pzvp6itfq3vfgkl53ogjr3asusp3cj3j4n5t5iy3va.ipfs.dweb.link/

The above is a directory with a few files, a sub-directory, and an index.html that fetches the directory’s own block from the same gateway and renders entries client-side. Works on any path or subdomain gateway. index.html is ~80 lines of inline JS + imported remote ESM libs.

To use it on your own content, drop a similar index.html into any directory you publish (download). Note: the demo loads JS from esm.sh for brevity. For production, bundle the dependency into index.html so the page is fully content-addressed with the data.

Note that the demo above uses very naive “blockstore” stub which just fetches blocks via ?format=raw. For advanced path resolution, content-type negotiation, caching, ranges, or full client-side gateway behavior, use @helia/verified-fetch or see even more advanced pre-built UI in service-worker-gateway.