---
name: slug-rules
description: |
  The exact rules a Anchorify slug must satisfy. Slugs are the
  human-chosen tail of a share URL (e.g. the `q1-report` in
  `https://anchorify.io/alice/q1-report`). The server enforces
  this regex on every write; calls that violate it get a 400 with a
  `reason` field describing the rule.
---

# Slug rules

Slugs are the part of a share URL you choose — the `q1-report` in
`https://anchorify.io/alice/q1-report`. They are validated on every
publish.

## The regex

```
^[a-z0-9](?:[a-z0-9-]{0,58}[a-z0-9])?$
```

In plain language:

- 1 to 60 characters total
- Lowercase letters (`a`–`z`), digits (`0`–`9`), and hyphens (`-`) only
- Cannot start or end with a hyphen
- Single-character slugs are allowed (e.g. `a`, `7`)
- No underscores, no uppercase, no dots, no slashes, no spaces, no
  punctuation, no unicode

## Valid examples

- `q1-report`
- `notes-2026`
- `a`
- `cardinal-heating-audit`
- `slug-with-many-hyphens-here`
- `geology-week3`

## Invalid examples

| Slug             | Why it fails                                |
| ---------------- | ------------------------------------------- |
| `Bad_Slug!`      | uppercase + underscore + punctuation        |
| `-leading`       | starts with a hyphen                        |
| `trailing-`      | ends with a hyphen                          |
| `UPPERCASE`      | uppercase letters                           |
| `has space`      | contains a space                            |
| `under_score`    | contains an underscore                      |
| `dot.in.slug`    | contains a dot                              |
| `slash/in/slug`  | contains a slash                            |
| (empty string)   | must be at least 1 character                |
| 61+ chars        | exceeds 60-char limit                       |

## Random IDs are slugs too

When you publish without choosing a slug, the server assigns a random
8-character base36 id (e.g. `k3p9x2af`). Those random ids satisfy the
same regex, so the `id` column is polymorphic — every value, random or
chosen, is a valid slug.

## Error response

A POST with an invalid slug returns:

```json
{
  "error": "invalid slug",
  "reason": "must be lowercase alphanumeric + hyphens, 1-60 chars, no leading/trailing dash"
}
```

with HTTP status `400`. If the slug is valid but already taken, the
status is `409` with `{"error":"slug taken","slug":"<the-slug>"}`
instead — pick a different one and retry.
