diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 7fdcffc..9238199 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -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' } ], @@ -130,6 +131,7 @@ export default defineConfig({ ], }, ], + '/blog/': [], '/': [ { text: 'Quickstart', @@ -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 }], + ) + } }, }) diff --git a/.vitepress/theme/BlogIndex.vue b/.vitepress/theme/BlogIndex.vue new file mode 100644 index 0000000..d4c7bca --- /dev/null +++ b/.vitepress/theme/BlogIndex.vue @@ -0,0 +1,54 @@ + + + diff --git a/.vitepress/theme/BlogPost.vue b/.vitepress/theme/BlogPost.vue new file mode 100644 index 0000000..b39af8b --- /dev/null +++ b/.vitepress/theme/BlogPost.vue @@ -0,0 +1,23 @@ + + + diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 1a9b266..ac5439c 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -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() @@ -16,5 +17,8 @@ const { frontmatter } = useData() + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 7b26328..048b843 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -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; +} diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 61f54d9..0b5ac92 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -7,6 +7,8 @@ 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, @@ -14,6 +16,8 @@ export default { async enhanceApp({ app }) { useOpenapi({ spec }) theme.enhanceApp({ app }) + app.component('BlogIndex', BlogIndex) + app.component('BlogPost', BlogPost) }, setup() { const route = useRoute() diff --git a/blog/index.md b/blog/index.md new file mode 100644 index 0000000..fe0e5ac --- /dev/null +++ b/blog/index.md @@ -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. + + diff --git a/blog/introducing-cycles-blog.md b/blog/introducing-cycles-blog.md new file mode 100644 index 0000000..3a1724b --- /dev/null +++ b/blog/introducing-cycles-blog.md @@ -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. diff --git a/blog/posts.data.ts b/blog/posts.data.ts new file mode 100644 index 0000000..459e2ce --- /dev/null +++ b/blog/posts.data.ts @@ -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()) + }, +})