# Nolorem Public Content API -- reference consumer

A worked example, not only a spec. `sync.mjs` is a single dependency-free Node
script (plain `fetch`, nothing else) that talks to the [Nolorem Public Content
API](https://nolorem.io/support/settings/public-content-api) the same way a
real integration would: paginated sync, cheap polling, and image copying.

This directory has no `package.json` and no lockfile, and must never gain
one. There is nothing to `npm install`. Copy this folder into your own
project and run it as-is.

## Requirements

- Node.js 18 or later (for the global `fetch`; nothing else is used beyond
  `node:fs/promises`, `node:path` and `node:url`).
- A Nolorem API key. Create one under **Settings, API Keys** in your Nolorem
  account -- the Content API is available on every paid plan, including a
  trial. See the [Publishing to your own
  site](https://nolorem.io/support/blog/publish-to-your-own-site) article for
  the full day-one setup walkthrough, or the [Public Content
  API](https://nolorem.io/support/settings/public-content-api) reference for
  every endpoint and query parameter this script uses.

## Configuration

Read from the environment. **Never commit a real key** -- not to this
directory, not anywhere.

```bash
export NOLOREM_API_KEY="nlr_live_your_key_here"
export NOLOREM_BASE_URL="https://nolorem.io"   # or http://localhost:3000 against a dev server
export NOLOREM_BLOG="your-blog-slug"           # optional; scopes every request to one blog
```

## The two consumption models

The Content API supports two different ways of consuming it, and this script
demonstrates both.

### 1. Fetch-on-render (default)

```bash
node sync.mjs
```

This is the model a customer's own site uses when it calls `/api/v1/posts`
directly, on every page render, with no local database of its own -- exactly
the situation a bespoke site is in, since building a store just to remember
"since when" is the CMS the customer does not want to run. `If-None-Match`
makes that cheap: the script fetches the feed, stores the `ETag` it gets
back, and sends it as `If-None-Match` on the next run.

Run the script twice in a row and the second run gets a `304 Not Modified`
with no body. Nothing changed, so the server skipped the row fetch and the
per-post link resolution entirely, and the script reuses the response it
already has.

### 2. Pull-and-store

```bash
node sync.mjs --sync
```

This is the model a connector -- like the [Drupal
connector](https://nolorem.io/support/blog/drupal-connector) -- uses: page
through the whole feed with `per_page=100`, apply `updated_since` on every
run after the first, and track a cursor. It also demonstrates the tombstone
contract (`include_deleted=true`): unpublished or deleted posts are appended
to `data` as minimal tombstone entries (`{ id, status, updated_at, deleted
}`), served once on page 1 of a sync and never repeated on later pages, so a
multi-page sync applies the full tombstone set exactly once.

The cursor and the last-seen `ETag` are both stored in
`.nolorem-sync-state.json` next to this script -- per-machine, disposable
state, listed in `.gitignore` so it is never committed.

Add `--dry-run` to either mode to make real requests without writing
anything to disk -- useful for a first connectivity check.

## Copying images

```bash
node sync.mjs --copy-images <post-id-or-slug>
```

Fetches one post, downloads every file listed in its `images[]` array (not
only the featured image), and rewrites the served `html` so every `<img>`
whose `data-nolorem-image-id` matches now points at the local file. Output
lands in `./downloaded-images/`.

### The three image options, and why this script does the third

**1. Do nothing (the default).** Nolorem keeps hosting your images on its own
public storage bucket. `featured_image_url` and every `images[].url` already
point there, and that works with zero configuration -- most customers never
need to touch this.

**2. Declare an asset base URL (an origin swap).** If you mirror Nolorem's
storage bucket under your own domain, at the identical object path, set that
address as the blog's asset base URL (Publishing settings, "My own site"
destination). Nolorem then rewrites every image URL in the API response --
`featured_image_url`, every `images[].url`, and every `<img src>` in `html`
-- to your domain instead of its own, with no other code changes on either
side. This is a straight string-prefix substitution, never a guessed path
pattern: Nolorem will not derive a filename for you, because a filename is
decided by your own storage at write time and guessing it is exactly what
produced a 404 the first time this project tried it.

**3. Copy the files yourself (what this script does, and the recommended
production pattern).** Download every image and store it under your own
filenames, in your own storage. This is the pattern this script demonstrates,
and it is the one worth building for two reasons: Nolorem has no backup of
any kind today, so a customer's live site should not have Nolorem's storage
bucket sitting in its critical path; and a vendor domain baked into your
published `<img src>` tags is a dependency you carry even after you leave
Nolorem. Copying costs one download per image, once, and after that your
site owns its own media, mirroring the [Drupal
connector](https://nolorem.io/support/blog/drupal-connector)'s own choice --
it downloads every featured image into Drupal's own file system rather than
hotlinking Nolorem's bucket.

## Rate limits

Every key is limited to 60 requests per minute and 5,000 per day, the same
for every plan. A `429` response carries a `Retry-After` header; this script
prints it and exits rather than retrying blindly. A production integration
should back off and retry using that value.

## Error handling

The script surfaces the API's own error messages rather than a raw stack
trace: a missing or revoked key (`401`), an organisation with no active
subscription or a key missing the `posts:read` scope (`403`), and rate
limiting (`429`, with `Retry-After`). See the [Public Content
API](https://nolorem.io/support/settings/public-content-api) article's Error
Codes section for the full list.
