s-m-r-t

@happyvertical/smrt-content

Content management system with flexible organization, publishing workflows, asset management, and AI-powered analysis.

v0.19.0Content ManagementAI-Powered

Overview

The @happyvertical/smrt-content package provides a specialized content management and processing module within the SMRT framework.

Key Features

  • Flexible Content Types: Article, Document, Mirror with STI support
  • Rich Organization: Context namespacing, hierarchical categories, tags
  • Publishing Workflow: Draft → Published → Archived lifecycle
  • Asset Management: Thumbnails, attachments, media relationships
  • AI-Powered: Analysis, summaries, translations with do()/is() methods
  • Markdown Export: YAML frontmatter + markdown for static sites
  • Content Mirroring: Scrape and cache external content
  • Thumbnail Generation: Headline cards, static maps, AI images

Architecture

┌─────────────────────────────────────────────┐
│          Content Management System           │
├─────────────────────────────────────────────┤
│  Collections Layer (Contents)                │
│  • Batch operations, bulk queries            │
│  • Export/sync operations                    │
│  • Thumbnail generation                      │
├─────────────────────────────────────────────┤
│  Entity Layer (Content)                      │
│  • Single content object management          │
│  • Asset relationships                       │
│  • Reference tracking                        │
├─────────────────────────────────────────────┤
│  Subtypes (STI)                              │
│  • Article (blog posts, news)                │
│  • ContentDocument (PDFs, reports)           │
│  • Mirror (cached external content)          │
├─────────────────────────────────────────────┤
│  Integration Layer                           │
│  • SMRT Core (ORM, persistence)              │
│  • SMRT Assets (media management)            │
│  • External APIs (documents, spider, geo)    │
└─────────────────────────────────────────────┘
			

Use Cases

  • Blog and news publishing platforms
  • Documentation sites with versioning
  • Knowledge bases and wikis
  • Content archival and web mirroring
  • Location-based content (events, places)
  • Multi-language content management

Installation

Using pnpm (recommended)

bash
pnpm add @happyvertical/smrt-content

Using npm

bash
npm install @happyvertical/smrt-content

Using bun

bash
bun add @happyvertical/smrt-content

Basic Setup

typescript
import { Contents, Content } from '@happyvertical/smrt-content';

// Create collection with database
const contents = await Contents.create({
  db: { url: 'sqlite:./content.db' },
  ai: { type: 'openai', apiKey: process.env.OPENAI_API_KEY }
});

Quick Start (5 Minutes)

1. Create and Save Content

typescript
const article = new Content({
  title: 'Getting Started with SMRT',
  body: 'Learn how to build with the SMRT framework...',
  type: 'article',
  author: 'Dev Team',
  status: 'published',
  tags: ['smrt', 'tutorial'],
  metadata: { difficulty: 'beginner' }
});

await article.initialize();
await article.save();

2. Query Content

typescript
// Get single content
const article = await contents.get({ slug: 'my-article' });

// List with filters
const published = await contents.list({
  where: {
    type: 'article',
    status: 'published',
    state: 'active'
  },
  limit: 10
});

// Count results
const total = await contents.count({ where: { type: 'article' } });

3. Use AI Features

typescript
// Analyze content
const summary = await article.do('Create a 2-sentence summary');
const isAcademic = await article.is('written in academic style');
const topics = await article.do('Extract key topics as JSON array');

// Transform content
const simplified = await article.do('Rewrite at 5th grade reading level');
const spanish = await article.do('Translate to Spanish');

4. Export to Markdown

typescript
// Export single content
await contents.writeContentFile({
  content: article,
  contentDir: './exported'
});
// Creates: ./exported/{context}/{slug}/index.md

// Sync all articles
await contents.syncContentDir({
  contentDir: './site/content'
});

Core Concepts

1. Content Types (STI)

Single Table Inheritance supports polymorphic content:

TypeUse Case
ArticleBlog posts, news articles, editorial content
ContentDocumentPDFs, reports, structured documents
MirrorMirrored/cached external content
typescript
// All stored in single 'contents' table with _meta_type
const article = new Article({ title: '...', type: 'article' });
const document = new ContentDocument({ title: '...', type: 'document' });
const mirror = new Mirror({ title: '...', type: 'mirror' });

