LLM Reference

LLM Reference: Content Rendering

This is a dense reference for LLMs working with Vextro’s frontend content rendering APIs. Import from vextro/content. Astro components are available as direct file imports.

Import Path

import {
  renderTipTapToHtml,
  extractPlainText,
  buildSrcSet,
  buildImageSizes,
  focalPointToObjectPosition,
  groupVariantsByFormat,
  hasModernFormat,
  getLargestVariant,
  getSmallestVariant,
} from 'vextro/content';

Types

type TipTapDoc = { type: 'doc'; content: TipTapNode[] };
type TipTapNode = {
  type: string;
  attrs?: Record<string, unknown>;
  content?: TipTapNode[];
  marks?: TipTapMark[];
  text?: string;
};
type TipTapMark = { type: string; attrs?: Record<string, unknown> };

// Accepted by renderTipTapToHtml and extractPlainText
type RichTextContent = TipTapDoc | string | null | undefined;

// Minimal file record shape accepted by image utilities and VextroImage
type VextroImageSource = {
  url: string;
  focalX?: number;
  focalY?: number;
  sizes?: ImageVariant[];
};

type ImageVariant = {
  name: string;
  url: string;
  width: number;
  height: number;
  mimeType: string;
  filesize: number;
};

// Block types
type BlockReference = { blockType: string; blockId: string; order?: number };
type ResolvedBlock = BlockReference & { data: Record<string, unknown> };
type BlockRenderer = (props: { block: ResolvedBlock; [key: string]: unknown }) => unknown;
type ContentField = { type: 'richText' | 'blocks' | 'html'; value: unknown };

Rich Text Functions

renderTipTapToHtml(content: RichTextContent): string

Converts TipTap JSON to semantic HTML. Handles all standard node types: paragraph, heading (h1–h6), bulletList, orderedList, listItem, blockquote, table, tableRow, tableCell, tableHeader, codeBlock, hardBreak, horizontalRule, and custom vextroBlock. Returns "" for null/undefined.

const html = renderTipTapToHtml(post.body);

extractPlainText(content: RichTextContent): string

Strips all markup; block elements are followed by \n. Suitable for excerpts and search index entries.

const excerpt = extractPlainText(post.body).slice(0, 160);

Image Utility Functions

buildSrcSet(variants: ImageVariant[] | undefined, options?: { format?: string }): string

Returns an srcset attribute string from an array of variants. Optionally filter to a single MIME type string (e.g. "webp", "avif").

const srcset = buildSrcSet(file.sizes, { format: 'webp' });
// "image-800.webp 800w, image-1200.webp 1200w"

buildImageSizes(breakpoints: Record<string, string>): string

Returns a sizes attribute string. The default key provides the fallback value (appended last, without a media query).

const sizes = buildImageSizes({
  '(min-width: 1024px)': '50vw',
  default: '100vw',
});

focalPointToObjectPosition(focalX?: number, focalY?: number): string

Converts 0–1 focal point coordinates to a CSS object-position percentage string.

focalPointToObjectPosition(0.3, 0.7) // "30% 70%"

groupVariantsByFormat(variants: ImageVariant[] | undefined): Map<string, ImageVariant[]>

Groups variants by mimeType. Returns an empty Map if variants is undefined.

hasModernFormat(variants: ImageVariant[] | undefined): boolean

Returns true if any variant has mimeType containing "webp" or "avif".

getLargestVariant(source: VextroImageSource): string

Returns the URL of the variant with the greatest width. Falls back to source.url.

getSmallestVariant(source: VextroImageSource): string

Returns the URL of the variant with the smallest width. Falls back to source.url.


Astro Components

VextroImage.astro

import VextroImage from 'vextro/content/VextroImage.astro';

Renders <picture> with AVIF and WebP <source> elements and a fallback <img>. Applies object-position from focalX/focalY.

Props:

  • source: VextroImageSource — file record
  • alt: string — required
  • sizes?: string — HTML sizes attribute
  • width?: number, height?: number — explicit dimensions
  • loading?: "lazy" | "eager"
  • fetchpriority?: "high" | "low" | "auto"
  • class?: string — on <picture>
  • imgClass?: string — on <img>
  • focalX?: number, focalY?: number — override focal point (0–1)

VextroRichText.astro

import VextroRichText from 'vextro/content/VextroRichText.astro';

Wraps renderTipTapToHtml output in a semantic element. Renders nothing if content is empty.

Props:

  • content: RichTextContent — TipTap JSON or HTML string
  • class?: string
  • as?: "div" | "article" | "section" | "main" | "aside" — default "div"

VextroBlocks.astro

import VextroBlocks from 'vextro/content/VextroBlocks.astro';

Maps ResolvedBlock[] to renderer components by blockType. Unknown types show a dev-only placeholder (hidden in production).

Props:

  • blocks: ResolvedBlock[] — resolved blocks with data populated
  • components: Record<string, Component> — blockType → Astro component (plain object)
  • class?: string
  • as?: string — wrapper element tag, default "div"

VextroContent.astro

import VextroContent from 'vextro/content/VextroContent.astro';

Unified renderer for mixed content sequences (richText → blocks → richText patterns).

Props:

  • sections: ContentField[] — array of { type, value } sections
  • blockComponents?: Record<string, Component> — blockType → Astro component (plain object)
  • class?: string
  • richTextClass?: string
  • blocksClass?: string
  • as?: string

Block Resolution Pattern

Blocks are stored as references (blockType + blockId). Before passing to VextroBlocks, resolve each reference by loading its data from Convex:

// In your Astro page (server-side)
const rawBlocks: BlockReference[] = post.blocks ?? [];
const resolvedBlocks: ResolvedBlock[] = await Promise.all(
  rawBlocks.map(async (ref) => ({
    ...ref,
    data: await ctx.runQuery(api.blocks.getBlock, { blockType: ref.blockType, blockId: ref.blockId }),
  }))
);
Previous
VextroConfig & Utilities