@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-contentUsing npm
bash
npm install @happyvertical/smrt-contentUsing bun
bash
bun add @happyvertical/smrt-contentBasic 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:
| Type | Use Case |
|---|---|
Article | Blog posts, news articles, editorial content |
ContentDocument | PDFs, reports, structured documents |
Mirror | Mirrored/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 visiblepublished- Live and visiblearchived- Old, not visible but preserveddeleted- Soft delete marker
State (Quality Flag)
active- Normal contentdeprecated- Old version, may be replacedhighlighted- 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 identityCategory (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'); // trueTags (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.mdTutorial 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 tags2. 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 workflow3. 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 frequently4. 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 resultTroubleshooting
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.mdAI 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
| Method | Returns | Description |
|---|---|---|
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?) | boolean | Check if in category hierarchy |
do(prompt) | Promise<string> | AI transformation/analysis |
is(question) | Promise<boolean> | AI classification |
Contents Collection
| Method | Returns | Description |
|---|---|---|
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 |