2. Status and State

Status (Publication Lifecycle)

  • draft - Work in progress, not visible
  • published - Live and visible
  • archived - Old, not visible but preserved
  • deleted - Soft delete marker

State (Quality Flag)

  • active - Normal content
  • deprecated - Old version, may be replaced
  • highlighted - Featured/pinned content
typescript
// Draft workflow
const post = new Content({
  title: 'My Post',
  status: 'draft'
});
await post.save();

// Publish
post.status = 'published';
post.publish_date = new Date();
await post.save();

// Feature content
post.state = 'highlighted';
await post.save();

3. Content Organization

Context (Namespace)

typescript
await contents.getOrUpsert({
  slug: 'best-practices',
  context: 'blog-posts',     // Different from 'documentation'
  title: 'Best Practices'
});
// Slug + context creates unique identity

Category (Hierarchical)

typescript
content.category = 'technology/ai/nlp';

content.getCategorySegments();      // ['technology', 'ai', 'nlp']
content.getParentCategory();        // 'technology/ai'
content.getRootCategory();          // 'technology'
content.getAncestorPaths();         // ['technology', 'technology/ai', ...]
content.isInCategory('technology'); // true

Tags (Free-form)

typescript
content.tags = ['ai', 'machine-learning', 'python', 'tutorial'];

4. Content Variant

Namespaced classification for complex content organization:

typescript
// Format: generator:domain:specific-type

const upcoming = new Article({
  variant: 'praeco:meeting:upcoming',
  title: 'Council Meeting - Next Week'
});

const summary = new Article({
  variant: 'praeco:meeting:summary',
  title: 'Council Meeting Summary'
});

const transcript = new Article({
  variant: 'praeco:meeting:transcript',
  title: 'Meeting Transcript'
});

5. Asset Relationships

typescript
// Thumbnail (featured image)
await content.setThumbnail(image);
const thumb = await content.getThumbnail();

// Other attachments
await content.addAsset(pdf, 'attachment', 0);
await content.addAsset(image, 'inline', 1);

// Query by relationship type
const allAssets = await content.getAssets();
const attachments = await content.getAssets('attachment');

Thumbnail Generation

Three strategies for generating thumbnail images:

1. Headline Card

Draw title on branded background - ideal for blog posts and news

typescript
const thumbnail = await content.generateThumbnail({
  strategy: 'headline-card',
  brandColor: '#1a56db',
  backgroundColor: '#ffffff',
  subtitle: 'Blog Post'
});

2. Static Map

Use static maps API for location-based content

typescript
// Requires latitude/longitude in metadata
content.metadata = {
  latitude: 40.7128,
  longitude: -74.0060
};

const mapThumbnail = await content.generateThumbnail({
  strategy: 'static-map',
  mapProvider: 'mapbox',
  zoom: 12,
  markerColor: 'red'
});

3. AI Generate

AI image generation from content

typescript
const aiThumbnail = await content.generateThumbnail({
  strategy: 'ai-generate',
  style: 'photorealistic', // or 'illustration', 'abstract', 'minimal'
  ai: { type: 'openai', apiKey: process.env.OPENAI_API_KEY }
});

Bulk Generation

typescript
const result = await contents.generateMissingThumbnails({
  strategy: 'headline-card',
  where: { type: 'article', status: 'published' },
  brandColor: '#1a56db',
  limit: 100
});

console.log('Generated ' + result.images.length + ' thumbnails');
console.log('Failed: ' + result.failed.length);

Tutorials

Tutorial 1: Building a Blog CMS

Step 1: Create Blog Post Structure

typescript
const post = await contents.getOrUpsert({
  type: 'article',
  title: 'New Blog Post',
  slug: 'new-blog-post',
  context: 'blog',
  author: 'Writer Name',
  status: 'draft',
  body: '# Introduction\n\nContent here...',
  tags: ['feature', 'announcement'],
  metadata: { readingTime: 5 }
});

Step 2: Add Featured Image

typescript
const image = ...; // load or generate image
await post.setThumbnail(image);

// Or generate headline card
await post.generateThumbnail({
  strategy: 'headline-card',
  brandColor: '#0066cc'
});

Step 3: Publish Content

