Skip to main content

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.

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.
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. 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}.
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

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.
Last modified on May 2, 2026