Skip to main content

Types reference

The TypeScript types theme + plugin authors most often use, with the canonical definitions and what's actually in each field.

For the exhaustive list see the source: src/core/types.ts (admin-side types) and src/themes/types.ts (theme-side types).

Content types

Post

The same shape covers both posts (type: "post") and pages (type: "page").

interface Post {
id: string;
type: "post" | "page";
title: string;
slug: string;
contentMarkdown: string;
excerpt?: string;
heroMediaId?: string; // Media doc id; resolve via `media.get(heroMediaId)`
authorId: string; // User uid
termIds: string[]; // Category + tag ids — at most ONE category
primaryTermId?: string; // The category id (denormalised — affects URL)
status: "draft" | "online";
seo?: SeoMeta;
createdAt?: Timestamp;
updatedAt?: Timestamp;
publishedAt?: Timestamp;
lastPublishedPath?: string; // current live path (e.g. "news/launch.html")
previousPublishedPaths?: string[]; // paths whose deletion failed — retried on next publish
lastPublishedHash?: string; // sha256 of rendered HTML — skip-upload optimisation
legacyUrl?: string; // imported from external CMS (WP, etc.)
}

interface SeoMeta {
title?: string;
description?: string;
ogImage?: string;
}

Term

Categories and tags share one schema, distinguished by type.

interface Term {
id: string;
type: "category" | "tag";
name: string;
slug: string;
description?: string;
parentId?: string; // for hierarchical categories
createdAt?: Timestamp;
updatedAt?: Timestamp;
lastPublishedPath?: string; // for category archives ("news/index.html")
}

Media

interface Media {
id: string;
filename: string; // user-uploaded filename
alt?: string;
caption?: string;
uploadedAt: Timestamp;
uploadedBy: string; // user uid
// Modern format (every upload after the variant pipeline):
formats?: Record<string, MediaVariant>;
defaultFormat?: string;
originalSlug?: string; // slug + 6-char hex suffix
// Legacy single-URL format (uploaded before variant pipeline):
url?: string;
storagePath?: string;
}

interface MediaVariant {
url: string;
width: number;
height: number;
bytes: number;
}

For consumption in templates, use mediaToView(media) from @flexweg/cms-runtime to convert to MediaView — handles the legacy / modern divergence transparently.

MediaView

interface MediaView {
alt?: string;
caption?: string;
default: string; // name of the default variant
formats: Record<string, { url: string; width: number; height: number }>;
}

Pick a specific URL via pickFormat(view, name) or pickMediaUrl(view).

SiteSettings

interface SiteSettings {
title: string;
description?: string;
language?: string; // BCP-47, e.g. "en", "fr-FR"
baseUrl?: string; // public URL — required for sitemaps, RSS, search
activeThemeId: string;
themeConfigs?: Record<string, unknown>;
enabledPlugins?: Record<string, boolean>;
pluginConfigs?: Record<string, unknown>;
homeMode?: "latest-posts" | "static-page";
homePageId?: string; // required when homeMode === "static-page"
postsPerPage?: number;
paginationMode?: "global" | "paginated";
menus?: { header: MenuItem[]; footer: MenuItem[] };
socials?: SocialEntry[];
// …
}

The pluginConfigs.<id> map carries each plugin's config. The themeConfigs.<id> map carries each theme's config.

interface MenuItem {
id: string; // local-only stable id
kind: "post" | "page" | "category" | "tag" | "url";
ref?: string; // post/page/term id — when kind isn't "url"
label?: string; // override; defaults to entity name
href?: string; // direct URL — when kind === "url"
target?: "_blank" | "_self";
children?: MenuItem[]; // nested submenus
}
type UserRole = "admin" | "editor";
type AdminLocale = "en" | "fr" | "de" | "es" | "nl" | "pt" | "ko";

interface UserRecord {
uid: string;
email: string;
displayName?: string;
firstName?: string;
lastName?: string;
role: UserRole;
bio?: string;
title?: string; // job title shown publicly
avatarMediaId?: string;
socials?: SocialEntry[];
preferences?: UserPreferences;
}

