# Kagi Small Web

> Read the small web from a workflow: the personal blogs, one-person YouTube channels and webcomics that Kagi curates as the non-commercial web. List what has just been published, narrowed to a subject, a blog, an author or a stretch of time, with every post carrying a plain-text summary ready to post or mail. Pick one post at random for a daily reading prompt. Search the 41,000 sites in the index and take their feed addresses straight into a reader, or check whether one particular blog is in there. The feed holds the most recent posts only, which is roughly the last day and a half, so every result says how far back it reached. Every action reads: nothing here changes anything. No account or API key required.

App · Feeds & Read-Later · 4 actions. Web page: https://docs.lodol.com/docs/integrations/kagi-small-web

## Actions

### Check whether a site is indexed

Answer yes or no for one site: is this blog part of Kagi Small Web? Give it an address or a domain in any spelling, with or without https and www, and it comes back with the answer and, when the site is there, its feed address. A site that is not there comes back with the indexed domains closest to it, because a no is usually the wrong suffix or a typo rather than a real absence.

**Inputs**

- Site (`site`, text, required): The site to look up, as an address or a domain. 'danluu.com', 'www.danluu.com' and 'https://danluu.com/atom.xml' all ask about the same blog.

**Outputs**

- `feed_count` (number): How many feeds the index lists for it. Nearly always one; a site with two feeds, or a host that many channels sit on, has more.
- `feed_url` (text): Its feed, ready to hand to a reader.
- `is_indexed` (true/false): Whether the site is in the small web index.
- `name` (text): What the index calls it. Empty when it is not indexed.
- `similar_sites` (list): Indexed domains close to the one asked about, offered when it is not there. A missed check is usually the wrong suffix or a typo.
- `site` (text): The site asked about, reduced to a plain domain. Whatever was typed, this is what was looked up.
- `total_count` (number): How many sites the whole index holds.
- `url` (text): Its home page as the index lists it.

**Example output**

```json
{
  "feed_count": 1,
  "feed_url": "https://danluu.com/atom.xml",
  "is_indexed": true,
  "name": "danluu.com",
  "similar_sites": [],
  "site": "danluu.com",
  "total_count": 41494,
  "url": "https://danluu.com"
}
```

### Find sites in the index

Search the roughly 41,000 sites Kagi has accepted into the small web: personal blogs, one-person YouTube channels and webcomics. Every result carries the site's home page and its feed address, so a workflow can hand them straight to a reader, a database or a blogroll. Searching matches anywhere in the name or either address, so 'comic' finds bigblogcomics.com and 'wordpress.com' finds every blog hosted there. A search that matches more than you asked for says how many it found, so it is clear when it is worth narrowing. To take the whole index rather than a search of it, the result also carries its address as a single OPML file that any reader imports in one go.

**Inputs**

- Search (`contains`, text): Only sites whose name or address contains this. Every word has to appear, in any order, anywhere in the text, so 'comic' finds bigblogcomics.com and 'wp' finds a wordpress.com blog. Case and accents are ignored. Leave it empty for the whole index.
- How many sites (`limit`, number): How many sites to return. Fifty by default. The result always says how many matched, so a search worth narrowing is obvious from the step. From 1 to 500. Default: `50`.

**Outputs**

- `contains` (text): The text searched for, if any.
- `count` (number): How many sites came back.
- `index_url` (text): The address of the whole index as an OPML file, which any feed reader can import in one go.
- `matched_count` (number): How many sites matched before the limit was applied. Higher than the count means the search is worth narrowing.
- `sites` (list): The sites, in alphabetical order.
- `total_count` (number): How many sites the whole index holds.

**Example output**

```json
{
  "contains": "",
  "count": 3,
  "index_url": "https://kagi.com/smallweb/opml",
  "matched_count": 3,
  "sites": [
    {
      "domain": "danluu.com",
      "feed_url": "https://danluu.com/atom.xml",
      "name": "danluu.com",
      "url": "https://danluu.com"
    },
    {
      "domain": "simonwillison.net",
      "feed_url": "https://simonwillison.net/atom/everything/",
      "name": "simonwillison.net",
      "url": "https://simonwillison.net"
    },
    {
      "domain": "xkcd.com",
      "feed_url": "https://xkcd.com/atom.xml",
      "name": "xkcd.com",
      "url": "https://xkcd.com"
    }
  ],
  "total_count": 41494
}
```

### Get a random post

Pick one post at random from everything the small web has published recently, or from the posts about one subject. This is the step behind a daily reading prompt, a 'something to read' message in a channel, or a surprise link on a home page. Set a repeatable pick when the same post has to come back every run, for a screenshot or a test.

**Inputs**

- About (`contains`, text): Only posts whose title or summary mentions this. Every word has to appear somewhere, in any order, so 'async rust' finds a post called 'Rust, and async at last'. Words match from the start of a word, so 'program' finds programming while 'rust' does not find trust. Case and accents are ignored. Leave it empty for everything published recently.
- Repeatable pick (`seed`, text): Set this to any text to make the pick repeatable: the same text picks the same post for as long as that post is still in the feed. Leave it empty for a different post every run, which is what a daily reading prompt wants.

**Outputs**

