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
11 changes: 11 additions & 0 deletions .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export default defineConfig({
{ text: 'RunCycles Admin API', link: '/admin-api/' },
],
},
{ text: 'Blog', link: '/blog/' },
{ text: 'Protocol', link: 'https://github.com/runcycles/cycles-protocol' },
{ text: 'GitHub', link: 'https://github.com/runcycles' }
],
Expand All @@ -130,6 +131,7 @@ export default defineConfig({
],
},
],
'/blog/': [],
'/': [
{
text: 'Quickstart',
Expand Down Expand Up @@ -281,5 +283,14 @@ export default defineConfig({
'link',
{ rel: 'canonical', href: canonicalUrl },
])

if (pageData.frontmatter.blog) {
pageData.frontmatter.head.push(
['meta', { property: 'og:type', content: 'article' }],
['meta', { property: 'og:title', content: pageData.frontmatter.title }],
['meta', { property: 'og:description', content: pageData.frontmatter.description }],
['meta', { property: 'article:published_time', content: pageData.frontmatter.date }],
)
}
},
})
54 changes: 54 additions & 0 deletions .vitepress/theme/BlogIndex.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup>
import { ref, computed } from 'vue'
import { data as posts } from '../../blog/posts.data'

const selectedTag = ref(null)

const allTags = computed(() =>
[...new Set(posts.flatMap(p => p.tags))].sort()
)

const filteredPosts = computed(() =>
selectedTag.value
? posts.filter(p => p.tags.includes(selectedTag.value))
: posts
)

function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
})
}
</script>

<template>
<div class="blog-index">
<div class="blog-tags" v-if="allTags.length">
<button
:class="{ active: !selectedTag }"
@click="selectedTag = null"
>All</button>
<button
v-for="tag in allTags" :key="tag"
:class="{ active: selectedTag === tag }"
@click="selectedTag = tag"
>{{ tag }}</button>
</div>

<article v-for="post in filteredPosts" :key="post.url" class="blog-card">
<h2><a :href="post.url">{{ post.title }}</a></h2>
<div class="blog-meta">
<span class="blog-date">{{ formatDate(post.date) }}</span>
<span class="blog-author">{{ post.author }}</span>
</div>
<p class="blog-description">{{ post.description }}</p>
<div class="blog-card-tags" v-if="post.tags.length">
<span v-for="tag in post.tags" :key="tag" class="blog-tag">{{ tag }}</span>
</div>
</article>

<p v-if="filteredPosts.length === 0" class="blog-empty">
No posts found.
</p>
</div>
</template>
23 changes: 23 additions & 0 deletions .vitepress/theme/BlogPost.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup>
import { useData } from 'vitepress'

const { frontmatter } = useData()

function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
})
}
</script>

<template>
<div class="blog-post-header" v-if="frontmatter.blog">
<p class="blog-post-meta">
<time>{{ formatDate(frontmatter.date) }}</time>
<span v-if="frontmatter.author"> &middot; {{ frontmatter.author }}</span>
</p>
<div class="blog-post-tags" v-if="frontmatter.tags?.length">
<span v-for="tag in frontmatter.tags" :key="tag" class="blog-tag">{{ tag }}</span>
</div>
</div>
</template>
4 changes: 4 additions & 0 deletions .vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
import NotFound from './NotFound.vue'
import HomeCodeSnippet from './HomeCodeSnippet.vue'
import BlogPost from './BlogPost.vue'

const { Layout } = DefaultTheme
const { frontmatter } = useData()
Expand All @@ -16,5 +17,8 @@ const { frontmatter } = useData()
<template #not-found>
<NotFound />
</template>
<template #doc-before>
<BlogPost />
</template>
</Layout>
</template>
96 changes: 96 additions & 0 deletions .vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,99 @@
max-height: 500px !important;
}
}

/* ── Blog index ── */
.blog-index {
max-width: 740px;
margin: 0 auto;
}

.blog-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 32px;
}

.blog-tags button {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
padding: 4px 14px;
font-size: 13px;
cursor: pointer;
color: var(--vp-c-text-2);
transition: all 0.2s;
}

.blog-tags button.active,
.blog-tags button:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}

.blog-card {
padding: 24px 0;
border-bottom: 1px solid var(--vp-c-divider);
}

.blog-card h2 {
margin: 0 0 8px;
font-size: 20px;
}

.blog-card h2 a {
color: var(--vp-c-text-1);
text-decoration: none;
}

.blog-card h2 a:hover {
color: var(--vp-c-brand-1);
}

.blog-meta {
font-size: 13px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}

.blog-meta .blog-author::before {
content: " \00b7 ";
}

.blog-description {
color: var(--vp-c-text-2);
font-size: 15px;
line-height: 1.6;
}

.blog-tag {
display: inline-block;
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 2px 10px;
font-size: 12px;
color: var(--vp-c-text-3);
margin-right: 6px;
margin-top: 8px;
}

.blog-empty {
color: var(--vp-c-text-3);
font-style: italic;
}

/* ── Blog post header ── */
.blog-post-header {
margin-bottom: 24px;
}

.blog-post-meta {
color: var(--vp-c-text-3);
font-size: 14px;
margin: 0;
}

.blog-post-tags {
margin-top: 8px;
}
4 changes: 4 additions & 0 deletions .vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import 'vitepress-openapi/dist/style.css'
import './custom.css'
import spec from '../../public/openapi.json'
import Layout from './Layout.vue'
import BlogIndex from './BlogIndex.vue'
import BlogPost from './BlogPost.vue'

export default {
extends: DefaultTheme,
Layout,
async enhanceApp({ app }) {
useOpenapi({ spec })
theme.enhanceApp({ app })
app.component('BlogIndex', BlogIndex)
app.component('BlogPost', BlogPost)
},
setup() {
const route = useRoute()
Expand Down
11 changes: 11 additions & 0 deletions blog/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Blog
description: News, guides, and updates from the Cycles team.
sidebar: false
---

# Blog

Latest news, engineering insights, and updates from the Cycles team.

<BlogIndex />
26 changes: 26 additions & 0 deletions blog/introducing-cycles-blog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: Introducing the Cycles Blog
date: 2026-03-19
author: Cycles Team
tags: [announcement]
description: We're launching our blog to share engineering insights, product updates, and best practices for budget authority in autonomous systems.
blog: true
sidebar: false
---

# Introducing the Cycles Blog

We're excited to launch the Cycles blog — a space for engineering deep-dives, product updates, and practical guidance on budget authority for autonomous agents.

## What to Expect

- **Engineering deep-dives** on budget enforcement patterns, concurrency handling, and system design
- **Product updates** covering new features, SDK releases, and protocol changes
- **Best practices** for integrating Cycles into your stack — from shadow mode rollouts to production hardening
- **Community stories** from teams using Cycles to keep their agents in check

## Why a Blog?

Our docs cover the _how_. The blog covers the _why_ — the thinking behind design decisions, the incidents that shaped the protocol, and the patterns we see across teams adopting budget authority.

Stay tuned for our first technical post.
28 changes: 28 additions & 0 deletions blog/posts.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createContentLoader } from 'vitepress'

export interface PostData {
title: string
url: string
date: string
author: string
tags: string[]
description: string
}

export declare const data: PostData[]

export default createContentLoader('blog/**/*.md', {
transform(raw) {
return raw
.filter(page => page.url !== '/blog/')
.map(page => ({
title: page.frontmatter.title,
url: page.url,
date: page.frontmatter.date,
author: page.frontmatter.author ?? 'Cycles Team',
tags: page.frontmatter.tags ?? [],
description: page.frontmatter.description ?? '',
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
},
})
Loading