interface UserPreferences {
adminLocale?: AdminLocale;
}

Theme-side types

SiteContext

Every template + BaseLayout receives this:

interface SiteContext {
settings: SiteSettings;
resolvedMenus: {
header: ResolvedMenuItem[];
footer: ResolvedMenuItem[];
};
themeCssPath: string; // "theme-assets/<active-id>.css"
themeConfig?: unknown; // active theme's resolved config
}

interface ResolvedMenuItem {
label: string;
href: string; // absolute path on Flexweg
target?: "_blank" | "_self";
children?: ResolvedMenuItem[];
}

BaseLayoutProps

interface BaseLayoutProps {
site: SiteContext;
pageTitle: string;
pageDescription?: string;
ogImage?: string;
currentPath: string; // e.g. "news/launch.html"
extraHead?: string; // unused (sentinel-replaced post-render)
children: ReactNode;
}

Template-specific props

See Templates and props for HomeTemplateProps, SingleTemplateProps, CategoryTemplateProps, AuthorTemplateProps, NotFoundTemplateProps.

AuthorView

interface AuthorView {
id: string;
displayName: string;
title?: string;
bio?: string;
avatar?: MediaView;
socials?: AuthorSocial[];
}

interface AuthorSocial {
network: SocialNetwork;
url: string;
}

CardPost

type CardPost = Post & {
url: string; // pre-computed by publisher
hero?: MediaView;
category?: { name: string; url: string };
dateLabel?: string; // pre-formatted, locale-aware
};

HomeTemplateProps.posts, CategoryTemplateProps.posts, AuthorTemplateProps.posts are all CardPost[].

Publisher types

PublishContext

interface PublishContext {
posts: Post[];
pages: Post[];
terms: Term[];
media: Map<string, Media>;
settings: SiteSettings;
authors: Map<string, AuthorView>;
// …
}

Action handlers receive (post, ctx: PublishContext). The context is already patched with the post-transition state (e.g. after post.deleted, ctx.posts no longer contains the deleted post).

PublishLogger

Used by regeneration target runners:

interface PublishLogger {
(entry: PublishLogEntry): void;
}

interface PublishLogEntry {
level: "info" | "success" | "warn" | "error";
message: string;
}
api.registerRegenerationTarget({
id: "my-plugin",
labelKey: "regenerationTarget.label",
descriptionKey: "regenerationTarget.description",
run: async (ctx, log) => {
log({ level: "info", message: "Starting…" });
// …
log({ level: "success", message: "Done." });
},
});

Plugin types

PluginManifest

See Plugin manifest reference.

PluginApi

interface PluginApi {
addFilter: <T>(hook: string, fn, priority?: number) => void;
addAction: (hook: string, fn, priority?: number) => void;
registerBlock: (manifest: BlockManifest) => void;
registerDashboardCard: (def: DashboardCardManifest) => void;
registerRegenerationTarget: (def: RegenerationTarget) => void;
}

BlockManifest

See Plugin blocks and Theme blocks.

interface BlockManifest {
id: string;
titleKey: string;
namespace?: string;
icon: ComponentType<{ size?: number }>;
category: "text" | "media" | "layout" | "embed" | "advanced";
insert: (chain, ctx) => void;
isActive?: (editor) => boolean;
extensions?: Extension[];
inspector?: ComponentType<BlockInspectorProps>;
nodeName?: string;
}

Image format types

type ImageFit = "cover" | "contain";

interface ImageFormat {
width: number;
height: number;
fit: ImageFit;
}

interface ImageFormatConfig {
inputFormats: string[]; // e.g. [".jpg", ".jpeg", ".png", ".webp", ".gif"]
outputFormat: "webp" | "jpeg" | "png";
quality: number; // 1-100
formats: Record<string, ImageFormat>;
defaultFormat: string;
}

Continue