- `author` (text): Who wrote it, when the blog says.
- `chosen_from` (number): How many posts it was picked from.
- `contains` (text): The words it was narrowed to, if any.
- `found` (true/false): Whether there was a post to pick.
- `published_at` (text): When it was published, as an ISO 8601 time in UTC.
- `seed` (text): The repeatable pick used, if any.
- `site` (text): The blog it was published on, as a plain domain.
- `summary` (text): The post's own summary as plain readable text.
- `title` (text): The post's title.
- `url` (text): The address of the post itself.

**Example output**

```json
{
  "author": "Dan Luu",
  "chosen_from": 1000,
  "contains": "",
  "found": true,
  "published_at": "2026-09-15T02:30:00+00:00",
  "seed": "",
  "site": "danluu.com",
  "summary": "Hosting bills are mostly a story about defaults. Here is what ten years of a plain static site actually cost, and what changed.",
  "title": "The cheapest way to keep a blog online for a decade",
  "url": "https://danluu.com/cheap-blog/"
}
```

### List recent posts

Read what the small web has just published: personal blogs, one-person YouTube channels and webcomics, with no SEO pages and no press releases. Narrow to a subject, a single blog, an author or a stretch of time, and each post comes back with its title, link, blog, author, time and a plain-text summary ready to post or mail. Worth knowing before you build: the feed holds the most recent posts only, which is roughly the last day and a half, so this is a step to run daily rather than one to ask about last month. Every result says exactly how far back it reached.

**Inputs**

- About (`contains`, text): Only posts whose title or summary mentions this. Every word has to appear somewhere, in any order, so 'async rust' finds a post called 'Rust, and async at last'. Words match from the start of a word, so 'program' finds programming while 'rust' does not find trust. Case and accents are ignored. Leave it empty for everything published recently.
- From this site (`site`, text): Only posts from one blog, given as its address or its domain: 'danluu.com' and 'https://danluu.com/' both work. The list offers the sites that have published in the last day or so, and any other can be typed in. Naming a host that blogs sit on, such as 'tumblr.com', includes every blog underneath it. Leave it empty for the whole small web.
- By this author (`author`, text): Only posts credited to a writer whose name contains these words. Roughly four posts in ten name nobody at all, because the blog does not publish an author, and those can never match. Leave it empty for everyone.
- Published after (`published_since`, date and time): Only posts published at or after this moment, in UTC. The feed reaches back about a day and a half, so an earlier time returns everything there is and the result says the feed did not go back that far. Leave it empty for the whole feed.
- Published before (`published_until`, date and time): Only posts published at or before this moment, in UTC. Blogs stamp their own dates, so a handful are always dated a few hours ahead of now; leave this empty to keep them.
- How many posts (`limit`, number): How many posts to return. Twenty five by default. The filters run across the whole feed first, so a small number here is the best matches rather than a small search. From 1 to 1000. Default: `25`.
- Order (`order`, text): Newest first by default. Oldest first starts at the far end of the feed, which is where a step that has already handled today's posts picks up. One of: `newest` (Newest first), `oldest` (Oldest first).

**Outputs**

- `count` (number): How many posts came back.
- `covers_requested_period` (true/false): False when 'Published after' asked for posts older than the feed reaches. The result is everything the feed has, not everything that was published.
- `filtered` (true/false): Whether any filter was applied at all.
- `matched_count` (number): How many posts matched before the limit was applied. Higher than the count means there is more to see behind it.
- `posts` (list): The posts, in the order asked for.
- `requested_since` (text): The 'Published after' time asked for, if any.
- `requested_until` (text): The 'Published before' time asked for, if any.
- `site_count` (number): How many different blogs the returned posts come from.
- `window_end` (text): The newest post in the feed, as an ISO 8601 time.
- `window_post_count` (number): How many posts were read to answer this.
- `window_start` (text): The oldest post the feed still reaches, as an ISO 8601 time. Nothing published before this can be listed.

**Example output**

```json
{
  "count": 3,
  "covers_requested_period": true,
  "filtered": false,
  "matched_count": 3,
  "posts": [
    {
      "author": "Dan Luu",
      "published_at": "2026-09-15T02:30:00+00:00",
      "site": "danluu.com",
      "summary": "Hosting bills are mostly a story about defaults. Here is what ten years of a plain static site actually cost, and what changed.",
      "title": "The cheapest way to keep a blog online for a decade",
      "url": "https://danluu.com/cheap-blog/"
    },
    {
      "author": "Simon Willison",
      "published_at": "2026-09-14T18:05:00+00:00",
      "site": "simonwillison.net",
      "summary": "A forum with eighty people on it needs almost none of the software that a forum with eighty thousand does.",
      "title": "Notes on running a small forum in 2026",
      "url": "https://simonwillison.net/2026/Sep/14/small-forum/"
    },
    {
      "author": "",
      "published_at": "2026-09-14T07:12:00+00:00",
      "site": "bakingatnight.example",
      "summary": "",
      "title": "Sourdough, week eleven",
      "url": "https://bakingatnight.example/posts/week-eleven/"
    }
  ],
  "requested_since": "",
  "requested_until": "",
  "site_count": 3,
  "window_end": "2026-09-15T02:30:00+00:00",
  "window_post_count": 1000,
  "window_start": "2026-09-13T11:40:56+00:00"
}
```