typescript
post.status = 'published';
post.publish_date = new Date();
await post.save();

Step 4: Export to Static Site

typescript
await contents.writeContentFile({
  content: post,
  contentDir: './public/blog'
});
// Creates: ./public/blog/blog/new-blog-post/index.md

Tutorial 2: Content Mirroring

Step 1: Mirror URL

typescript
const mirrored = await contents.mirror({
  url: 'https://example.com/article.html',
  context: 'external-sources',
  mirrorDir: './cache'
});

Step 2: Enhance with Metadata

typescript
mirrored.type = 'document';
mirrored.tags = ['research', 'external'];
mirrored.metadata = {
  source: 'Example Corp',
  archiveDate: new Date()
};
await mirrored.save();

Step 3: Generate Summary with AI

typescript
const summary = await mirrored.do('Create a one-paragraph abstract');
mirrored.description = summary;
await mirrored.save();

Tutorial 3: Location-based Content

Step 1: Create Content with Location

typescript
const event = new Article({
  title: 'Annual Conference 2024',
  type: 'article',
  category: 'events/conferences',
  body: 'Join us for our annual conference...',
  metadata: {
    latitude: 37.7749,
    longitude: -122.4194,
    venue: 'San Francisco Convention Center'
  }
});
await event.initialize();
await event.save();

Step 2: Generate Map Thumbnail

typescript
await event.generateThumbnail({
  strategy: 'static-map',
  mapProvider: 'mapbox',
  zoom: 13,
  markerColor: 'red'
});

Step 3: Query by Category

typescript
const conferences = await contents.list({
  where: { category: 'events/conferences' }
});

// Filter by ancestor category
const allEvents = await contents.list()
  .then(items => items.filter(c => c.isInCategory('events', true)));

Examples

Example 1: News Portal with Categories

typescript
const story = new Article({
  type: 'article',
  title: 'Local Council Approves New Park',
  category: 'local/civic/parks',
  body: 'Full news story...',
  author: 'News Staff',
  status: 'published',
  metadata: {
    breaking: false,
    importance: 5
  }
});
await story.initialize();

// Generate headline card
await story.generateThumbnail({
  strategy: 'headline-card',
  brandColor: '#0066cc',
  subtitle: 'Civic News'
});

await story.save();

// Query civic news
const civicNews = await contents.list({
  where: { category: 'local/civic' }
}).then(items =>
  items.filter(c => c.isInCategory('local/civic', true))
);

Example 2: Documentation with Variants

typescript
// Create different documentation types
const getStarted = await contents.getOrUpsert({
  type: 'article',
  variant: 'docs:guide:getting-started',
  title: 'Getting Started',
  category: 'documentation/guides',
  body: '...'
});

const apiDocs = await contents.getOrUpsert({
  type: 'article',
  variant: 'docs:reference:api',
  title: 'API Reference',
  category: 'documentation/reference',
  body: '...'
});

// Query all documentation
const allDocs = await contents.list()
  .then(items => items.filter(c => c.variant?.startsWith('docs:')));

// Query specific variant
const guides = await contents.list({
  where: { variant: 'docs:guide:getting-started' }
});

Example 3: Research Paper Archive

typescript
// Mirror PDF from conference
const paper = await contents.mirror({
  url: 'https://conference.org/papers/2024-ai-research.pdf',
  context: 'conference-2024',
  mirrorDir: './cache'
});

// Enhance with metadata
paper.type = 'document';
paper.tags = ['ai', 'research', 'neural-networks'];
paper.metadata = {
  conference: 'NeurIPS 2024',
  authors: ['Author 1', 'Author 2'],
  pages: 12,
  doi: '10.1234/example'
};
await paper.save();

// Generate AI summary
const abstract = await paper.do('Create a one-paragraph abstract');
paper.description = abstract;
await paper.save();

Integration with Other Modules

smrt-core

  • Content extends SmrtObject for automatic persistence
  • STI support via @smrt({ tableStrategy: 'sti' })
  • Auto-generated REST API, CLI commands, and MCP tools

smrt-assets

typescript
// Link images and documents
const image = new Image({...});
await image.save();
await content.setThumbnail(image);

// Multiple assets with relationships
await content.addAsset(image, 'inline', 0);
await content.addAsset(pdf, 'attachment', 1);

