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();
})();