ghost-mcp/contracts/ghost-filtering.md

8.8 KiB

Ghost API Filtering Syntax Documentation

Overview

Ghost uses NQL (Node Query Language) for filtering API results. This provides powerful querying capabilities for both Content and Admin APIs.

Basic Syntax

Property-Operator-Value Format

property:operator value

Simple Examples

status:published          # Posts with published status
featured:true             # Featured posts only
slug:welcome              # Post with slug "welcome"
tag:news                  # Posts tagged with "news"

Operators

1. Equality (Default)

Operator: : (colon) Usage: Exact match

status:published
featured:true
author:john-doe

2. Negation

Operator: - (minus prefix) Usage: NOT equal to

-status:draft             # Not draft posts
-featured:true            # Non-featured posts
-tag:internal             # Posts not tagged "internal"

3. Comparison Operators

Operators: >, >=, <, <= Usage: Numeric and date comparisons

published_at:>2023-01-01  # Posts published after date
read_time:>5              # Posts with reading time > 5 minutes
published_at:<=now-7d     # Posts published within last 7 days

4. Text Matching

Operators: ~, ~^, ~$ Usage: Partial text matching

title:~ghost              # Title contains "ghost"
slug:~^getting-started    # Slug starts with "getting-started"
excerpt:~$tutorial        # Excerpt ends with "tutorial"

5. Group Selection

Operator: [value1, value2, ...] Usage: Match any value in the list

tag:[news,updates,blog]   # Posts with any of these tags
author:[john,jane,bob]    # Posts by any of these authors
status:[published,draft]  # Published or draft posts

Value Types

1. Null Values

feature_image:null        # Posts without feature image
custom_excerpt:-null      # Posts with custom excerpt (not null)

2. Boolean Values

featured:true
featured:false
page:true                 # Only pages
page:false                # Only posts

3. Numbers

read_time:>10
read_time:<=5
comment_count:0

4. Literal Strings

Format: No quotes needed for simple strings

status:published
tag:javascript
author:john-doe

5. Quoted Strings

Format: Single quotes for strings with special characters

title:'My Post Title'
tag:'getting started'
excerpt:'This is a long excerpt with spaces'

6. Relative Dates

Format: now, now-Xd (days), now-Xm (months), now-Xy (years)

published_at:>now-30d     # Last 30 days
created_at:<=now-1y       # More than 1 year old
updated_at:>now-7d        # Updated in last week

Combination Logic

1. AND Operator

Operator: + Usage: All conditions must be true

status:published+featured:true
tag:news+published_at:>now-7d
author:john+status:published+featured:true

2. OR Operator

Operator: , (comma) Usage: Any condition can be true

status:published,status:draft
tag:news,tag:updates
author:john,author:jane

3. Precedence Control

Operator: () (parentheses) Usage: Group conditions and control evaluation order

(tag:news,tag:updates)+status:published
author:john+(status:published,status:draft)

Complex Filter Examples

authors.slug:[john,jane]+featured:true+published_at:>now-30d

2. Published Content with Specific Tags, Excluding Drafts

tag:[tutorial,guide]+status:published+-status:draft

3. Posts with Reading Time Between 5-15 Minutes

read_time:>=5+read_time:<=15+status:published

4. Recent Posts with Feature Images

published_at:>now-7d+feature_image:-null+status:published
(page:true,featured:true)+authors.slug:[admin,editor,writer]

Field-Specific Filters

Posts/Pages

id:5f7c1b4b0c7b4b001f7b4b1c
slug:my-post-slug
title:~'How to'
html:~'tutorial'
plaintext:~'javascript'
feature_image:null
featured:true
page:false
status:published
visibility:public
created_at:>now-30d
published_at:<=2023-12-31
updated_at:>now-7d

Tags

id:tag_id
name:'JavaScript'
slug:javascript
description:~'programming'
visibility:public

Authors

id:author_id
name:'John Doe'
slug:john-doe
email:john@example.com

Relationships (Dot Notation)

authors.slug:john-doe
authors.name:'John Doe'
tags.slug:javascript
tags.name:'JavaScript'
primary_author.slug:admin
primary_tag.slug:featured

URL Encoding

Required Encoding

