## File: README.md

### See how [_the exact same_ Medium.com clone](https://demo.realworld.show) is built using different [frontends](https://codebase.show/projects/realworld?category=frontend) and [backends](https://codebase.show/projects/realworld?category=backend)

You can combine any frontend with any backend, because **they all adhere to the same [API spec](specs/api/)**

While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge required to actually build _real_ applications with it — nor the real-world constraints a minimal demo never has to face.

**RealWorld** solves this problem by providing the same demo app for each framework, at a sweet spot between simplicity and breadth.

Join us on [GitHub Discussions!](https://github.com/realworld-apps/realworld/discussions) 🎉

# Implementations

Over 100 implementations have been created using various languages, libraries, and frameworks.

Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld).

## Spec-compliant backends

These backends pass the full [API spec test suite](https://docs.realworld.show/specifications/backend/introduction/):

- [**Nitro + Prisma + Zod**](https://github.com/realworld-apps/nitro-prisma-zod-realworld-example-app) — TypeScript
- [**Django Ninja**](https://github.com/c4ffein/realworld-django-ninja) — Python

# Create a new implementation

[**Create a new implementation >>>**](https://docs.realworld.show/implementation-creation/introduction/)

Or you can [view upcoming implementations (WIPs)](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations).

# Learn more

- [Documentation introduction](https://docs.realworld.show/introduction/)
- Every tutorial is built against the same [API spec](specs/api/) to ensure modularity of every frontend & backend
- A shared [CSS theme](assets/theme/styles.css) is provided to build frontend implementations with identical UI/UX
- A shared [E2E test suite](specs/e2e/) is available to validate frontend implementations
- There is a hosted version of the backend API available for public usage at [api.realworld.show](https://api.realworld.show), no API keys required — demo accounts are provided, and real accounts can't see each other
- There is an angular frontend plugged to this backend available at [demo.realworld.show](https://demo.realworld.show)
- Interested in creating a new RealWorld stack? View our [starter guide & spec](https://docs.realworld.show/implementation-creation/introduction/)

# Logo Attribution

See [LICENSES_LOGOS.md](docs/non-included/LICENSES_LOGOS.md) for framework logo licensing and attribution details.

# Active Maintainers

- **[c4ffein](https://github.com/c4ffein) - Maintainer** - maintains the spec, the test suites and the [demo website](https://demo.realworld.show)
- **[Manuel Vila](https://github.com/mvila) - Maintainer** - creator of the [Layr framework](https://layrjs.com) and the [CodebaseShow website](https://codebase.show/)

---

## File: CLAUDE.md

# CLAUDE.md

## What this repo is

This is the **RealWorld spec & docs hub** — *not* an implementation. It defines the
contract that every RealWorld frontend and backend adheres to, and hosts the test
suites used to validate them. Nothing here is a runnable app.

- `specs/api/` — the API contract: `openapi.yml` plus the **Hurl** and **Bruno** test
  suites that a backend must pass.
- `specs/e2e/` — a shared **Playwright** suite that validates a frontend, plus
  `SELECTORS.md` (the selector contract) and `playwright.base.ts`.
- `docs/` — the Astro/Starlight site published at <https://docs.realworld.show/>.
- `Makefile` — entry points (run `make help`).
- `CONTRIBUTING.md` — the human contribution process.

## If you are here as a submodule / vendored dependency

An implementation embeds this repo to test itself against the spec (the E2E suite is
literally consumed as `./e2e`). If that's why you're reading this:

- **Fix the implementation, never edit the spec or tests to make them pass.** Files
  under `specs/` are the source of truth a failing implementation must conform to.
- Edit files here only when the task is explicitly about *changing the RealWorld spec
  itself*.

## Conventions

- Use **`bun`**, not `npm`/`node`, for everything in this repo.
- **Hurl is the source of truth** for the API suite; the Bruno collection is generated
  from it. Don't hand-edit `specs/api/bruno/` — change the Hurl files and regenerate.
- Docs URLs are written with a trailing slash.

## Running the test suites

API suite (point `HOST` at a running backend, run from `specs/api/`):

```
HOST=http://localhost:3000/api ./run-api-tests-hurl.sh    # source of truth
HOST=http://localhost:3000/api ./run-api-tests-bruno.sh    # generated mirror
```

Regenerate / verify the Bruno collection:

```
make bruno-generate    # rebuild specs/api/bruno/ from the Hurl files
make bruno-check       # CI check: fail if bruno/ is out of sync
```

E2E suite: an implementation extends `specs/e2e/playwright.base.ts` (override `baseURL`
and `webServer`) and must satisfy `specs/e2e/SELECTORS.md`. See
<https://docs.realworld.show/specifications/frontend/tests/>.

## Working on the docs

```
make documentation-setup     # cd docs && bun install
make documentation-dev       # local dev server
make documentation-build     # production build
```

## Answering "how do I build an implementation?"

Implementations live in their **own** repos, not here. Point users to:

- Backends → validate against `specs/api/` (the OpenAPI spec + Hurl/Bruno suites).
- Frontends → use the shared CSS theme + `specs/e2e/` (the Playwright suite + selector
  contract).
- The full guide: <https://docs.realworld.show/implementation-creation/introduction/>.

---

## File: CONTRIBUTING.md

# Contributing to RealWorld

We would love for you to contribute to RealWorld and help make it even better than it is
today! As a contributor, here are the guidelines we would like you to follow:

- [Question or Problem?](#question)
- [Issues and Bugs](#issue)
- [Feature Requests](#feature)
- [Submission Guidelines](#submit)
- [Coding Rules](#rules)
- [Commit Message Guidelines](#commit)

## <a name="question"></a> Got a Question or Problem?

Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.  
For open discussions, we encourage you to use the [Github Discussions][github-discussions] channels.

## <a name="issue"></a> Interested in creating Conduit for your framework?

To create an official implementation of Conduit, check out our [Github Discussions](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations) and see if anyone else has requested and/or is already working on your framework.
If not, feel free to start working on one!

Start [here][github-spec]!

## <a name="issue"></a> Found a Bug?

If you find a bug in the project, you can help us by
[submitting an issue][github-issue] to our [GitHub Repository][github]. Even better, you can
[submit a Pull Request](#submit-pr) with a fix.

## <a name="feature"></a> Missing a Feature?

You can _request_ a new feature by [submitting an issue](#submit-issue) to our GitHub
repository.

If you would like to _implement_ a new feature, please submit an issue with
a proposal for your work **FIRST**, to be sure that we can use it.
Please consider what kind of change it is:

- For a **Major Feature**, first open an issue and outline your proposal so that it can be
  discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
  and help you to craft the change so that it is successfully accepted into the project.
- **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).

## <a name="submit"></a> Submission Guidelines

### <a name="submit-issue"></a> Submitting an Issue

Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.

You can file new issues by selecting from our [new issue templates][github-choose] and filling out the issue template.

### <a name="submit-pr"></a> Submitting a Pull Request (PR)

Before you submit your Pull Request (PR) consider the following guidelines:

1. Search [GitHub](https://github.com/realworld-apps/realworld/pulls) for an open or closed PR
   that relates to your submission. You don't want to duplicate effort.
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
   Discussing the design up front helps to ensure that we're ready to accept your work.
1. Fork the realworld-apps/realworld repo.
1. Make your changes in a new git branch:

   ```bash
   git checkout -b my-fix-branch master
   ```

1. Create your patch.

1. Commit your changes using a descriptive commit message that follows our
   [commit message conventions](#commit).

1. Push your branch to GitHub:

   ```bash
   git push origin my-fix-branch
   ```

1. In GitHub, send a pull request to `realworld:master`.

- If we suggest changes then:

  - Make the required updates.
  - Rebase your branch and force push to your GitHub repository (this will update your Pull Request):

    ```bash
    git rebase master -i
    git push -f
    ```

That's it! Thank you for your contribution!

#### After your pull request is merged

After your pull request is merged, you can safely delete your branch and pull the changes
from the master (upstream) repository:

- Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:

  ```bash
  git push origin --delete my-fix-branch
  ```

- Check out the master branch:

  ```bash
  git checkout master -f
  ```

- Delete the local branch:

  ```bash
  git branch -D my-fix-branch
  ```

- Update your master with the latest upstream version:

  ```bash
  git pull --ff upstream master
  ```

## <a name="commit"></a> Commit Message Guidelines

> These guidelines have been added to the project starting from February 2025

We have very precise rules over how our git commit messages can be formatted. This leads to **more
readable messages** that are easy to follow when looking through the **project history**.

### Commit Message Format

Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:

```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```

The **header** is mandatory and the **scope** of the header is optional.

Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.

The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.

Samples:

```
docs(changelog): update changelog to beta.5
```

```
fix(release): need to depend on latest ng-lib

The version in our package.json gets copied to the one we publish, and users need the latest of these.
```

### Type

Must be one of the following:

- **docs**: Documentation only changes
- **feat**: A new feature
- **fix**: A bug fix

### Scope

The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).

The following is the list of supported scopes:

- **specs**
- **project**

### Subject

The subject contains a succinct description of the change:

- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter
- no dot (.) at the end

### Body

Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.

### Footer

The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.

**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

Samples :

```
Close #394
```

```
BREAKING CHANGE:
change login route to /users/login
```

[github]: https://github.com/realworld-apps/realworld
[github-issue]: https://github.com/realworld-apps/realworld/issues/new?assignees=&labels=bug&template=---bug-report.md&title=
[github-feature]: https://github.com/realworld-apps/realworld/issues/new?assignees=&labels=enhancement&template=---feature-request.md&title=
[github-choose]: https://github.com/realworld-apps/realworld/issues/new/choose
[github-discussions]: https://github.com/realworld-apps/realworld/discussions
[github-spec]: https://github.com/realworld-apps/realworld/tree/master/spec

---

## File: assets/generator/logo_parts.json

{
  "viewBox": "0 0 221 252",
  "width": 221,
  "height": 252,
  "colors": {
    "top": "#55aaff",
    "middle": "#3388ff",
    "bottom": "#1f6fe0",
    "check": "#80b000"
  },
  "paths": {
    "top": "M 9.763 40.565 C 4.371 43.491, 4 45.579, 4 73 C 4 106.897, 3.600 106.270, 24.929 105.782 C 39.807 105.442, 40.514 104.946, 37.173 97.179 C 30.873 82.535, 39.500 68.959, 55.066 69.022 C 70.046 69.083, 78.260 80.234, 74.370 95.232 C 71.567 106.040, 69.934 105.414, 102.445 106 L 130.176 106.500 132.588 109.306 C 135.631 112.846, 135.766 118.462, 133 126.500 C 130.506 133.749, 130.491 136.653, 132.926 140.647 C 136.918 147.193, 147.082 147.193, 151.074 140.647 C 153.545 136.594, 153.498 133.188, 150.857 124.989 C 148.467 117.566, 148.797 112.698, 151.954 108.821 C 153.801 106.554, 154.195 106.507, 168.886 106.786 C 186.101 107.113, 188 106.383, 188 99.434 C 188 94.290, 186.464 92, 183.014 92 C 163.101 92, 139.888 73.311, 135.921 54.085 C 134.195 45.721, 132.175 41.401, 129.399 40.136 C 125.228 38.235, 13.316 38.637, 9.763 40.565",
    "middle": "M 50.400 79.428 C 45.053 81.756, 43.435 88.021, 46.037 96.320 C 48.398 103.852, 48.572 111.739, 46.443 114.777 C 44.921 116.951, 44.435 117, 24.443 117 L 4 117 4.004 142.250 C 4.009 176.508, 4.502 177.294, 25.655 176.782 C 39.728 176.441, 40.484 175.876, 37.173 168.179 C 29.971 151.440, 43.084 135.719, 60.574 140.123 C 71.734 142.933, 77.579 153.284, 74.623 165 C 72.530 173.297, 72.570 174.699, 74.934 175.965 C 76.128 176.604, 87 177, 103.323 177 C 127.087 177, 130 177.179, 131.952 178.760 C 135.344 181.507, 135.721 187.823, 133.009 196.470 C 129.217 208.564, 132.526 216, 141.702 216 C 152.508 216, 154.914 209.915, 150.448 193.882 C 148.866 188.201, 148.782 186.672, 149.856 183.087 C 151.460 177.732, 153.413 177, 166.097 177 C 186.371 177, 186.991 175.969, 186.996 142.250 L 187 117 177.125 117 C 159.978 117, 157.584 118.397, 160.429 126.745 C 165.580 141.862, 156.862 154.910, 141.566 154.978 C 126.628 155.045, 117.562 141.521, 123.249 127.656 C 127.579 117.102, 127.293 117, 93.435 117 C 68.333 117, 66.899 116.899, 65 115 C 62.366 112.366, 62.351 104.653, 64.963 96.320 C 69.004 83.432, 61.417 74.632, 50.400 79.428",
    "bottom": "M 51 149.657 C 45.001 152.073, 43.211 158.305, 45.979 167.136 C 48.645 175.639, 48.653 183.347, 46 186 C 44.133 187.867, 42.667 188, 24 188 L 4 188 4 213.050 C 4 240.329, 4.280 241.929, 9.699 245.602 C 13.772 248.362, 171.707 249.123, 178.835 246.416 C 186.714 243.424, 186.992 242.296, 186.996 213.250 L 187 188 175.111 188 C 164.951 188, 162.883 188.274, 160.892 189.886 L 158.563 191.772 160.281 197.224 C 162.637 204.697, 162.456 210.637, 159.725 215.458 C 151.090 230.704, 127.872 228.628, 122.172 212.101 C 120.467 207.157, 120.704 204.643, 123.500 198 C 126.458 190.973, 126.395 191.538, 124.429 189.571 C 123.075 188.218, 118.991 188, 95.001 188 L 67.145 188 65.073 185.365 C 62.393 181.958, 62.375 175.577, 65.021 167.136 C 67.264 159.978, 66.604 154.761, 63.045 151.541 C 61.035 149.721, 53.691 148.573, 51 149.657",
    "check": "M 170.079 5.643 C 160.398 9.032, 152.186 16.330, 147.380 25.816 C 143.246 33.976, 143.128 49.573, 147.135 58.120 C 161.140 87.988, 203.268 88.158, 217.192 58.404 C 222.140 47.830, 221.627 30.213, 216.212 24.783 C 213.843 22.407, 213.724 22.535, 201.595 40.500 C 189.431 58.518, 188.050 60.014, 183.639 59.965 C 178.576 59.907, 176.605 58.133, 168.125 46 C 159.447 33.584, 159.411 33.227, 166.827 33.070 C 170.555 32.992, 173.340 35.378, 178.729 43.271 C 182.327 48.539, 183.852 49.064, 185.929 45.750 C 193.403 33.821, 204 14.682, 204 13.113 C 204 6.154, 182.281 1.372, 170.079 5.643"
  }
}

---

## File: assets/media/mobile_icons/ios/README.md

## iTunesArtwork & iTunesArtwork@2x (App Icon) file extension:

PNG extension is prepended to these two files -

While Apple suggested to omit the extension for these files,
the '.png' extension is actually required for iTunesConnect submission.

This is done for you so you don't have to.

However, for Ad_hoc or Enterprise distribution, the extension should be removed
from the files before adding to XCode to avoid error.

refs: https://developer.apple.com/library/ios/qa/qa1686/_index.html

## iTunesArtwork & iTunesArtwork@2x (App Icon) transparency handling:

As images with alpha channels or transparencies cannot be set as an application's icon on
iTunesConnect, all transparent pixels in your images will be converted into
solid blacks.

To achieve the best result, you're advised to adjust the transparency settings
in your source files before converting them with makeAppIcon.

refs: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/AppIcons.html

---

## File: assets/media/mobile_icons/ios/AppIcon.appiconset/Contents.json

{
  "images": [
    {
      "idiom": "iphone",
      "size": "20x20",
      "scale": "2x",
      "filename": "Icon-App-20x20@2x.png"
    },
    {
      "idiom": "iphone",
      "size": "20x20",
      "scale": "3x",
      "filename": "Icon-App-20x20@3x.png"
    },
    {
      "idiom": "iphone",
      "size": "29x29",
      "scale": "1x",
      "filename": "Icon-App-29x29@1x.png"
    },
    {
      "idiom": "iphone",
      "size": "29x29",
      "scale": "2x",
      "filename": "Icon-App-29x29@2x.png"
    },
    {
      "idiom": "iphone",
      "size": "29x29",
      "scale": "3x",
      "filename": "Icon-App-29x29@3x.png"
    },
    {
      "idiom": "iphone",
      "size": "40x40",
      "scale": "1x",
      "filename": "Icon-App-40x40@1x.png"
    },
    {
      "idiom": "iphone",
      "size": "40x40",
      "scale": "2x",
      "filename": "Icon-App-40x40@2x.png"
    },
    {
      "idiom": "iphone",
      "size": "40x40",
      "scale": "3x",
      "filename": "Icon-App-40x40@3x.png"
    },
    {
      "idiom": "iphone",
      "size": "57x57",
      "scale": "1x",
      "filename": "Icon-App-57x57@1x.png"
    },
    {
      "idiom": "iphone",
      "size": "57x57",
      "scale": "2x",
      "filename": "Icon-App-57x57@2x.png"
    },
    {
      "idiom": "iphone",
      "size": "60x60",
      "scale": "1x",
      "filename": "Icon-App-60x60@1x.png"
    },
    {
      "idiom": "iphone",
      "size": "60x60",
      "scale": "2x",
      "filename": "Icon-App-60x60@2x.png"
    },
    {
      "idiom": "iphone",
      "size": "60x60",
      "scale": "3x",
      "filename": "Icon-App-60x60@3x.png"
    },
    {
      "idiom": "iphone",
      "size": "76x76",
      "scale": "1x",
      "filename": "Icon-App-76x76@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "20x20",
      "scale": "1x",
      "filename": "Icon-App-20x20@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "20x20",
      "scale": "2x",
      "filename": "Icon-App-20x20@2x.png"
    },
    {
      "idiom": "ipad",
      "size": "29x29",
      "scale": "1x",
      "filename": "Icon-App-29x29@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "29x29",
      "scale": "2x",
      "filename": "Icon-App-29x29@2x.png"
    },
    {
      "idiom": "ipad",
      "size": "40x40",
      "scale": "1x",
      "filename": "Icon-App-40x40@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "40x40",
      "scale": "2x",
      "filename": "Icon-App-40x40@2x.png"
    },
    {
      "size": "50x50",
      "idiom": "ipad",
      "filename": "Icon-Small-50x50@1x.png",
      "scale": "1x"
    },
    {
      "size": "50x50",
      "idiom": "ipad",
      "filename": "Icon-Small-50x50@2x.png",
      "scale": "2x"
    },
    {
      "idiom": "ipad",
      "size": "72x72",
      "scale": "1x",
      "filename": "Icon-App-72x72@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "72x72",
      "scale": "2x",
      "filename": "Icon-App-72x72@2x.png"
    },
    {
      "idiom": "ipad",
      "size": "76x76",
      "scale": "1x",
      "filename": "Icon-App-76x76@1x.png"
    },
    {
      "idiom": "ipad",
      "size": "76x76",
      "scale": "2x",
      "filename": "Icon-App-76x76@2x.png"
    },
    {
      "idiom": "ipad",
      "size": "76x76",
      "scale": "3x",
      "filename": "Icon-App-76x76@3x.png"
    },
    {
      "idiom": "ipad",
      "size": "83.5x83.5",
      "scale": "2x",
      "filename": "Icon-App-83.5x83.5@2x.png"
    }
  ],
  "info": {
    "version": 1,
    "author": "makeappicon"
  }
}

---

## File: assets/media/mobile_icons/watchkit/AppIcon.appiconset/Contents.json

{
  "images": [
    {
      "size": "24x24",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-24@2x.png",
      "role": "notificationCenter",
      "subtype": "38mm"
    },
    {
      "size": "27.5x27.5",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-27.5@2x.png",
      "role": "notificationCenter",
      "subtype": "42mm"
    },
    {
      "size": "29x29",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-29@2x.png",
      "role": "companionSettings"
    },
    {
      "size": "29x29",
      "idiom": "watch",
      "scale": "3x",
      "filename": "Icon-29@3x.png",
      "role": "companionSettings"
    },
    {
      "size": "40x40",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-40@2x.png",
      "role": "appLauncher",
      "subtype": "38mm"
    },
    {
      "size": "44x44",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-44@2x.png",
      "role": "longLook",
      "subtype": "42mm"
    },
    {
      "size": "86x86",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-86@2x.png",
      "role": "quickLook",
      "subtype": "38mm"
    },
    {
      "size": "98x98",
      "idiom": "watch",
      "scale": "2x",
      "filename": "Icon-98@2x.png",
      "role": "quickLook",
      "subtype": "42mm"
    }
  ],
  "info": {
    "version": 1,
    "author": "makeappicon"
  }
}

---

## File: docs/README.md

# Starlight Starter Kit: Basics

[](https://starlight.astro.build)

```
npm create astro@latest -- --template starlight
```

[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)

> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!

## 🚀 Project Structure

Inside of your Astro + Starlight project, you'll see the following folders and files:

```
.
├── public/
├── src/
│   ├── assets/
│   ├── content/
│   │   ├── docs/
│   │   └── config.ts
│   └── env.d.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```

Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.

Images can be added to `src/assets/` and embedded in Markdown with a relative link.

Static assets, like favicons, can be placed in the `public/` directory.

## 🧞 Commands

All commands are run from the root of the project, from a terminal:

| Command                   | Action                                           |
| :------------------------ | :----------------------------------------------- |
| `npm install`             | Installs dependencies                            |
| `npm run dev`             | Starts local dev server at `localhost:4321`      |
| `npm run build`           | Build your production site to `./dist/`          |
| `npm run preview`         | Preview your build locally, before deploying     |
| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI                     |

## 👀 Want to learn more?

Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

---

## File: docs/package.json

{
  "name": "documentation",
  "type": "module",
  "version": "0.0.1",
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro check && astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@astrojs/check": "^0.9.6",
    "@astrojs/react": "^4.4.2",
    "@astrojs/starlight": "^0.37.4",
    "@astrojs/starlight-tailwind": "^4.0.2",
    "@tailwindcss/vite": "^4.1.8",
    "@types/react": "^19.2.10",
    "@types/react-dom": "^19.2.3",
    "astro": "^5.16.16",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "sharp": "^0.34.5",
    "tailwindcss": "^4.1.18",
    "typescript": "^5.9.3"
  }
}

---

## File: docs/tsconfig.json

{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}

---

## File: docs/non-included/LICENSES_LOGOS.md

# Logo Licenses & Trademark Analysis

> **Disclaimer**: This document was generated by Claude (Anthropic) and is provided
> for informational purposes only. It does not constitute legal advice. Consult a
> qualified attorney for legal guidance.

This document covers the licensing and trademark status of the framework logos
used in the animated SVG (`frameworks.svg`) of this repository.

## Individual Logo Licenses

### Angular
- **License**: CC BY 4.0 (Creative Commons Attribution 4.0 International)
- **Source**: [angular.dev/press-kit](https://angular.dev/press-kit)
- **Requirements**: Attribution to Google required
- **Trademark**: "ANGULAR" is a registered trademark of Google LLC

### React
- **License**: CC BY 4.0 (Creative Commons Attribution 4.0 International)
- **Source**: [github.com/facebook/react](https://github.com/facebook/react) / react.dev repo
- **Requirements**: Attribution to Meta required
- **Trademark**: React name and logo are trademarks of Meta Platforms, Inc.
- **Reference**: Confirmed in [GitHub issue #12570](https://github.com/facebook/react/issues/12570)

### Vue.js
- **License**: CC BY-NC-SA 4.0 (Creative Commons Attribution-NonCommercial-ShareAlike 4.0)
- **Source**: [github.com/vuejs/art](https://github.com/vuejs/art)
- **Requirements**: Attribution, non-commercial use, share-alike on derivatives
- **Permitted explicitly**: Open source / non-profit projects related to Vue.js
- **Trademark**: VUE.JS is a registered US trademark owned by Yuxi (Evan) You

### Django
- **License**: No open license — governed by DSF Trademark Policy
- **Source**: [djangoproject.com/community/logos](https://www.djangoproject.com/community/logos/)
- **Requirements**:
  - Must not imply endorsement by the Django Software Foundation
  - Must not modify the logo (colors, proportions, added text)
- **Trademark**: Registered trademark of the Django Software Foundation
- **Reference**: [djangoproject.com/trademarks](https://www.djangoproject.com/trademarks/)

### Hono
- **License**: MIT (as part of the honojs/hono repository)
- **Source**: [github.com/honojs/hono/docs/images](https://github.com/honojs/hono/tree/main/docs/images)
- **Requirements**: Include MIT copyright notice
- **Also available as**: CC0 1.0 (public domain) on third-party collections (gilbarbara/logos, Wikimedia Commons)

## Combined License of the Animated SVG

Since `frameworks.svg` is a derivative work incorporating all five logos, the
**most restrictive compatible terms** apply to the combined work.

### Governing constraints

| Constraint        | Source         | Impact                                         |
|-------------------|----------------|-------------------------------------------------|
| **Attribution**   | Angular, React, Vue, Hono | Credit all projects and their respective owners |
| **NonCommercial** | Vue.js (CC BY-NC-SA 4.0)  | The animated SVG cannot be used commercially    |
| **ShareAlike**    | Vue.js (CC BY-NC-SA 4.0)  | Derivatives of the SVG must use CC BY-NC-SA 4.0 or compatible |
| **No modification** | Django (trademark) | The Django logo within the SVG must remain unmodified (colors, proportions) |
| **Linking**       | Django (trademark) | Logo should link to djangoproject.com "wherever technically possible" — not applicable here as the logo cycles within an animated SVG, making a dedicated hyperlink technically infeasible |
| **No endorsement** | All (trademark) | Must not imply official affiliation with any project |

### Effective license for this animated SVG

**CC BY-NC-SA 4.0**, with additional trademark constraints from Django.

This means:
- Free to share and adapt for **non-commercial** purposes
- Must give **attribution** to all five projects
- Derivatives must be shared under **the same license**
- The Django logo portion must remain **unmodified** and **link to djangoproject.com** when possible
- Usage must not suggest **endorsement** by any of the projects

### Risk assessment

| Logo    | Risk  | Notes                                                          |
|---------|-------|----------------------------------------------------------------|
| Angular | Low   | CC BY 4.0 is very permissive                                  |
| React   | Low   | CC BY 4.0 is very permissive                                  |
| Vue.js  | Low   | Explicitly allows open-source project use                     |
| Django  | Medium | Trademark-only; animation of the logo could be seen as modification. We use minimal fade-in/out animation only, keeping the logo visually unaltered |
| Hono    | Low   | MIT / CC0 — most permissive                                   |

### Recommendation

The animated SVG is suitable for an open-source demo-apps repository, provided:
1. This `LICENSES_LOGOS.md` file is kept in the repository for attribution
2. The Django logo is not distorted, recolored, or visually altered (fade-in/out is acceptable)
3. The Django logo links to djangoproject.com when technically possible
4. The repository does not imply official endorsement by any of the five projects

## Attribution

- Angular logo by Google — [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- React logo by Meta Platforms, Inc. — [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- Vue.js logo by Evan You — [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
- Django logo by the Django Software Foundation — [Trademark Policy](https://www.djangoproject.com/trademarks/)
- Hono logo by Yusuke Wada — [MIT License](https://github.com/honojs/hono/blob/main/LICENSE)

---

*Generated by Claude (Anthropic)*

---

## File: docs/src/content/docs/index.mdx

---
title: RealWorld apps
template: splash
hero:
  tagline: |
    While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build real applications with it. That's why we, with the help of open source experts, design and serve as exemplary real world applications for each framework.
  image:
    file: ../../assets/img/realworld-logo.png
  actions:
    - text: Documentation
      link: /introduction
      icon: right-arrow
---

---

## File: docs/src/content/docs/introduction.mdx

---
title: Introduction
---

# Introduction

> See how _the exact same_ Medium.com clone is built using different [frontends](https://codebase.show/projects/realworld?category=frontend) and [backends](https://codebase.show/projects/realworld?category=backend). Yes, you can mix and match them, because **they all adhere to the same [API spec](/specifications/backend/introduction)** 😮😎

While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build _real_ applications with it.

**RealWorld** solves this by allowing you to choose any frontend (React, Angular, & more) and any backend (Node, Django, & more).

_Read the [full blog post announcing RealWorld on Medium.](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5)_

Join us on [GitHub Discussions!](https://github.com/realworld-apps/realworld/discussions) 🎉

## Implementations

Over 150 implementations have been created using various languages, libraries, and frameworks.

Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld).

## Create a new implementation

[**Create a new implementation >>>**](implementation-creation/introduction)

Or you can [view upcoming implementations (WIPs)](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations).

## Learn more

- ["Introducing RealWorld 🙌"](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5) by Eric Simons
- Every tutorial is built against the same [API spec](/specifications/backend/introduction) to ensure modularity of every frontend & backend
- Every frontend utilizes the same hand crafted [Bootstrap 4 theme](https://github.com/gothinkster/conduit-bootstrap-template) for identical UI/UX
- There is a [hosted version](https://realworld-docs.netlify.app/docs/specs/frontend-specs/api#demo-api) of the backend API available for public usage, no API keys are required
- Interested in creating a new RealWorld stack? View our [starter guide & spec](/implementation-creation/introduction)

---

## File: docs/src/content/docs/community/authors.md

---
title: Authors
---

# Who currently maintains the project

#### [Gérôme Grignon](https://github.com/geromegrignon) - Maintainer



Gérôme is a Software Engineer at Lucca. He's an open-source enthusiast.

#### [Manuel Vila](https://github.com/mvila) - Maintainer



Manuel is an independent Software Engineer, creator of the [Layr framework](https://layrjs.com) and the [CodebaseShow website](https://codebase.show/).



# Who created it

RealWorld would not be possible without the [open source community](https://realworld-docs.netlify.app/docs/community/special-thanks) continuously helping push the project forward. In addition, the former team was composed of:

#### [Anish Karandikar](https://github.com/anishkny) - Core Maintainer



MathWorker, ex-Google, ex-Computational Fluid Dynamicist, forever lover of tech & humanities ❤️

#### [Cameron Chapman](https://github.com/Cameron-C-Chapman) - Core Maintainer



Cameron Chapman is a Software Engineer at FanThreeSixty. He's an open source enthusiast and is helping to teach a local web development boot camp at Kansas University.

#### [Eric Simons](https://twitter.com/ericsimons40) - Founder/Maintainer



Eric is a Software Engineer, UI Designer, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team.

#### [Albert Pai](https://twitter.com/iamalbertpai) - Founder/Maintainer



Albert is a Software Engineer, DevOps ninja, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team.

#### [Thinkster](https://twitter.com/gothinkster) - Funded



[Thinkster](https://x.com/GoThinkster) created high quality resources that helped Javascript developers succeed. The RealWorld project wouldn't exist without their funding!

#### [James Brewer](https://twitter.com/brwr_) - Admin



James is a Software Engineer at Square and a contributor to the Django project. He created & maintained the RW Django codebase and continually provides guidance for the RealWorld project itself.

#### [Sandeesh S.](https://github.com/sandeesh) - Admin



Full stack developer, Laravel enthusiast, Digital marketing specialist and an avid gamer.

---

## File: docs/src/content/docs/community/resources.md

---
title: Resources
---

# Community created resources

- Performance comparisons:
  - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2020](https://medium.com/dailyjs/a-realworld-comparison-of-front-end-frameworks-2020-4e50655fe4c1)
  - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2019](https://medium.freecodecamp.org/a-realworld-comparison-of-front-end-frameworks-with-benchmarks-2019-update-4be0d3c78075)
  - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2018](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update-e5760fb4a962)
  - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2017](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-e1cb62fd526c)

:::tip
Hello fellow writer, get in touch with us in [**GitHub Discussions**](https://github.com/realworld-apps/realworld/discussions/categories/community) so we can add your RealWorld related content here.
:::

---

## File: docs/src/content/docs/community/special-thanks.md

---
title: Special thanks
---

RealWorld would not be possible without the open source community's assistance in reviewing codebases, developing new app implementations, and a variety of other duties that help the project progress. We'd like to thank the following OSS leaders for their contributions to RealWorld:

- **Dan Abramov** (creator of Redux) for helping [spark the initial idea](https://twitter.com/dan_abramov/status/692009757775896577), [getting the Redux community involved](https://github.com/reactjs/redux/issues/1353), as well as graciously taking the time to provide feedback on the Redux codebase
- **Max Lynch** (creator of Ionic) for taking the time to provide guidance in the early days of this project
- **Addy Osmani** (creator of TodoMVC) for helping [spark the initial idea](https://twitter.com/addyosmani/status/762828483433144320) and his amazing work with TodoMVC
- **TodoMVC** ([team & contributors](https://github.com/tastejs/todomvc#team)) for their exemplary & successful work; their project & org has been an invaluable analogy for us as we've built out RealWorld
- **James Brewer** (docs contributor to Django) for countless brainstorming sessions, helping name this project, and creating the Django codebase + tutorial

---

## File: docs/src/content/docs/implementation-creation/expectations.md

---
title: Expectations
---

## Remember: Keep your codebases _simple_, yet _robust_.

If a new developer to your framework comes along and takes longer than 10 minutes to grasp the high-level architecture, it's likely that you went a little overboard in the engineering department.

Alternatively, you should _never_ forgo following fundamental best practices for the sake of simplicity, lest we teach that same newbie dev the _wrong_ way of doing things.

The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered.

## To write tests, or to not write tests?

**TL;DR** — we require a minimum of **one** unit test with every repo, but we'd definitely prefer all of them to include excellent testing coverage if the maintainers are willing to add it (or if someone in the community is kind enough to make a pull request :)

We believe that tests are a good concept, and we are big supporters of TDD in general. Building Conduit implementations without complete testing coverage, on the other hand, is a significant time commitment in and of itself, therefore we didn't include it in the spec at first since we believed that if people wanted it, it would be a fantastic "extra credit" aim for the repo. For example, a request for unit tests was made in our Angular 2 repo, and several fantastic community members are presently working on a PR to address it.

Another reason we didn’t include them in the spec is from the "Golden Rule" above:

> The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered.

Most startups we know that work in consumer-facing apps (like Conduit) don’t apply TDD/testing until they have a solid product-market fit, which is smart because they then spend most of their time iterating on product & UI and thus are far more likely to find PMF.

This doesn’t mean that TDD/testing === over-engineering, but in certain circumstances that statement does evaluate true (ex: consumer product finding PMF, side-projects, robust prototypes, etc).

That said, we do _prefer_ that every repo includes excellent tests that are exemplary of TDD/testing with that framework 👍

## Other Expectations

- All the required features (see specs) should be implemented.
- You should publish your implementation on a dedicated GitHub repository with the "Issues" section open.
- You should provide a README that presents an overview of your implementation and explains how to run it locally.
- The library/framework you are using should have at least 300 GitHub stars.
- You should do your best to keep your implementation up to date.

---

## File: docs/src/content/docs/implementation-creation/features.md

---
title: Features
---

**General functionality:**

- Authenticate users via JWT (login/signup pages + logout button on settings page)
- CRU- users (sign up & settings page - no deleting required)
- CRUD Articles
- CR-D Comments on articles (no updating required)
- GET and display paginated lists of articles
- Favorite articles
- Follow other users

---

## File: docs/src/content/docs/implementation-creation/introduction.md

---
title: Introduction
---

**Conduit** is a social blogging site (i.e. a Medium.com clone). It uses a custom API for all requests, including authentication.

:::tip
Check for [Discussions](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations) about works in progress as we don't list duplicate projects.  
An opportunity to collaborate might await you already.
:::

Otherwise:

1. [fork our starter kit](https://github.com/gothinkster/realworld-starter-kit)
2. Read the following sections: _expectations_ and _features_ for a better understanding of this project
3. Read the frontend and/or the backend specs
4. Submit the new implementation on [CodebaseShow](https://codebase.show/projects/realworld)

**Happy coding!**

---

## File: docs/src/content/docs/specifications/backend/api-response-format.md

---
title: API response format
---

## JSON Objects returned by API:

Make sure the right content type like `Content-Type: application/json; charset=utf-8` is correctly returned.

### Users (for authentication)

```json
{
  "user": {
    "email": "jake@jake.jake",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": null,
    "image": null
  }
}
```

### Profile

```json
{
  "profile": {
    "username": "jake",
    "bio": "I work at statefarm",
    "image": "https://api.realworld.io/images/smiley-cyrus.jpg",
    "following": false
  }
}
```

### Single Article

```json
{
  "article": {
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "It takes a Jacobian",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }
}
```

### Multiple Articles

:::caution
Starting from the 2024/08/16, the endpoints retrieving a list of articles do no longer return the body of an article for performance reasons.
It affects:
- `GET /api/articles`
- `GET /api/articles/feed`
:::

```json
{
  "articles":[{
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }, {
    "slug": "how-to-train-your-dragon-2",
    "title": "How to train your dragon 2",
    "description": "So toothless",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }],
  "articlesCount": 2
}
```

### Single Comment

```json
{
  "comment": {
    "id": 1,
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:22:56.637Z",
    "body": "It takes a Jacobian",
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }
}
```

### Multiple Comments

```json
{
  "comments": [{
    "id": 1,
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:22:56.637Z",
    "body": "It takes a Jacobian",
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }]
}
```

### List of Tags

```json
{
  "tags": [
    "reactjs",
    "angularjs"
  ]
}
```

---

## File: docs/src/content/docs/specifications/backend/bruno.md

---
title: Bruno
---

For your convenience, we have a [Bruno collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/bruno) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-bruno.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-bruno.sh), or open the `bruno/` folder in the [Bruno app](https://www.usebruno.com) to run requests interactively.

The Bruno collection is automatically generated from the [Hurl test suite](/specifications/backend/hurl), which is the source of truth. It is kept in sync via CI.

## Running API tests locally

To locally run the provided Bruno collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).

---

## File: docs/src/content/docs/specifications/backend/cors.md

---
title: CORS
---

## Considerations for your backend with [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)

If the backend is about to run on a different host/port than the frontend, make sure to handle `OPTIONS` too and return correct `Access-Control-Allow-Origin` and `Access-Control-Allow-Headers` (e.g. `Content-Type`).

---

## File: docs/src/content/docs/specifications/backend/endpoints.md

---
title: Endpoints
---

### Authentication Header:

You can read the authentication header from the headers of the request

`Authorization: Token jwt.token.here`

### Authentication:

`POST /api/users/login`

Example request body:

```json
{
  "user":{
    "email": "jake@jake.jake",
    "password": "jakejake"
  }
}
```

No authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication)

Required fields: `email`, `password`

### Registration:

`POST /api/users`

Example request body:

```json
{
  "user":{
    "username": "Jacob",
    "email": "jake@jake.jake",
    "password": "jakejake"
  }
}
```

No authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication)

Required fields: `email`, `username`, `password`

### Get Current User

`GET /api/user`

Authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication) that's the current user

### Update User

`PUT /api/user`

Example request body:

```json
{
  "user":{
    "email": "jake@jake.jake",
    "bio": "I like to skateboard",
    "image": "https://i.stack.imgur.com/xHWG8.jpg"
  }
}
```

Authentication required, returns the [User](/specifications/backend/api-response-format#users-for-authentication)

Accepted fields: `email`, `username`, `password`, `image`, `bio`

### Get Profile

`GET /api/profiles/:username`

Authentication optional, returns a [Profile](/specifications/backend/api-response-format#profile)

### Follow user

`POST /api/profiles/:username/follow`

Authentication required, returns a [Profile](/specifications/backend/api-response-format#profile)

No additional parameters required

### Unfollow user

`DELETE /api/profiles/:username/follow`

Authentication required, returns a [Profile](/specifications/backend/api-response-format#profile)

No additional parameters required

### List Articles

`GET /api/articles`

Returns most recent articles globally by default, provide `tag`, `author` or `favorited` query parameter to filter results

Query Parameters:

Filter by tag:

`?tag=AngularJS`

Filter by author:

`?author=jake`

Favorited by user:

`?favorited=jake`

Limit number of articles (default is 20):

`?limit=20`

Offset/skip number of articles (default is 0):

`?offset=0`

Authentication optional, will return [multiple articles](/specifications/backend/api-response-format#multiple-articles), ordered by most recent first

### Feed Articles

`GET /api/articles/feed`

Can also take `limit` and `offset` query parameters like [List Articles](/specifications/backend/endpoints#list-articles)

Authentication required, will return [multiple articles](/specifications/backend/api-response-format#multiple-articles) created by followed users, ordered by most recent first.

### Get Article

`GET /api/articles/:slug`

No authentication required, will return [single article](/specifications/backend/api-response-format#single-article)

### Create Article

`POST /api/articles`

Example request body:

```json
{
  "article": {
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "You have to believe",
    "tagList": ["reactjs", "angularjs", "dragons"]
  }
}
```

Authentication required, will return an [Article](/specifications/backend/api-response-format#single-article)

Required fields: `title`, `description`, `body`

Optional fields: `tagList` as an array of Strings

### Update Article

`PUT /api/articles/:slug`

Example request body:

```json
{
  "article": {
    "title": "Did you train your dragon?"
  }
}
```

Authentication required, returns the updated [Article](/specifications/backend/api-response-format#single-article)

Optional fields: `title`, `description`, `body`

The `slug` also gets updated when the `title` is changed

> The `slug` is the article's URL identifier. The spec only requires it to be a unique string that you can use to fetch, update, and delete the article — duplicate titles must still produce distinct slugs. How you derive it is up to your implementation (commonly a kebab-cased title); no particular format is enforced by the test suite.

### Delete Article

`DELETE /api/articles/:slug`

Authentication required

### Add Comments to an Article

`POST /api/articles/:slug/comments`

Example request body:

```json
{
  "comment": {
    "body": "His name was my name too."
  }
}
```

Authentication required, returns the created [Comment](/specifications/backend/api-response-format#single-comment)

Required field: `body`

### Get Comments from an Article

`GET /api/articles/:slug/comments`

Authentication optional, returns [multiple comments](/specifications/backend/api-response-format#multiple-comments)

### Delete Comment

`DELETE /api/articles/:slug/comments/:id`

Authentication required

### Favorite Article

`POST /api/articles/:slug/favorite`

Authentication required, returns the [Article](/specifications/backend/api-response-format#single-article)

No additional parameters required

### Unfavorite Article

`DELETE /api/articles/:slug/favorite`

Authentication required, returns the [Article](/specifications/backend/api-response-format#single-article)

No additional parameters required

### Get Tags

`GET /api/tags`

No authentication required, returns a [List of Tags](/specifications/backend/api-response-format#list-of-tags)

---

## File: docs/src/content/docs/specifications/backend/error-handling.md

---
title: Error handling
---

### Errors and Status Codes

If a request fails any validations, expect a 422 and errors in the following format:

```json
{
  "errors":{
    "body": [
      "can't be empty"
    ]
  }
}
```

#### Other status codes:

401 for Unauthorized requests, when a request requires authentication but it isn't provided

403 for Forbidden requests, when a request may be valid but the user doesn't have permissions to perform the action

404 for Not found requests, when a resource can't be found to fulfill the request

---

## File: docs/src/content/docs/specifications/backend/hurl.md

---
title: Hurl
---

For your convenience, we have a [Hurl collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/hurl) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-hurl.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-hurl.sh).

A [Bruno collection](/specifications/backend/bruno) is also available if you prefer a GUI-based workflow.

## Running API tests locally

To locally run the provided Hurl collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).

---

## File: docs/src/content/docs/specifications/backend/introduction.md

---
title: Introduction
---

All backend implementations need to adhere to our [API spec](https://github.com/realworld-apps/realworld/tree/main/specs/api). The full API is described in the [OpenAPI spec](https://github.com/realworld-apps/realworld/blob/main/specs/api/openapi.yml).

For your convenience, we have a [Hurl collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/hurl) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-hurl.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-hurl.sh).

:::tip[The tests are the source of truth]
These pages summarize the contract in prose, but the [OpenAPI spec](https://github.com/realworld-apps/realworld/blob/main/specs/api/openapi.yml) and the [Hurl suite](https://github.com/realworld-apps/realworld/tree/main/specs/api/hurl) are what actually define it. Where the prose and the tests disagree, **the tests win** — so build against them, and if you find a discrepancy, please [open an issue](https://github.com/realworld-apps/realworld/issues).
:::

Once you're set up, read the rest of the backend specification: [Endpoints](/specifications/backend/endpoints/), [API response format](/specifications/backend/api-response-format/), and [Error handling](/specifications/backend/error-handling/). You can also start from our [starter kit](https://github.com/gothinkster/realworld-starter-kit).

---

## File: docs/src/content/docs/specifications/backend/postman.md

---
title: Postman
---

For your convenience, we have a [Postman collection](https://github.com/realworld-apps/realworld/blob/main/specs/api/legacy_Conduit.postman_collection.json) that you can use to test your API endpoints as you build your app.

## Running API tests locally

To locally run the provided Postman collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).

---

## File: docs/src/content/docs/specifications/backend/tests.md

---
title: Tests
---

Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!)

---

## File: docs/src/content/docs/specifications/frontend/api.md

---
title: API
---

This project provides you different solutions to test your frontend implementation with an API by:

- [running our official backend implementation locally](#run-the-official-backend-implementation-locally)
- [using the API deployed for the official demo](#demo-api)

## Run the official backend implementation locally

The official backend implementation is open-sourced.  
You can find the GitHub repository [here](https://github.com/realworld-apps/nitro-prisma-zod-realworld-example-app).
The Readme will provide you guidances to start the server locally.

## Demo API

This project provides you with a public hosted API to test your frontend implementations.  
Point your API requests to `https://api.realworld.show/api` and you're good to go!

### API Usage

The API is freely available for public usage but its access is limited to RealWorld usage only: you won't be able to consume it on its own but with a frontend application.

## API Limitations

:::info
To avoid the need for content moderation on the public API, the following limitations have been introduced in 2021
:::

The visibility of user content is limited :

- logged out users see only content created by demo accounts
- logged in users see only their content and the content created by demo accounts

---

## File: docs/src/content/docs/specifications/frontend/routing.md

---
title: Routing
---

- Home page (URL: `/` )
  - List of tags
  - List of articles pulled from either Feed, Global, or by Tag
  - Pagination for list of articles
- Sign in/Sign up pages (URL: `/login`, `/register` )
  - Uses JWT (store the token in localStorage)
  - Authentication can be easily switched to session/cookie based
- Settings page (URL: `/settings` )
- Editor page to create/edit articles (URL: `/editor`, `/editor/article-slug-here` )
- Article page (URL: `/article/article-slug-here` )
  - Delete article button (only shown to article's author)
  - Render markdown from server client side
  - Comments section at bottom of page
  - Delete comment button (only shown to comment's author)
- Profile page (URL: `/profile/:username`, `/profile/:username/favorites` )
  - Show basic user info
  - List of articles populated from author's created articles or author's favorited articles

---

## File: docs/src/content/docs/specifications/frontend/styles.md

---
title: Styles
---

All frontend implementations should use the shared [styles.css](https://github.com/realworld-apps/realworld/blob/main/assets/theme/styles.css) file from the main repository. This is a single CSS file (Conduit Minimal CSS v4) that includes only the classes actually used by Conduit.

The CSS classes it provides match the [templates](/specifications/frontend/templates/) and the [E2E test selectors contract](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/SELECTORS.md).

### Fonts and icons

`styles.css` covers layout and components only — it does **not** bundle fonts or icons, so load these separately in your `<head>`:

- **Fonts:** the theme uses `Source Sans Pro` (body) and `Lora` (headings/article text). Without them, browsers fall back to generic sans-serif/serif. Load them however you prefer (e.g. Google Fonts).
- **Icons:** the templates use `ion-*` classes (`ion-heart`, `ion-compose`, `ion-edit`, `ion-gear-a`, `ion-plus-round`, `ion-trash-a`, `ion-close-round`) from the legacy [Ionicons](https://ionic.io/ionicons) v2 set. Load an icon stylesheet that provides them, or substitute equivalent icons of your own.

### Default Avatar

When a user has no profile image, implementations should display the [default avatar](https://github.com/realworld-apps/realworld/blob/main/assets/media/default-avatar.svg) (a smiley face icon).

---

## File: docs/src/content/docs/specifications/frontend/templates.md

---
title: Templates
---

## Head

The `<head>` element includes the page metadata (title, description) and loads the stylesheet.

Load the shared [Conduit theme](https://github.com/realworld-apps/realworld/blob/main/assets/theme/styles.css) plus the fonts and icons it relies on. `styles.css` does **not** bundle fonts or icons, so the head below loads all three; see the [Styles](/specifications/frontend/styles/) page for details.

```html
<head>
  <meta charset="utf-8" />
  <title>Conduit</title>
  <!-- Icons: the ion-* classes used in the templates below (legacy Ionicons v2) -->
  <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css"
  />
  <!-- Fonts the theme uses -->
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700|Lora:400,700"
  />
  <!-- The shared Conduit theme (serve styles.css from your own app) -->
  <link rel="stylesheet" href="/styles.css" />
</head>
```

## Layout

### Header

#### Unauthenticated user

If no user is logged in, then the header should include links to:

- the home page
- the login page
- the register page

> the link of the active page should use the **active** css class.

```html
<nav class="navbar navbar-light">
  <div class="container">
    <a class="navbar-brand" href="/">conduit</a>
    <ul class="nav navbar-nav pull-xs-right">
      <li class="nav-item">
        <!-- Add "active" class when you're on that page" -->
        <a class="nav-link active" href="/">Home</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/login">Sign in</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/register">Sign up</a>
      </li>
    </ul>
  </div>
</nav>
```

#### Authenticated user

If a user is logged in, then the header should include links to:

- the home page
- the new article page
- the settings page
- the profile page

> the link of the active page should use the **active** css class.

```html
<nav class="navbar navbar-light">
  <div class="container">
    <a class="navbar-brand" href="/">conduit</a>
    <ul class="nav navbar-nav pull-xs-right">
      <li class="nav-item">
        <!-- Add "active" class when you're on that page" -->
        <a class="nav-link active" href="/">Home</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/editor"> <i class="ion-compose"></i>&nbsp;New Article </a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/settings"> <i class="ion-gear-a"></i>&nbsp;Settings </a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/profile/eric-simons">
          <img src="" class="user-pic" />
          Eric Simons
        </a>
      </li>
    </ul>
  </div>
</nav>
```

### Footer

```html
<footer>
  <div class="container">
    <a href="/" class="logo-font">conduit</a>
    <span class="attribution">
      An interactive learning project. Code &amp; design licensed under MIT.
    </span>
  </div>
</footer>
```

## Pages

### Home

The Home page includes up to three tabs:

- a default **Global Feed** tab
- an optional **tag name** tab, appears after clicking one of the popular tags
- an optional **Your Feed** tab, appears after logging in

```html
<div class="home-page">
  <div class="banner">
    <div class="container">
      <h1 class="logo-font">conduit</h1>
      <p>A place to share your knowledge.</p>
    </div>
  </div>

  <div class="container page">
    <div class="row">
      <div class="col-md-9">
        <div class="feed-toggle">
          <ul class="nav nav-pills outline-active">
            <li class="nav-item">
              <a class="nav-link" href="">Your Feed</a>
            </li>
            <li class="nav-item">
              <a class="nav-link active" href="">Global Feed</a>
            </li>
          </ul>
        </div>

        <div class="article-preview">
          <div class="article-meta">
            <a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
            <div class="info">
              <a href="/profile/eric-simons" class="author">Eric Simons</a>
              <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
              <i class="ion-heart"></i> 29
            </button>
          </div>
          <a href="/article/how-to-build-webapps-that-scale" class="preview-link">
            <h1>How to build webapps that scale</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            <ul class="tag-list">
              <li class="tag-default tag-pill tag-outline">realworld</li>
              <li class="tag-default tag-pill tag-outline">implementations</li>
            </ul>
          </a>
        </div>

        <div class="article-preview">
          <div class="article-meta">
            <a href="/profile/albert-pai"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
            <div class="info">
              <a href="/profile/albert-pai" class="author">Albert Pai</a>
              <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
              <i class="ion-heart"></i> 32
            </button>
          </div>
          <a href="/article/the-song-you" class="preview-link">
            <h1>The song you won't ever stop singing. No matter how hard you try.</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            <ul class="tag-list">
              <li class="tag-default tag-pill tag-outline">realworld</li>
              <li class="tag-default tag-pill tag-outline">implementations</li>
            </ul>
          </a>
        </div>

        <ul class="pagination">
          <li class="page-item active">
            <a class="page-link" href="">1</a>
          </li>
          <li class="page-item">
            <a class="page-link" href="">2</a>
          </li>
        </ul>
      </div>

      <div class="col-md-3">
        <div class="sidebar">
          <p>Popular Tags</p>

          <div class="tag-list">
            <a href="" class="tag-pill tag-default">programming</a>
            <a href="" class="tag-pill tag-default">javascript</a>
            <a href="" class="tag-pill tag-default">emberjs</a>
            <a href="" class="tag-pill tag-default">angularjs</a>
            <a href="" class="tag-pill tag-default">react</a>
            <a href="" class="tag-pill tag-default">mean</a>
            <a href="" class="tag-pill tag-default">node</a>
            <a href="" class="tag-pill tag-default">rails</a>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
```

### Authentication

#### Login

```html
<div class="auth-page">
  <div class="container page">
    <div class="row">
      <div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Sign in</h1>
        <p class="text-xs-center">
          <a href="/register">Need an account?</a>
        </p>

        <ul class="error-messages">
          <li>That email is already taken</li>
        </ul>

        <form>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="text" placeholder="Email" />
          </fieldset>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="password" placeholder="Password" />
          </fieldset>
          <button class="btn btn-lg btn-primary pull-xs-right">Sign in</button>
        </form>
      </div>
    </div>
  </div>
</div>
```

#### Register

```html
<div class="auth-page">
  <div class="container page">
    <div class="row">
      <div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Sign up</h1>
        <p class="text-xs-center">
          <a href="/login">Have an account?</a>
        </p>

        <ul class="error-messages">
          <li>That email is already taken</li>
        </ul>

        <form>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="text" placeholder="Username" />
          </fieldset>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="text" placeholder="Email" />
          </fieldset>
          <fieldset class="form-group">
            <input class="form-control form-control-lg" type="password" placeholder="Password" />
          </fieldset>
          <button class="btn btn-lg btn-primary pull-xs-right">Sign up</button>
        </form>
      </div>
    </div>
  </div>
</div>
```

### Profile

```html
<div class="profile-page">
  <div class="user-info">
    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-md-10 offset-md-1">
          <img src="http://i.imgur.com/Qr71crq.jpg" class="user-img" />
          <h4>Eric Simons</h4>
          <p>
            Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from
            the Hunger Games
          </p>
          <button class="btn btn-sm btn-outline-secondary action-btn">
            <i class="ion-plus-round"></i>
            &nbsp; Follow Eric Simons
          </button>
          <button class="btn btn-sm btn-outline-secondary action-btn">
            <i class="ion-gear-a"></i>
            &nbsp; Edit Profile Settings
          </button>
        </div>
      </div>
    </div>
  </div>

  <div class="container">
    <div class="row">
      <div class="col-xs-12 col-md-10 offset-md-1">
        <div class="articles-toggle">
          <ul class="nav nav-pills outline-active">
            <li class="nav-item">
              <a class="nav-link active" href="">My Articles</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="">Favorited Articles</a>
            </li>
          </ul>
        </div>

        <div class="article-preview">
          <div class="article-meta">
            <a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
            <div class="info">
              <a href="/profile/eric-simons" class="author">Eric Simons</a>
              <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
              <i class="ion-heart"></i> 29
            </button>
          </div>
          <a href="/article/how-to-buil-webapps-that-scale" class="preview-link">
            <h1>How to build webapps that scale</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            <ul class="tag-list">
              <li class="tag-default tag-pill tag-outline">realworld</li>
              <li class="tag-default tag-pill tag-outline">implementations</li>
            </ul>
          </a>
        </div>

        <div class="article-preview">
          <div class="article-meta">
            <a href="/profile/albert-pai"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
            <div class="info">
              <a href="/profile/albert-pai" class="author">Albert Pai</a>
              <span class="date">January 20th</span>
            </div>
            <button class="btn btn-outline-primary btn-sm pull-xs-right">
              <i class="ion-heart"></i> 32
            </button>
          </div>
          <a href="/article/the-song-you" class="preview-link">
            <h1>The song you won't ever stop singing. No matter how hard you try.</h1>
            <p>This is the description for the post.</p>
            <span>Read more...</span>
            <ul class="tag-list">
              <li class="tag-default tag-pill tag-outline">Music</li>
              <li class="tag-default tag-pill tag-outline">Song</li>
            </ul>
          </a>
        </div>

        <ul class="pagination">
          <li class="page-item active">
            <a class="page-link" href="">1</a>
          </li>
          <li class="page-item">
            <a class="page-link" href="">2</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
```

### Settings

```html
<div class="settings-page">
  <div class="container page">
    <div class="row">
      <div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Your Settings</h1>

        <ul class="error-messages">
          <li>That name is required</li>
        </ul>

        <form>
          <fieldset>
            <fieldset class="form-group">
              <input class="form-control" type="text" placeholder="URL of profile picture" />
            </fieldset>
            <fieldset class="form-group">
              <input class="form-control form-control-lg" type="text" placeholder="Your Name" />
            </fieldset>
            <fieldset class="form-group">
              <textarea
                class="form-control form-control-lg"
                rows="8"
                placeholder="Short bio about you"
              ></textarea>
            </fieldset>
            <fieldset class="form-group">
              <input class="form-control form-control-lg" type="text" placeholder="Email" />
            </fieldset>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="password"
                placeholder="New Password"
              />
            </fieldset>
            <button class="btn btn-lg btn-primary pull-xs-right">Update Settings</button>
          </fieldset>
        </form>
        <hr />
        <button class="btn btn-outline-danger">Or click here to logout.</button>
      </div>
    </div>
  </div>
</div>
```

### Create/Edit Article

```html
<div class="editor-page">
  <div class="container page">
    <div class="row">
      <div class="col-md-10 offset-md-1 col-xs-12">
        <ul class="error-messages">
          <li>That title is required</li>
        </ul>

        <form>
          <fieldset>
            <fieldset class="form-group">
              <input type="text" class="form-control form-control-lg" placeholder="Article Title" />
            </fieldset>
            <fieldset class="form-group">
              <input type="text" class="form-control" placeholder="What's this article about?" />
            </fieldset>
            <fieldset class="form-group">
              <textarea
                class="form-control"
                rows="8"
                placeholder="Write your article (in markdown)"
              ></textarea>
            </fieldset>
            <fieldset class="form-group">
              <input type="text" class="form-control" placeholder="Enter tags" />
              <div class="tag-list">
                <span class="tag-default tag-pill"> <i class="ion-close-round"></i> tag </span>
              </div>
            </fieldset>
            <button class="btn btn-lg pull-xs-right btn-primary" type="button">
              Publish Article
            </button>
          </fieldset>
        </form>
      </div>
    </div>
  </div>
</div>
```

### Article

// TODO : update to switch between follow/favorite AND edit/delete

```html
<div class="article-page">
  <div class="banner">
    <div class="container">
      <h1>How to build webapps that scale</h1>

      <div class="article-meta">
        <a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
        <div class="info">
          <a href="/profile/eric-simons" class="author">Eric Simons</a>
          <span class="date">January 20th</span>
        </div>
        <button class="btn btn-sm btn-outline-secondary">
          <i class="ion-plus-round"></i>
          &nbsp; Follow Eric Simons <span class="counter">(10)</span>
        </button>
        &nbsp;&nbsp;
        <button class="btn btn-sm btn-outline-primary">
          <i class="ion-heart"></i>
          &nbsp; Favorite Post <span class="counter">(29)</span>
        </button>
        <button class="btn btn-sm btn-outline-secondary">
          <i class="ion-edit"></i> Edit Article
        </button>
        <button class="btn btn-sm btn-outline-danger">
          <i class="ion-trash-a"></i> Delete Article
        </button>
      </div>
    </div>
  </div>

  <div class="container page">
    <div class="row article-content">
      <div class="col-md-12">
        <p>
          Web development technologies have evolved at an incredible clip over the past few years.
        </p>
        <h2 id="introducing-ionic">Introducing RealWorld.</h2>
        <p>It's a great solution for learning how other frameworks work.</p>
        <ul class="tag-list">
          <li class="tag-default tag-pill tag-outline">realworld</li>
          <li class="tag-default tag-pill tag-outline">implementations</li>
        </ul>
      </div>
    </div>

    <hr />

    <div class="article-actions">
      <div class="article-meta">
        <a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
        <div class="info">
          <a href="" class="author">Eric Simons</a>
          <span class="date">January 20th</span>
        </div>

        <button class="btn btn-sm btn-outline-secondary">
          <i class="ion-plus-round"></i>
          &nbsp; Follow Eric Simons
        </button>
        &nbsp;
        <button class="btn btn-sm btn-outline-primary">
          <i class="ion-heart"></i>
          &nbsp; Favorite Article <span class="counter">(29)</span>
        </button>
        <button class="btn btn-sm btn-outline-secondary">
          <i class="ion-edit"></i> Edit Article
        </button>
        <button class="btn btn-sm btn-outline-danger">
          <i class="ion-trash-a"></i> Delete Article
        </button>
      </div>
    </div>

    <div class="row">
      <div class="col-xs-12 col-md-8 offset-md-2">
        <form class="card comment-form">
          <div class="card-block">
            <textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea>
          </div>
          <div class="card-footer">
            <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
            <button class="btn btn-sm btn-primary">Post Comment</button>
          </div>
        </form>

        <div class="card">
          <div class="card-block">
            <p class="card-text">
              With supporting text below as a natural lead-in to additional content.
            </p>
          </div>
          <div class="card-footer">
            <a href="/profile/author" class="comment-author">
              <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
            </a>
            &nbsp;
            <a href="/profile/jacob-schmidt" class="comment-author">Jacob Schmidt</a>
            <span class="date-posted">Dec 29th</span>
          </div>
        </div>

        <div class="card">
          <div class="card-block">
            <p class="card-text">
              With supporting text below as a natural lead-in to additional content.
            </p>
          </div>
          <div class="card-footer">
            <a href="/profile/author" class="comment-author">
              <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
            </a>
            &nbsp;
            <a href="/profile/jacob-schmidt" class="comment-author">Jacob Schmidt</a>
            <span class="date-posted">Dec 29th</span>
            <span class="mod-options">
              <i class="ion-trash-a"></i>
            </span>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
```

---

## File: docs/src/content/docs/specifications/frontend/tests.md

---
title: Tests
---

Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!)

## E2E Tests

A shared [Playwright E2E test suite](https://github.com/realworld-apps/realworld/tree/main/specs/e2e) is available to validate your frontend implementation. It covers authentication, articles, comments, navigation, settings, social features, error handling, and even basic XSS security.

To make your implementation compatible with the E2E tests, it **must** follow the [selectors contract](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/SELECTORS.md), which defines:

- Form input `name` attributes
- Required CSS classes (layout, feed, tags, comments, profile, pagination, buttons, errors)
- Required text content for buttons and links
- Routes
- A debug interface (`window.__conduit_debug__`)
- LocalStorage key for the JWT token
- Default avatar behavior

### Running the tests

The test suite ships with a [base Playwright config](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/playwright.base.ts) that you can extend in your implementation. Override `baseURL` and `webServer` to point to your dev server.

See the [Angular implementation](https://github.com/realworld-apps/angular-realworld-example-app) for a working example.

---

## File: docs/src/content/docs/specifications/mobile-specs/introduction.md

---
title: Introduction
---

### [Icons for (iOS/Android)](https://github.com/realworld-apps/realworld/tree/master/spec/mobile_icons)

### Styles/Templates

Unfortunately, there isn't a common way for us to reuse & share styles/templates for cross-platform mobile apps.

Instead, we recommend using the Medium.com [iOS](https://itunes.apple.com/us/app/medium/id828256236?mt=8) and [Android](https://play.google.com/store/apps/details?id=com.medium.reader&hl=en) apps as a "north star" regarding general UI functionality/layout, but try not to go too overboard otherwise it will unnecessarily complicate your codebase (in other words, [KISS](https://en.wikipedia.org/wiki/KISS_principle) :)

---

## File: specs/api/README.md

# RealWorld API Spec

## Running API tests locally

### With Hurl

To locally run the provided [Hurl](https://hurl.dev) collection against your backend, execute:

```
HOST=http://localhost:3000/api ./run-api-tests-hurl.sh
```

For more details, see [`run-api-tests-hurl.sh`](run-api-tests-hurl.sh).

### With Bruno

A [Bruno](https://www.usebruno.com) collection is also available, automatically generated from the Hurl test suite. To run it:

```
HOST=http://localhost:3000/api ./run-api-tests-bruno.sh
```

For more details, see [`run-api-tests-bruno.sh`](run-api-tests-bruno.sh).

You can also open the `bruno/` folder directly in the Bruno app to run and inspect requests interactively.

> **Note:** The Hurl files are the source of truth. The Bruno collection is generated with `make bruno-generate` and kept in sync via CI (`make bruno-check`).

---

## File: specs/api/openapi.yml

openapi: 3.1.0
info:
  title: RealWorld Conduit API
  contact:
    name: RealWorld
    url: https://realworld-docs.netlify.app/
  license:
    name: MIT License
    url: https://opensource.org/licenses/MIT
  version: 2.0.0
tags:
  - name: Articles
  - name: Comments
  - name: Favorites
  - name: Profile
  - name: Tags
  - name: User and Authentication
servers:
  - url: https://api.realworld.show/api
paths:
  /users/login:
    post:
      tags:
        - User and Authentication
      summary: Existing user login
      operationId: Login
      requestBody:
        $ref: '#/components/requestBodies/LoginUserRequest'
      responses:
        '200':
          $ref: '#/components/responses/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/GenericError'
      x-codegen-request-body-name: body
  /users:
    post:
      tags:
        - User and Authentication
      operationId: CreateUser
      requestBody:
        $ref: '#/components/requestBodies/NewUserRequest'
      responses:
        '201':
          $ref: '#/components/responses/UserResponse'
        '409':
          $ref: '#/components/responses/ConflictError'
        '422':
          $ref: '#/components/responses/GenericError'
      x-codegen-request-body-name: body
  /user:
    get:
      tags:
        - User and Authentication
      summary: Get current user
      operationId: GetCurrentUser
      responses:
        '200':
          $ref: '#/components/responses/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
    put:
      tags:
        - User and Authentication
      summary: Update current user
      operationId: UpdateCurrentUser
      requestBody:
        $ref: '#/components/requestBodies/UpdateUserRequest'
      responses:
        '200':
          $ref: '#/components/responses/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
      x-codegen-request-body-name: body
  /profiles/{username}:
    get:
      tags:
        - Profile
      summary: Get a profile
      operationId: GetProfileByUsername
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/ProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
  /profiles/{username}/follow:
    post:
      tags:
        - Profile
      summary: Follow a user
      operationId: FollowUserByUsername
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/ProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
    delete:
      tags:
        - Profile
      summary: Unfollow a user
      operationId: UnfollowUserByUsername
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/ProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
  /articles/feed:
    get:
      tags:
        - Articles
      summary: Get recent articles from users you follow
        to limit. Auth is required
      operationId: GetArticlesFeed
      parameters:
        - $ref: '#/components/parameters/offsetParam'
        - $ref: '#/components/parameters/limitParam'
      responses:
        '200':
          $ref: '#/components/responses/MultipleArticlesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
  /articles:
    get:
      tags:
        - Articles
      summary: Get recent articles globally
        results. Auth is optional
      operationId: GetArticles
      parameters:
        - name: tag
          in: query
          schema:
            type: string
        - name: author
          in: query
          schema:
            type: string
        - name: favorited
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/offsetParam'
        - $ref: '#/components/parameters/limitParam'
      responses:
        '200':
          $ref: '#/components/responses/MultipleArticlesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/GenericError'
    post:
      tags:
        - Articles
      summary: Create an article
      operationId: CreateArticle
      requestBody:
        $ref: '#/components/requestBodies/NewArticleRequest'
      responses:
        '201':
          $ref: '#/components/responses/SingleArticleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/ConflictError'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
      x-codegen-request-body-name: article
  /articles/{slug}:
    get:
      tags:
        - Articles
      summary: Get an article
      operationId: GetArticle
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/SingleArticleResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
    put:
      tags:
        - Articles
      summary: Update an article
      operationId: UpdateArticle
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        $ref: '#/components/requestBodies/UpdateArticleRequest'
      responses:
        '200':
          $ref: '#/components/responses/SingleArticleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
      x-codegen-request-body-name: article
    delete:
      tags:
        - Articles
      summary: Delete an article
      operationId: DeleteArticle
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          $ref: '#/components/responses/EmptyOkResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
  /articles/{slug}/comments:
    get:
      tags:
        - Comments
      summary: Get comments for an article
      operationId: GetArticleComments
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/MultipleCommentsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
    post:
      tags:
        - Comments
      summary: Create a comment for an article
      operationId: CreateArticleComment
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        $ref: '#/components/requestBodies/NewCommentRequest'
      responses:
        '201':
          $ref: '#/components/responses/SingleCommentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
      x-codegen-request-body-name: comment
  /articles/{slug}/comments/{id}:
    delete:
      tags:
        - Comments
      summary: Delete a comment for an article
      operationId: DeleteArticleComment
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          $ref: '#/components/responses/EmptyOkResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
  /articles/{slug}/favorite:
    post:
      tags:
        - Favorites
      summary: Favorite an article
      operationId: CreateArticleFavorite
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/SingleArticleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
    delete:
      tags:
        - Favorites
      summary: Unfavorite an article
      operationId: DeleteArticleFavorite
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/SingleArticleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/GenericError'
      security:
        - Token: [ ]
  /tags:
    get:
      tags:
        - Tags
      summary: Get tags
      operationId: GetTags
      responses:
        '200':
          $ref: '#/components/responses/TagsResponse'
        '422':
          $ref: '#/components/responses/GenericError'
components:
  schemas:
    LoginUser:
      required:
        - email
        - password
      type: object
      properties:
        email:
          type: string
        password:
          type: string
          format: password
    NewUser:
      required:
        - email
        - password
        - username
      type: object
      properties:
        username:
          type: string
        email:
          type: string
        password:
          type: string
          format: password
    User:
      required:
        - bio
        - email
        - image
        - token
        - username
      type: object
      properties:
        email:
          type: string
        token:
          type: string
        username:
          type: string
        bio:
          type:
            - string
            - 'null'
        image:
          type:
            - string
            - 'null'
    UpdateUser:
      type: object
      properties:
        email:
          type: string
        password:
          type: string
        username:
          type: string
        bio:
          type:
            - string
            - 'null'
        image:
          type:
            - string
            - 'null'
    Profile:
      required:
        - bio
        - following
        - image
        - username
      type: object
      properties:
        username:
          type: string
        bio:
          type:
            - string
            - 'null'
        image:
          type:
            - string
            - 'null'
        following:
          type: boolean
    Article:
      required:
        - author
        - body
        - createdAt
        - description
        - favorited
        - favoritesCount
        - slug
        - tagList
        - title
        - updatedAt
      type: object
      properties:
        slug:
          type: string
        title:
          type: string
          type: string
        body:
          type: string
        tagList:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        favorited:
          type: boolean
        favoritesCount:
          type: integer
        author:
          $ref: '#/components/schemas/Profile'
    NewArticle:
      required:
        - body
        - description
        - title
      type: object
      properties:
        title:
          type: string
          type: string
        body:
          type: string
        tagList:
          type: array
          items:
            type: string
    UpdateArticle:
      type: object
      properties:
        title:
          type: string
          type: string
        body:
          type: string
        tagList:
          type: array
          items:
            type: string
    Comment:
      required:
        - author
        - body
        - createdAt
        - id
        - updatedAt
      type: object
      properties:
        id:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        body:
          type: string
        author:
          $ref: '#/components/schemas/Profile'
    NewComment:
      required:
        - body
      type: object
      properties:
        body:
          type: string
    GenericErrorModel:
      required:
        - errors
      type: object
      properties:
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
  responses:
    TagsResponse:
      content:
        application/json:
          schema:
            required:
              - tags
            type: object
            properties:
              tags:
                type: array
                items:
                  type: string
    SingleCommentResponse:
      content:
        application/json:
          schema:
            required:
              - comment
            type: object
            properties:
              comment:
                $ref: '#/components/schemas/Comment'
    MultipleCommentsResponse:
      content:
        application/json:
          schema:
            required:
              - comments
            type: object
            properties:
              comments:
                type: array
                items:
                  $ref: '#/components/schemas/Comment'
    SingleArticleResponse:
      content:
        application/json:
          schema:
            required:
              - article
            type: object
            properties:
              article:
                $ref: '#/components/schemas/Article'
    MultipleArticlesResponse:
      content:
        application/json:
          schema:
            required:
              - articles
              - articlesCount
            type: object
            properties:
              articles:
                type: array
                items:
                  required:
                    - author
                    - createdAt
                    - description
                    - favorited
                    - favoritesCount
                    - slug
                    - tagList
                    - title
                    - updatedAt
                  type: object
                  properties:
                    slug:
                      type: string
                    title:
                      type: string
                      type: string
                    tagList:
                      type: array
                      items:
                        type: string
                    createdAt:
                      type: string
                      format: date-time
                    updatedAt:
                      type: string
                      format: date-time
                    favorited:
                      type: boolean
                    favoritesCount:
                      type: integer
                    author:
                      $ref: '#/components/schemas/Profile'
              articlesCount:
                type: integer
    ProfileResponse:
      content:
        application/json:
          schema:
            required:
              - profile
            type: object
            properties:
              profile:
                $ref: '#/components/schemas/Profile'
    UserResponse:
      content:
        application/json:
          schema:
            required:
              - user
            type: object
            properties:
              user:
                $ref: '#/components/schemas/User'
    EmptyOkResponse:
      content: { }
    Unauthorized:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GenericErrorModel'
          example:
            errors:
              token:
                - "is missing"
    ConflictError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GenericErrorModel'
          example:
            errors:
              username:
                - "has already been taken"
    Forbidden:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GenericErrorModel'
          example:
            errors:
              resource:
                - "forbidden"
    NotFound:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GenericErrorModel'
          example:
            errors:
              resource:
                - "not found"
    GenericError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GenericErrorModel'
          example:
            errors:
              title:
                - "can't be blank"
  requestBodies:
    LoginUserRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - user
            type: object
            properties:
              user:
                $ref: '#/components/schemas/LoginUser'
    NewUserRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - user
            type: object
            properties:
              user:
                $ref: '#/components/schemas/NewUser'
    UpdateUserRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - user
            type: object
            properties:
              user:
                $ref: '#/components/schemas/UpdateUser'
    NewArticleRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - article
            type: object
            properties:
              article:
                $ref: '#/components/schemas/NewArticle'
    UpdateArticleRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - article
            type: object
            properties:
              article:
                $ref: '#/components/schemas/UpdateArticle'
    NewCommentRequest:
      required: true
      content:
        application/json:
          schema:
            required:
              - comment
            type: object
            properties:
              comment:
                $ref: '#/components/schemas/NewComment'
  parameters:
    offsetParam:
      in: query
      name: offset
      required: false
      schema:
        type: integer
        minimum: 0
    limitParam:
      in: query
      name: limit
      required: false
      schema:
        type: integer
        minimum: 1
        default: 20
  securitySchemes:
    Token:
      type: apiKey
        \ a a valid JWT token after registering or logging in. This JWT token must\
        \ then be used for all protected resources by passing it in via the 'Authorization'\
        \ header.\n\nA JWT token is generated by the API by either registering via\
        \ /users or logging in via /users/login.\n\nThe following format must be in\
        \ the 'Authorization' header :\n\n    Token xxxxxx.yyyyyyy.zzzzzz\n    \n"
      name: Authorization
      in: header

---

## File: specs/api/bruno/bruno.json

{
  "version": "1",
  "name": "RealWorld API",
  "type": "collection"
}

---

## File: specs/e2e/SELECTORS.md

# RealWorld E2E Test Selectors Contract

This document lists every CSS class, HTML attribute, text label, route, and
interface that the shared e2e tests depend on. Any RealWorld implementation
that wants to use these tests **must** provide all of the selectors below.

---

## Form Inputs (`name` attributes)

Inputs are located via the standard HTML `name` attribute.

| Selector                                     | Page                      |
| -------------------------------------------- | ------------------------- |
| `input[name="username"]`                     | Register, Settings        |
| `input[name="email"]`                        | Login, Register, Settings |
| `input[name="password"]`                     | Login, Register, Settings |
| `input[name="title"]`                        | Editor                    |
| `input[name="description"]`                  | Editor                    |
| `textarea[name="body"]`                      | Editor                    |
| `input[name="image"]`                        | Settings                  |
| `textarea[name="bio"]`                       | Settings                  |
| `input[placeholder="Enter tags"]`            | Editor                    |
| `textarea[placeholder="Write a comment..."]` | Article detail            |

---

## CSS Classes

### Layout & Navigation

| Class           | Element | Purpose               |
| --------------- | ------- | --------------------- |
| `.navbar`       | `nav`   | Main navigation bar   |
| `.navbar-brand` | `a`     | Logo / site name link |
| `.nav-link`     | `a`     | Navigation tab links  |
| `.banner`       | `div`   | Home page hero banner |
| `.container`    | `div`   | Page width container  |

### Feed & Articles

| Class                 | Element | Purpose                             |
| --------------------- | ------- | ----------------------------------- |
| `.feed-toggle`        | `div`   | Global Feed / Your Feed tab bar     |
| `.article-preview`    | `div`   | Article card in a feed list         |
| `.article-meta`       | `div`   | Author avatar + name + date         |
| `.article-content`    | `div`   | Rendered article body               |
| `.article-page`       | `div`   | Article detail page wrapper         |
| `.preview-link`       | `a`     | Clickable link wrapping the preview |
| `.author`             | `a`     | Author name in article meta         |
| `.empty-feed-message` | `div`   | "No articles here" placeholder      |

### Tags

| Class          | Element | Purpose                 |
| -------------- | ------- | ----------------------- |
| `.sidebar`     | `div`   | Home page sidebar       |
| `.tag-list`    | `div`   | Container for tag pills |
| `.tag-default` | `span`  | Base tag class          |
| `.tag-pill`    | `span`  | Pill-shaped tag         |

### Comments

| Class                 | Element | Purpose                                   |
| --------------------- | ------- | ----------------------------------------- |
| `.card`               | `div`   | Comment card (also wraps `.comment-form`) |
| `.card-block`         | `div`   | Comment text body                         |
| `.comment-form`       | `div`   | Comment input form card                   |
| `.comment-author-img` | `img`   | Commenter's avatar                        |
| `.mod-options`        | `span`  | Delete button container                   |
| `.ion-trash-a`        | `i`     | Delete icon (inside `.mod-options`)       |

Tests select posted comments with `.card:not(.comment-form) .card-block`.

### Profile

| Class           | Element | Purpose                       |
| --------------- | ------- | ----------------------------- |
| `.profile-page` | `div`   | Profile page wrapper          |
| `.user-info`    | `div`   | Username, bio, avatar section |
| `.user-img`     | `img`   | Large avatar on profile page  |
| `.user-pic`     | `img`   | Small avatar in the navbar    |

### Pagination

| Class         | Element   | Purpose                        |
| ------------- | --------- | ------------------------------ |
| `.pagination` | `nav/div` | Pagination container           |
| `.page-item`  | `li/div`  | Individual page button wrapper |

### Buttons

| Class                  | Element  | Purpose                        |
| ---------------------- | -------- | ------------------------------ |
| `.btn-outline-primary` | `button` | Favorite (not yet favorited)   |
| `.btn-primary`         | `button` | Unfavorite (already favorited) |
| `.btn-outline-danger`  | `button` | Destructive action (logout)    |

### Errors

| Class             | Element | Purpose                     |
| ----------------- | ------- | --------------------------- |
| `.error-messages` | `ul`    | Validation / API error list |

---

## Required Text Content

Buttons and links are located by visible text via `:has-text()`.

### Buttons

| Text                      | Element  | Context                  |
| ------------------------- | -------- | ------------------------ |
| `Post Comment`            | `button` | Article detail           |
| `Delete Article`          | `button` | Article detail           |
| `Publish Article`         | `button` | Editor                   |
| `Update Settings`         | `button` | Settings                 |
| `Or click here to logout` | `button` | Settings                 |
| `Follow` / `Unfollow`     | `button` | Profile, article meta    |
| `Favorite` / `Unfavorite` | `button` | Article detail           |
| `Favorite Article`        | `button` | Article detail (variant) |

### Headings & Links

| Text                    | Element      | Context               |
| ----------------------- | ------------ | --------------------- |
| `Sign in`               | `h1`         | Login page heading    |
| `Sign up`               | `h1`         | Register page heading |
| `Global Feed`           | `a.nav-link` | Home feed toggle      |
| `Your Feed`             | `a.nav-link` | Home feed toggle      |
| `Favorited`             | `a`          | Profile tab           |
| `Edit Article`          | `a`          | Article detail        |
| `Edit Profile Settings` | `a`          | Profile page          |
| `Home`                  | `a.nav-link` | Navbar                |

---

## Routes

### Pages

| Route                          | Page                      |
| ------------------------------ | ------------------------- |
| `/`                            | Home / Global Feed        |
| `/?feed=following`             | Your Feed                 |
| `/?page=N`                     | Paginated feed            |
| `/tag/:tag`                    | Filtered by tag           |
| `/tag/:tag?page=N`             | Paginated tag view        |
| `/login`                       | Login                     |
| `/register`                    | Register                  |
| `/editor`                      | New article               |
| `/editor/:slug`                | Edit article              |
| `/settings`                    | User settings             |
| `/profile/:username`           | User profile              |
| `/profile/:username/favorites` | User's favorited articles |
| `/article/:slug`               | Article detail            |

### API Endpoints (used in route interception)

| Endpoint                           | Methods          |
| ---------------------------------- | ---------------- |
| `/api/articles*`                   | GET              |
| `/api/articles/:slug`              | GET, PUT, DELETE |
| `/api/articles/:slug/comments`     | GET, POST        |
| `/api/articles/:slug/comments/:id` | DELETE           |
| `/api/articles/:slug/favorite`     | POST, DELETE     |
| `/api/users`                       | POST (register)  |
| `/api/users/login`                 | POST             |
| `/api/user`                        | GET, PUT         |
| `/api/profiles/:username`          | GET              |
| `/api/profiles/:username/follow`   | POST, DELETE     |
| `/api/tags`                        | GET              |

---

## Debug Interface (`window.__conduit_debug__`)

Implementations **must** expose this on `window.__conduit_debug__`:

```typescript
interface ConduitDebug {
  getToken(): string | null;
  getAuthState(): 'authenticated' | 'unauthenticated' | 'unavailable' | 'loading';
  getCurrentUser(): { username: string; email: string; bio: string | null; image: string | null; token: string } | null;
}
```

See `helpers/debug.ts` for the full contract and implementation guide.

---

## LocalStorage

| Key        | Value      | Purpose              |
| ---------- | ---------- | -------------------- |
| `jwtToken` | JWT string | Authentication token |

---

## Default Avatar

When a user's `image` is `null` or empty, the `src` attribute of avatar
images (`.user-img`, `.user-pic`, `.comment-author-img`, `.article-meta img`)
must contain `default-avatar.svg`.

---

## Button State Conventions

- **Favorite**: Uses `.btn-outline-primary` when not favorited, switches to `.btn-primary` when favorited.
- **Follow**: Button text toggles between `Follow {username}` and `Unfollow {username}`.
- **Pagination active**: The current page's `.page-item` has the CSS class `active`.

---

## File: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: github-actions
    directory: '/'
    schedule:
      interval: weekly
    open-pull-requests-limit: 10

  - package-ecosystem: npm
    directory: '/'
    schedule:
      interval: weekly
    open-pull-requests-limit: 10

---

## File: .github/ISSUE_TEMPLATE/BUG_REPORT.yml

name: 🐞 Bug report
title: '[Bug]: '
labels:
  - bug
body:
  - type: dropdown
    attributes:
      label: Relevant scope
      options:
        - Frontend specs
        - Backend specs
        - Deployed demo
        - 'Other: describe below'
    validations:
      required: true
  - type: textarea
    attributes:
      label: Description
    validations:
      required: true
  - type: markdown
    attributes:
      value: >-
        This template was generated with [Issue Forms
        Creator](https://www.issue-forms-creator.app/)

---

## File: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml

name: 🚀 Feature request
title: '[Feature Request]:'
body:
  - type: markdown
    attributes:
      value: '# Feature Request'
  - type: dropdown
    attributes:
      label: Relevant Scope
      options:
        - Frontend specs
        - Backend specs
        - 'Other: describe below'
    validations:
      required: true
  - type: textarea
    attributes:
      label: Description
    validations:
      required: true
  - type: textarea
    attributes:
      label: Describe the solution you'd like
  - type: textarea
    attributes:
      label: Describe alternatives you've considered
  - type: markdown
    attributes:
      value: >-
        This template was generated with [Issue Forms
        Creator](https://www.issue-forms-creator.app/)

---

## File: .github/workflows/bruno-check.yml

name: 'Bruno Check'

on:
  push:
  pull_request:

jobs:
  bruno-check:
    name: Verify Bruno collection is up-to-date
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Check Bruno collection is up-to-date
        run: make bruno-check

---

## File: .github/workflows/codeql.yml

name: 'CodeQL'

on:
  workflow_dispatch:
  schedule:
    - cron: '24 3 * * 3'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: ['javascript']

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v4
        with:
          languages: ${{ matrix.language }}

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v4
        with:
          category: '/language:${{matrix.language}}'

---

## File: .github/workflows/deploy-docs.yml

name: Deploy Documentation

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - '.github/workflows/deploy-docs.yml'
  workflow_dispatch:  # allow manual trigger

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Setup Pages
        uses: actions/configure-pages@v6

      - name: Install dependencies
        run: bun install
        working-directory: ./docs

      - name: Build documentation
        run: bun run build
        working-directory: ./docs

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v5
        with:
          path: './docs/dist'

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5

---

## File: .github/workflows/spammy-guardian.yml

name: Spammy Guardian
on:
  workflow_dispatch:
    inputs:
      issueId:
        required: true
  issue_comment:
  issues:
    types: [opened]
jobs:
  spammy-guardian:
    runs-on: ubuntu-latest
    if: ${{ github.actor != 'dependabot[bot]' || github.actor != 'netlify[bot]' }}
    steps:
      - uses: kerhub/spammy-guardian@fa79bcda24df6dae5b93285e1749e59c77add4bd
        with:
          token: ${{ secrets.GITHUB_TOKEN }}