← Files WixARCHIVED FILE

skills/wix-headless-templates/blog/rest/taxonomy.ts

3.72 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// Category + tag reads over REST — the twin of app/wix/blog/taxonomy.ts. Same exports, same DTOs;
// the mappers come from taxonomy-core (the SAME file the SDK transport uses, deployed flat next to
// this one). Taxonomy queries page by OFFSET (`paging`), unlike posts (`cursorPaging`); a blog's
// taxonomy fits in one read of 100. All calls are visitor-safe.
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/category/query-categories.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/category/get-category-by-slug.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/tags/query-tags.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/blog/tags/get-tag-by-slug.md
import { wixRequest } from "./client.js";
import { imgSrc } from "./media.js";
import { TAG_SORT, TAXONOMY_LIMIT, categoriesQuery, inIdOrder, tagsByIdsQuery, toCategory, toTag, type FetchCategoriesOptions } from "./taxonomy-core.js";
import type { Raw } from "./posts-core.js";
import type { BlogCategory, BlogTag } from "./types.js";

export { inIdOrder, type FetchCategoriesOptions };

/**
 * Categories in menu order; `withPosts` drops empty ones at the source. Non-fatal: [] on failure
 * (the feed renders without its filter row).
 * POST /blog/v3/categories/query  { query: { filter?: { postCount: { $gt: 0 } }, sort: [{ fieldName: "displayPosition", order: "ASC" }], paging: { limit: 100 } } }
 */
export async function fetchBlogCategories(o: FetchCategoriesOptions = {}): Promise<BlogCategory[]> {
  try {
    const res = await wixRequest<Raw>("/blog/v3/categories/query", { body: { query: categoriesQuery(o) } });
    return ((res?.categories ?? []) as Raw[]).map((c) => toCategory(c, imgSrc)).filter((c) => c.id);
  } catch {
    return [];
  }
}

/**
 * Tags, most-published-posts first — a tag cloud. Non-fatal: [] on failure.
 * POST /blog/v3/tags/query  { query: { sort: [{ fieldName: "publishedPostCount", order: "DESC" }], paging: { limit: 100 } } }
 */
export async function fetchBlogTags(): Promise<BlogTag[]> {
  try {
    const res = await wixRequest<Raw>("/blog/v3/tags/query", { body: { query: { sort: TAG_SORT, paging: { limit: TAXONOMY_LIMIT } } } });
    return ((res?.tags ?? []) as Raw[]).map(toTag).filter((t) => t.id);
  } catch {
    return [];
  }
}

/**
 * A post's tags by id, in the post's own order. Non-fatal: [] on failure.
 * POST /blog/v3/tags/query  { query: { filter: { id: { $in: ids } }, paging: { limit: 30 } } }
 */
export async function fetchTagsByIds(ids: readonly string[]): Promise<BlogTag[]> {
  if (!ids.length) return [];
  try {
    const res = await wixRequest<Raw>("/blog/v3/tags/query", { body: { query: tagsByIdsQuery([...ids]) } });
    return inIdOrder(ids, ((res?.tags ?? []) as Raw[]).map(toTag).filter((t) => t.id));
  } catch {
    return [];
  }
}

// The two by-slug getters answer in DIFFERENT envelopes ({ category } and { tag }); a missing slug
// is a 404 — mapped to null here, as the SDK transport does.

/** One category by its URL slug. Null when not found.  GET /blog/v3/categories/slugs/{slug} */
export async function fetchCategoryBySlug(slug: string): Promise<BlogCategory | null> {
  try {
    const res = await wixRequest<Raw>(`/blog/v3/categories/slugs/${encodeURIComponent(slug)}`, { method: "GET" });
    return res?.category ? toCategory(res.category, imgSrc) : null;
  } catch {
    return null;
  }
}

/** One tag by its URL slug. Null when not found.  GET /blog/v3/tags/slugs/{slug} */
export async function fetchTagBySlug(slug: string): Promise<BlogTag | null> {
  try {
    const res = await wixRequest<Raw>(`/blog/v3/tags/slugs/${encodeURIComponent(slug)}`, { method: "GET" });
    return res?.tag ? toTag(res.tag) : null;
  } catch {
    return null;
  }
}

SHA-256: 56252b2d0ca4bf2e6540127e55e2ea95d589d245f52cf2de68e73f9fe844c96a