Filter strings must be URL encoded when used in URLs:

const filter = "tag:javascript+published_at:>now-7d";
const encoded = encodeURIComponent(filter);
// Result: tag%3Ajavascript%2Bpublished_at%3A%3Enow-7d

Common Encodings

:  → %3A
+  → %2B
,  → %2C
>  → %3E
<  → %3C
~  → %7E
[  → %5B
]  → %5D
'  → %27

Limitations & Constraints

1. Property Naming

  • Properties must start with a letter
  • Use dot notation for relationships
  • Case-sensitive property names

2. Value Constraints

  • No regex or advanced pattern matching
  • Limited to defined operators
  • String values with special chars need quotes

3. Performance Considerations

  • Complex filters may impact query performance
  • Index-based fields filter faster
  • Limit filter complexity for optimal response times

4. API Limitations

  • Some fields may not be filterable
  • Admin API may have different filter availability than Content API
  • Check specific endpoint documentation for filter support

Implementation for MCP Server

Filter Builder Class

class GhostFilterBuilder {
  constructor() {
    this.conditions = [];
  }

  equals(property, value) {
    this.conditions.push(`${property}:${this.formatValue(value)}`);
    return this;
  }

  notEquals(property, value) {
    this.conditions.push(`-${property}:${this.formatValue(value)}`);
    return this;
  }

  greaterThan(property, value) {
    this.conditions.push(`${property}:>${this.formatValue(value)}`);
    return this;
  }

  contains(property, value) {
    this.conditions.push(`${property}:~${this.formatValue(value)}`);
    return this;
  }

  inArray(property, values) {
    const formatted = values.map(v => this.formatValue(v)).join(',');
    this.conditions.push(`${property}:[${formatted}]`);
    return this;
  }

  formatValue(value) {
    if (value === null) return 'null';
    if (typeof value === 'boolean') return value.toString();
    if (typeof value === 'number') return value.toString();
    if (typeof value === 'string' && /^[a-zA-Z0-9\-_]+$/.test(value)) {
      return value; // Simple string, no quotes needed
    }
    return `'${value}'`; // Complex string, needs quotes
  }

  and() {
    // Next condition will be ANDed
    this.operator = '+';
    return this;
  }

  or() {
    // Next condition will be ORed
    this.operator = ',';
    return this;
  }

  build() {
    return this.conditions.join(this.operator || '+');
  }

  buildEncoded() {
    return encodeURIComponent(this.build());
  }
}

// Usage example
const filter = new GhostFilterBuilder()
  .equals('status', 'published')
  .and()
  .equals('featured', true)
  .and()
  .greaterThan('published_at', 'now-30d')
  .build();
// Result: status:published+featured:true+published_at:>now-30d

Phase Implementation Strategy

Phase 1: Basic Filters (Current)

// Simple equality filters
status:published
featured:true
tag:news
author:john-doe

Phase 2: Advanced Filters (Future)

// Complex filters with operators
published_at:>now-30d+featured:true
authors.slug:[john,jane]+tag:[news,updates]
title:~'tutorial'+read_time:>=5

Phase 3: Filter Builder (Future)

  • Programmatic filter construction
  • Validation and sanitization
  • Advanced query optimization

Testing Strategy

Unit Tests

describe('Ghost Filter Syntax', () => {
  test('simple equality filter', () => {
    expect(buildFilter('status', 'published')).toBe('status:published');
  });

  test('complex AND filter', () => {
    const filter = 'status:published+featured:true';
    expect(parseFilter(filter)).toMatchObject({
      conditions: [
        { property: 'status', operator: ':', value: 'published' },
        { property: 'featured', operator: ':', value: 'true' }
      ]
    });
  });
});

Integration Tests

test('filter with real API', async () => {
  const response = await contentApi.request('/posts/', {
    filter: 'status:published+featured:true',
    limit: 5
  });
  expect(response.posts.every(post =>
    post.status === 'published' && post.featured === true
  )).toBe(true);
});

Implementation Status

  • Filter syntax documented
  • Operators and examples identified
  • Implementation strategy planned
  • Phase 1 vs Phase 2 filters defined
  • Filter builder implementation pending
  • Real API testing pending API keys