External Integrations

  • @happyvertical/documents: PDF text extraction
  • @happyvertical/spider: Web scraping for mirroring
  • @happyvertical/geo: Geolocation for map thumbnails

Best Practices

1. Content Organization

typescript
// DO: Use context for namespacing
const contexts = ['blog', 'documentation', 'news', 'archived-sources'];

// DO: Use hierarchical categories for navigation
const categories = ['tech/frontend/react', 'tech/backend/nodejs'];

// DO: Use tags for flexible filtering
content.tags = ['tutorial', 'intermediate', 'javascript'];

// DON'T: Mix namespacing mechanisms
// DON'T: Use categories for things better suited to tags

2. Publishing Workflow

typescript
// DO: Use status for publication state
content.status = 'draft';
// ... editorial review ...
content.status = 'published';
content.publish_date = new Date();

// DO: Use state for quality flagging
content.state = 'highlighted'; // Featured content

// DON'T: Update status manually in production without workflow

3. Metadata Usage

typescript
// DO: Store structured data as metadata
content.metadata = {
  readingTime: 15,
  difficulty: 'advanced',
  prerequisite: ['javascript-basics']
};

// DON'T: Overload metadata with things that should be columns
// DON'T: Query deeply nested metadata structures frequently

4. AI Feature Usage

typescript
// DO: Cache AI results
const summary = await content.do('Summarize in 2 sentences');
content.metadata.aiSummary = summary;
await content.save();

// DO: Use classification for automation
const isAcademic = await content.is('written in academic style');
if (isAcademic) {
  content.tags.push('academic-paper');
}

// DON'T: Call do()/is() repeatedly for same result

Troubleshooting

Duplicate Content on Mirror

Solution: Check for existing content before mirroring

typescript
const existing = await contents.get({ url: mirrorUrl });
if (existing) {
  return existing;
}

// Or use getOrUpsert
const mirrored = await contents.getOrUpsert({
  url: mirrorUrl,
  slug: makeSlug(title),
  context: 'mirrors'
});

Thumbnail Generation Fails

Solution: Ensure required metadata is present

typescript
// For static-map: coordinates required
content.metadata = {
  latitude: 40.7128,
  longitude: -74.0060
};

// For headline-card: brand color required
content.generateThumbnail({
  strategy: 'headline-card',
  brandColor: '#1a56db'
});

Export Creates Nested Directories

Solution: Understand path structure

typescript
// Path: {contentDir}/{context}/{slug}/index.md

content.context = 'blog';        // → blog/slug/index.md
content.context = 'blog/tech';   // → blog/tech/slug/index.md
content.context = '';            // → slug/index.md

AI Features Not Available

Solution: Ensure AI config is passed

typescript
const contents = await Contents.create({
  db: { url: 'sqlite:./content.db' },
  ai: {
    type: 'openai',
    apiKey: process.env.OPENAI_API_KEY // Required
  }
});

API Reference

Content Class

MethodReturnsDescription
initialize()Promise<this>Initialize content before saving
addReference(content)Promise<void>Link related content
getReferences()Promise<Content[]>Get linked content
getAssets(relationship?)Promise<Asset[]>Get media assets
addAsset(asset, relationship, order)Promise<void>Link asset with relationship type
getThumbnail()Promise<Image | null>Get featured image
setThumbnail(image)Promise<void>Set featured image
generateThumbnail(options)Promise<Image>Generate thumbnail with strategy
getCategorySegments()string[]Split category into array
isInCategory(path, includeChildren?)booleanCheck if in category hierarchy
do(prompt)Promise<string>AI transformation/analysis
is(question)Promise<boolean>AI classification

Contents Collection

MethodReturnsDescription
create(options)Promise<Contents>Create collection instance
get(where)Promise<Content | null>Get single content
list(options)Promise<Content[]>Query multiple content
count(options)Promise<number>Count matching content
getOrUpsert(data)Promise<Content>Find or create content
mirror(options)Promise<Content | undefined>Scrape and cache URL
writeContentFile(options)Promise<void>Export to markdown file
syncContentDir(options)Promise<void>Export all content to directory
generateMissingThumbnails(options)Promise<BulkResult>Bulk thumbnail generation