> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usenotra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Caching

> Cache paginated Notra post data safely, including sorting and invalidation patterns.

When caching Notra post data, include every query input that changes the result set in the cache key.

For `list posts`, that usually means:

* `page`
* `limit`
* `sort`
* `status`
* `contentType`

## Cache key design

Treat each unique list query as its own cache entry.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
type ListPostsParams = {
  page?: number;
  limit?: number;
  sort?: "asc" | "desc";
  status?: Array<"draft" | "published">;
  contentType?: Array<
    "changelog" | "linkedin_post" | "twitter_post" | "blog_post"
  >;
};

function createPostsCacheKey(params: ListPostsParams) {
  return [
    "notra",
    "posts",
    params.page ?? 1,
    params.limit ?? 10,
    params.sort ?? "desc",
    [...(params.status ?? [])].sort().join(","),
    [...(params.contentType ?? [])].sort().join(","),
  ] as const;
}
```

Sorting array filters before building the key helps avoid duplicate cache entries for equivalent queries.

## Recommended strategy

Use separate caches for:

* post lists, keyed by pagination and filters
* individual posts, keyed by `postId`

This keeps list pages independent while still letting you refresh a single post after `PATCH /v1/posts/{postId}`.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const listKey = createPostsCacheKey({
  page: 2,
  limit: 20,
  sort: "desc",
  status: ["published"],
  contentType: ["blog_post"],
});

const postKey = ["notra", "post", "post_abc"] as const;
```

## Invalidation rules

Invalidate caches whenever the underlying list order or membership can change.

### After a post update

After `PATCH /v1/posts/{postId}` succeeds:

* invalidate the individual post cache for that `postId`
* invalidate all cached post lists, because title, status, and updated content can affect what users should see

### After a new generated post becomes available

When a generation job finishes and a new post is created:

* invalidate all cached post lists
* optionally prefetch page 1 again if your UI shows newest posts first with `sort=desc`

### After a delete

After deleting a post:

* remove the individual post cache
* invalidate all cached post lists so pagination totals and offsets stay correct

## Example with TanStack Query

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  QueryClient,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

function usePosts(params: ListPostsParams) {
  return useQuery({
    queryKey: createPostsCacheKey(params),
    queryFn: async () => {
      const search = new URLSearchParams({
        page: String(params.page ?? 1),
        limit: String(params.limit ?? 10),
        sort: params.sort ?? "desc",
      });

      for (const status of params.status ?? []) search.append("status", status);
      for (const type of params.contentType ?? []) {
        search.append("contentType", type);
      }

      const response = await fetch(
        `https://api.usenotra.com/v1/posts?${search.toString()}`,
        {
          headers: {
            Authorization: `Bearer ${process.env.NOTRA_API_KEY}`,
          },
        }
      );

      if (!response.ok) throw new Error("Failed to fetch posts");
      return response.json();
    },
  });
}

function useUpdatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({
      postId,
      updates,
    }: {
      postId: string;
      updates: { title?: string; markdown?: string; status?: "draft" | "published" };
    }) => {
      const response = await fetch(`https://api.usenotra.com/v1/posts/${postId}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.NOTRA_API_KEY}`,
        },
        body: JSON.stringify(updates),
      });

      if (!response.ok) throw new Error("Failed to update post");
      return response.json();
    },
    onSuccess: (_data, { postId }) => {
      queryClient.invalidateQueries({ queryKey: ["notra", "posts"] });
      queryClient.invalidateQueries({ queryKey: ["notra", "post", postId] });
    },
  });
}
```

## React and Next.js notes

If you use React Server Components or Next.js, React's `cache()` function can help dedupe repeated reads during a single render on the server.

Use it for read paths, but do not rely on it alone for mutation invalidation. Pair it with route-level revalidation, cache tags, or your client cache invalidation strategy after updates, deletes, or completed generation jobs.

## Practical defaults

* Use a short TTL for list caches if content changes frequently.
* Use explicit invalidation after `update`, `delete`, and successful generation completion.
* Prefer `sort=desc` plus page-1 refetch if your UI is a newest-first feed.
* Cache individual posts separately from paginated lists.
