Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions cloud/app/lib/content/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Parse frontmatter from MDX content
*/
export function parseFrontmatter(content: string): {
frontmatter: Record<string, string>;
content: string;
} {
if (!content.startsWith("---")) {
return { frontmatter: {}, content };
}

const parts = content.split("---");

if (parts.length >= 3 && parts[1].trim() === "") {
return {
frontmatter: {},
content: parts.slice(2).join("---").trimStart(),
};
}

if (parts.length >= 3) {
const frontmatterStr = parts[1].trim();
const contentParts = parts.slice(2).join("---");
const cleanContent = contentParts.trimStart();

const frontmatter: Record<string, string> = {};

frontmatterStr.split("\n").forEach((line) => {
const trimmedLine = line.trim();
if (!trimmedLine) return;

const colonIndex = trimmedLine.indexOf(":");
if (colonIndex > 0) {
const key = trimmedLine.slice(0, colonIndex).trim();
const value = trimmedLine.slice(colonIndex + 1).trim();
frontmatter[key] = value.replace(/^["'](.*)["']$/, "$1");
}
});

return { frontmatter, content: cleanContent };
}

return { frontmatter: {}, content };
}
25 changes: 25 additions & 0 deletions cloud/app/lib/content/toc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Extract table of contents from MDX content
*/
export function extractTOC(content: string): Array<{
id: string;
text: string;
level: number;
}> {
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
const toc: Array<{ id: string; text: string; level: number }> = [];
let match;

while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const text = match[2].trim();
const id = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");

toc.push({ id, text, level });
}

return toc;
}
64 changes: 64 additions & 0 deletions cloud/app/lib/content/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Table of contents item extracted from MDX headings
*/
export interface TOCItem {
id: string;
text: string;
level: number;
children?: TOCItem[];
}

/* ========== CONTENT TYPES =========== */

/**
* All recognized content types in the system
* Each type is mapped to:
* - Source directory: content/{type}
* - Output directory: static/content/{type}
* - Metadata file: static/content-meta/{type}/index.json
*/
export type ContentType = "docs" | "blog" | "policy" | "dev" | "llm-docs";
export const CONTENT_TYPES: ContentType[] = ["docs", "blog", "policy", "dev"];

/**
* Base metadata interface that all content types extend
* This metadata is generated during preprocessing and stored with the content
*/
export interface ContentMeta {
title: string;
description: string;
path: string;
slug: string;
type: ContentType;
route: string; // Full URL route for cross-referencing with search results
}

/**
* Core content interface that combines metadata with content
* The meta and content are loaded from JSON, with MDX processed on demand
*/
export interface Content<T extends ContentMeta = ContentMeta> {
meta: T; // Typed, validated metadata
content: string; // MDX with frontmatter stripped out

// MDX structure expected by components (used in MDXRenderer)
mdx: {
code: string; // Compiled MDX code
frontmatter: Record<string, unknown>; // Extracted frontmatter
tableOfContents: TOCItem[]; // Table of contents extracted from headings
};
}

/* ========== BLOG CONTENT TYPES =========== */

/**
* Blog-specific metadata extends the base ContentMeta
*/
export interface BlogMeta extends ContentMeta {
date: string; // Publication date in YYYY-MM-DD format
author: string; // Author name
readTime: string; // Estimated reading time
lastUpdated: string; // Last update date
}

export type BlogContent = Content<BlogMeta>;
12 changes: 2 additions & 10 deletions cloud/app/lib/mdx/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { TOCItem } from "@/app/lib/content/types";

/**
* MDX Type Definitions
*
Expand All @@ -6,16 +8,6 @@

import type React from "react";

/**
* Table of contents item extracted from MDX headings
*/
export interface TOCItem {
id: string;
text: string;
level: number;
children?: TOCItem[];
}

/**
* Frontmatter extracted from MDX files
*/
Expand Down
6 changes: 6 additions & 0 deletions cloud/app/types/virtual-content-meta.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ContentMeta, BlogMeta } from "@/app/lib/content/types";

declare module "virtual:content-meta" {
export const blogPosts: BlogMeta[];
export const allContent: ContentMeta[];
}
76 changes: 69 additions & 7 deletions cloud/vite-plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Transforms `.mdx` files into importable ES modules at build time.
Import MDX files directly in your components:

```typescript
import { mdx } from "@/content/docs/v1/getting-started.mdx";
import { mdx } from "@/content/docs/v1/placeholder.mdx";

// mdx is a React component with metadata attached
console.log(mdx.frontmatter.title); // Access frontmatter
Expand All @@ -40,12 +40,17 @@ description: Learn how to use Mirascope
Your content here...
```

### Adding New MDX Files
For blog posts, include additional frontmatter:

1. Create a new `.mdx` file in `content/docs/v1/`
2. Add frontmatter (optional but recommended)
3. Import and use it in your route components
4. The plugin automatically handles compilation and metadata extraction
```mdx
---
title: My Blog Post
description: A description of the post
date: "2024-01-15"
author: "Author Name"
readTime: "5 min read"
---
```

### Build-time vs Runtime

Expand All @@ -55,7 +60,64 @@ Your content here...

### Type Safety

TypeScript types are provided in `app/types/mdx.d.ts` for proper autocomplete and type checking.
TypeScript types are provided in:
- `app/types/mdx.d.ts` - types for MDX imports

## Content Plugin (`content.ts`)

Scans the content directory and maintains metadata about all MDX files for listing and querying.

### Features

- **Directory scanning**: Scans `content/` on startup with parallel processing
- **Metadata extraction**: Builds metadata for all MDX files (title, description, slug, route, etc.)
- **Virtual module**: Exposes meta via `virtual:content-meta`
- **Hot Module Replacement**: Meta is automatically updated when files change

### Usage

Access content metadata via the virtual module:

```typescript
// @ts-expect-error - virtual module resolved by vite plugin
import { blogPosts, allContent } from "virtual:content-meta";

// blogPosts: BlogMeta[] - blog posts sorted by date (newest first)
// allContent: ContentMeta[] - all MDX content entries

blogPosts.forEach(post => {
console.log(post.title, post.date, post.route);
});
```

### Content Types

The plugin recognizes content types based on directory structure:
- `content/blog/` → type: "blog"
- `content/docs/` → type: "docs"
- `content/policy/` → type: "policy"
- `content/dev/` → type: "dev"

### Blog Metadata

Blog posts include additional fields:
- `date`: Publication date
- `author`: Author name
- `readTime`: Estimated reading time
- `lastUpdated`: Last update date

### Adding New Content

1. Create a new `.mdx` file in the appropriate `content/` subdirectory
2. Add frontmatter with required fields
3. The meta is automatically updated during development (HMR)
4. Use `blogPosts` or `allContent` to list and query content

### Type Safety

TypeScript types are provided in:
- `app/types/virtual-content-meta.d.ts` - types for the virtual module
- `app/lib/content/types.ts` - `ContentMeta` and `BlogMeta` interfaces

## Images Plugin (`images.ts`)

Expand Down
Loading