Skip to main content
Extensions are the foundation of Halo’s data model. Every resource in Halo - posts, users, comments, plugins, themes - is represented as an extension. This unified model provides consistency and powerful capabilities.

What is an extension?

An extension is a structured data object that follows the Kubernetes resource model. Extensions have a predictable structure with metadata, specification, and status sections.
If you’re familiar with Kubernetes, Halo extensions work similarly to Kubernetes Custom Resources.

Extension structure

Every extension has four key components:

API version and kind (GVK)

Every extension is identified by its GroupVersionKind (GVK):
apiVersion: content.halo.run/v1alpha1
kind: Post
  • Group: Organizes related resources (e.g., content.halo.run, plugin.halo.run)
  • Version: API version, typically v1alpha1 or v1
  • Kind: Resource type (e.g., Post, User, Plugin)
The GVK pattern enables API versioning and evolution without breaking existing clients.

Metadata

Metadata contains identifying information and system-managed fields:
metadata:
  name: my-first-post
  labels:
    content.halo.run/published: "true"
    content.halo.run/owner: admin
  annotations:
    content.halo.run/stats: '{"visits": 100}'
  creationTimestamp: "2024-01-15T10:30:00Z"
  version: 5
Key metadata fields:
  • name: Unique identifier for the extension (required)
  • labels: Key-value pairs for filtering and selection
  • annotations: Key-value pairs for storing arbitrary metadata
  • creationTimestamp: When the extension was created (system-managed)
  • deletionTimestamp: When deletion was requested (system-managed)
  • version: Optimistic concurrency control version (system-managed)
  • finalizers: Prevent deletion until cleanup is complete

Spec (specification)

The spec defines the desired state of the resource. This is where you declare what you want:
spec:
  title: "Getting Started with Halo"
  slug: getting-started
  publish: true
  allowComment: true
  visible: PUBLIC
  owner: admin
The spec should be immutable once created, or changes should trigger reconciliation.

Status

The status reflects the current observed state. Controllers update the status to report what’s actually happening:
status:
  phase: PUBLISHED
  permalink: "/posts/getting-started"
  conditions:
    - type: Ready
      status: True
      lastTransitionTime: "2024-01-15T10:35:00Z"
  observedVersion: 5
Users modify spec; controllers modify status. This separation enables clear responsibility boundaries.

Working with extensions in code

Halo provides Java interfaces for working with extensions:

Defining an extension

Extensions implement the Extension interface and use the @GVK annotation:
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;

@GVK(
    group = "content.halo.run",
    version = "v1alpha1",
    kind = "Post",
    plural = "posts",
    singular = "post"
)
public class Post extends AbstractExtension {
    private PostSpec spec;
    private PostStatus status;
    
    // Getters and setters...
}

Using ExtensionClient

The ExtensionClient provides CRUD operations for extensions:
// Fetch an extension
Optional<Post> post = client.fetch(Post.class, "my-post");

// List extensions with filtering
List<Post> publishedPosts = client.listAll(
    Post.class,
    ListOptions.builder()
        .labelSelector("content.halo.run/published=true")
        .build(),
    Sort.by("metadata.creationTimestamp").descending()
);

// Create an extension
Post newPost = new Post();
// ... set properties ...
client.create(newPost);

// Update an extension
post.ifPresent(p -> {
    p.getSpec().setTitle("Updated Title");
    client.update(p);
});

Labels and selectors

Labels enable powerful querying capabilities:

Label selectors

Filter extensions using label selectors:
// Equality-based selector
ListOptions.builder()
    .labelSelector("content.halo.run/published=true")
    .build()

// Set-based selector
ListOptions.builder()
    .labelSelector("content.halo.run/visible in (PUBLIC,INTERNAL)")
    .build()

Field selectors

Filter by specific fields:
ListOptions.builder()
    .fieldSelector("spec.owner=admin")
    .build()

Indexing and querying

Halo provides an indexing system for efficient queries:
// Define an index
IndexSpec indexSpec = new IndexSpecBuilder()
    .withName("author")
    .withIndexFunc(post -> List.of(post.getSpec().getOwner()))
    .build();

// Query using the index
Query query = QueryFactory.equal("spec.owner", "admin")
    .and(QueryFactory.equal("spec.published", "true"));
Indexing dramatically improves query performance for large datasets.

Controllers and reconciliation

Controllers watch extensions and reconcile them to their desired state:
public class PostReconciler implements Reconciler<Request> {
    @Override
    public Result reconcile(Request request) {
        // Fetch the post
        return client.fetch(Post.class, request.name())
            .map(post -> {
                // Reconcile logic
                updatePostStatus(post);
                generatePermalink(post);
                return Result.doNotRetry();
            })
            .orElse(Result.doNotRetry());
    }
}
Controllers enable reactive, event-driven architectures that automatically respond to changes.

Extension lifecycle

Extensions move through a defined lifecycle:

Finalizers and deletion

Finalizers prevent deletion until cleanup is complete:
// Add finalizer
MetadataOperator metadata = post.getMetadata();
Set<String> finalizers = metadata.getFinalizers();
if (finalizers == null) {
    finalizers = new HashSet<>();
    metadata.setFinalizers(finalizers);
}
finalizers.add("my-finalizer");
client.update(post);

// In reconciler, check for deletion and remove finalizer
if (post.getMetadata().getDeletionTimestamp() != null) {
    // Perform cleanup
    cleanup(post);
    
    // Remove finalizer
    post.getMetadata().getFinalizers().remove("my-finalizer");
    client.update(post);
}

Core extensions

Halo includes several built-in extensions:
  • Post: Blog posts with categories and tags
  • SinglePage: Standalone pages
  • Comment: Comments on posts and pages
  • Category: Content categories
  • Tag: Content tags
  • User: User accounts
  • Role: Permission roles
  • Plugin: Installed plugins
  • Theme: Installed themes
  • Setting: Configuration settings
  • ConfigMap: Configuration data
  • Secret: Sensitive data

Best practices

Follow these guidelines when working with extensions: Use meaningful names: Extension names should be descriptive and URL-safe. Leverage labels: Use labels for categorization and filtering, not for storing large amounts of data. Keep specs immutable: Avoid frequent spec changes. Use status for dynamic data. Implement reconcilers: For complex logic, use controllers to maintain consistency. Handle errors gracefully: Controllers may be retried, so ensure idempotency.

Next steps

Plugins

Learn how plugins use extensions

Custom models

Create your own extension types

Reconcilers

Build controllers to manage extensions

Indexing

Optimize queries with indexing

Build docs developers (and LLMs) love