Litter Layer

An indie search engine largely built by visitors like you.

Join the Federation

TL;DRDownload starter pack (descriptor, sites.json, search.php, and .htaccess). Upload to your web root, edit your domain and pages, then register below.

If you want to contribute to the Litter Layer Federation, you can register your site as a federated node. Approved nodes share search results when visitors turn on Federated search on the results page.

How federated search works

When a visitor has the federated toggle on, Litter Layer queries approved partner nodes once per search and merges their results with this hub's index. Network results can appear across multiple result pages when the visitor clicks “Load more results,” until that search's federated pool runs out. After that, later pages come from this hub's central index only.

This keeps the network efficient: nodes are not re-queried on every page. The hub caches the merged pool briefly (about 10 minutes) so load-more stays fast on shared hosting. If a visitor waits too long before loading more, federated results may expire and only hub results will appear.

Each node may return up to 10 results per search. Those results may spread across several pages — not only the first page. No changes are required on node operators' servers for multi-page pagination.

Who is this guide for? We want anyone to contribute — including people on cheap shared hosting with cPanel, Plesk, or similar. The steps below assume your website files live in a folder called public_html (sometimes named www or httpdocs). You upload files with File Manager or FTP.

If your setup is different — VPS, static host, your own app stack — you probably already know how to adapt these files. The rules are the same: publish a descriptor, answer search requests at /search, then register here.

What you will create

You do not need to install all of Litter Layer on your host. You need four small files:

When someone searches, Litter Layer sends your site a query; your script returns matching rows from sites.json. You can add or edit entries anytime.

File tree (inside public_html)

When you are done, your hosting account should look like this:

In cPanel: open File Manager → public_html. Create the .well-known folder if it does not exist (some hosts hide dot-folders — enable “Show Hidden Files” in Settings).

Step 1 — Create .well-known/litterlayer.json

This file describes your node. Replace your-domain.example with your real domain (no www is fine if that is your canonical host). Use https:// in base_url.

Live example from this hub: https://litterlayer.com/.well-known/litterlayer.json

{
  "node_id": "your-domain.example",
  "base_url": "https://your-domain.example",
  "categories": ["writing", "tech"],
  "capabilities": {
    "search": true
  },
  "performance": {
    "timeout_ms": 1200
  }
}

Test: open https://your-domain.example/.well-known/litterlayer.json in a browser. You should see the JSON text, not a 404.

Step 2 — Create sites.json

List every page you want searchable. Copy the sample below into public_html/sites.json and edit the URLs, titles, and descriptions. Add more objects inside the [ ] array as you publish new pages.

[
  {
    "url": "https://your-domain.example/",
    "title": "Home page",
    "description": "Welcome to my site — essays, links, and projects.",
    "score": 1.0
  },
  {
    "url": "https://your-domain.example/blog/hello-world/",
    "title": "Hello world",
    "description": "My first post about joining the small web.",
    "score": 0.9
  },
  {
    "url": "https://your-domain.example/links/",
    "title": "Links",
    "description": "Friends, tools, and places I like on the internet.",
    "score": 0.8
  }
]

Tip: keep URLs on the same domain as your descriptor. Use the full https:// address for each entry.

Step 3 — Create search.php

Save this file as public_html/search.php. It reads sites.json, matches the visitor’s keywords, and returns JSON. No database required.

<?php
header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Only POST allowed']);
    exit;
}

$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
$query = is_array($data) ? trim((string)($data['query'] ?? '')) : '';
$limit = is_array($data) ? max(1, min(50, (int)($data['limit'] ?? 10))) : 10;

$path = __DIR__ . '/sites.json';
if (!is_file($path)) {
    echo json_encode(['results' => []]);
    exit;
}

$sites = json_decode(file_get_contents($path), true);
if (!is_array($sites)) {
    echo json_encode(['results' => []]);
    exit;
}

$terms = preg_split('/\s+/u', mb_strtolower($query), -1, PREG_SPLIT_NO_EMPTY);
$results = [];

foreach ($sites as $row) {
    if (!is_array($row)) {
        continue;
    }
    $haystack = mb_strtolower(
        ($row['title'] ?? '') . ' ' . ($row['description'] ?? '') . ' ' . ($row['url'] ?? '')
    );
    if ($terms) {
        $match = true;
        foreach ($terms as $term) {
            if ($term !== '' && mb_strpos($haystack, $term) === false) {
                $match = false;
                break;
            }
        }
        if (!$match) {
            continue;
        }
    }
    $results[] = [
        'url' => (string)($row['url'] ?? ''),
        'title' => (string)($row['title'] ?? ''),
        'description' => (string)($row['description'] ?? ''),
        'score' => (float)($row['score'] ?? 0.5),
    ];
}

usort($results, function ($a, $b) {
    return ($b['score'] <=> $a['score']);
});

echo json_encode(
    ['results' => array_slice($results, 0, $limit)],
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);

Step 4 — Update .htaccess

Litter Layer calls https://your-domain.example/search (not search.php). On most shared Apache hosts, add this rewrite rule so /search runs your script:

# Add these lines to public_html/.htaccess
# (If you already have a .htaccess file, paste inside it — do not delete existing rules.)

RewriteEngine On
RewriteRule ^search$ search.php [L,QSA]

If your host uses Nginx or you cannot edit what you need to, ask your web hosting support how to route /search to search.php. Also, these instructions are mainly for non-developers. If you have web building experience you may set this up however works best for your environment.

Test the search endpoint: after uploading, you can use your host’s Terminal (if available) or any online HTTP client. A successful response looks like {"results":[...]}. From a terminal:

curl -sS -X POST https://your-domain.example/search \
  -H "Content-Type: application/json" \
  -d '{"query":"hello","limit":5}'

Step 5 — Register your node

Once the descriptor URL works and search returns JSON, submit your descriptor below. We fetch it to verify HTTPS and settings, then queue your node for approval.

Here's what your descriptor URL should look like:

https://yoursite.com/.well-known/litterlayer.json

Step 6 — Heartbeats (optional)

After approval, you can occasionally ping this hub so we know your node is online. On shared hosting, add a once-daily cron job in cPanel if you like — it is optional.

curl -sS -X POST https://litterlayer.com/api/federation/heartbeat.php -H "Content-Type: application/json" -d '{"node_id":"your-domain.example"}'

Replace your-domain.example with the same node_id from your descriptor.

Federated search result with a litterlayer.com node badge
This is what a federated search result looks like in the results list. Each one includes a badge with the node's URL — in this example, litterlayer.com.

Need help?

See the Help page for how federated search works for visitors.

Instructions for other environments can be found at our Github wiki(opens in new tab).

For any questions, please ask in our Github forum(opens in new tab).