diff --git a/.github/workflows/deploy-posts.yml b/.github/workflows/deploy-posts.yml new file mode 100644 index 00000000000..943ed49a85d --- /dev/null +++ b/.github/workflows/deploy-posts.yml @@ -0,0 +1,37 @@ +name: deploy-posts + +on: + push: + branches: + - v2 + paths: + - packages/posts/** + - bun.lock + - .github/workflows/deploy-posts.yml + workflow_dispatch: + +concurrency: + group: deploy-posts-${{ github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + deploy: + if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: ./.github/actions/setup-bun + + - name: Build + working-directory: packages/posts + run: bun run build + + - name: Deploy + working-directory: packages/posts + run: bun run deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/packages/posts/.gitignore b/packages/posts/.gitignore new file mode 100644 index 00000000000..6ffb280fd3f --- /dev/null +++ b/packages/posts/.gitignore @@ -0,0 +1,4 @@ +.astro/ +dist/ +node_modules/ +.wrangler/ diff --git a/packages/posts/astro.config.ts b/packages/posts/astro.config.ts new file mode 100644 index 00000000000..03864dffa79 --- /dev/null +++ b/packages/posts/astro.config.ts @@ -0,0 +1,16 @@ +import cloudflare from "@astrojs/cloudflare" +import mdx from "@astrojs/mdx" +import { defineConfig } from "astro/config" + +export default defineConfig({ + site: "https://opencode.ai", + base: "/posts", + output: "server", + adapter: cloudflare({ imageService: "passthrough" }), + integrations: [mdx()], + vite: { + server: { + allowedHosts: true, + }, + }, +}) diff --git a/packages/posts/package.json b/packages/posts/package.json new file mode 100644 index 00000000000..1a3ec752673 --- /dev/null +++ b/packages/posts/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/posts", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build && bun script/prepare-cloudflare.ts", + "deploy": "wrangler deploy --config dist/server/wrangler.json", + "typecheck": "astro check" + }, + "dependencies": { + "@astrojs/mdx": "7.0.5", + "@fontsource/commit-mono": "5.3.0", + "astro": "7.1.3" + }, + "devDependencies": { + "@astrojs/cloudflare": "14.1.4", + "@astrojs/check": "0.9.6", + "@types/bun": "catalog:", + "typescript": "catalog:", + "wrangler": "4.110.0" + } +} diff --git a/packages/posts/public/logo-dark.svg b/packages/posts/public/logo-dark.svg new file mode 100644 index 00000000000..367289acc40 --- /dev/null +++ b/packages/posts/public/logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/posts/script/prepare-cloudflare.ts b/packages/posts/script/prepare-cloudflare.ts new file mode 100644 index 00000000000..164e96afd30 --- /dev/null +++ b/packages/posts/script/prepare-cloudflare.ts @@ -0,0 +1,10 @@ +export {} + +const path = "dist/server/wrangler.json" +const config = await Bun.file(path).json() + +delete config.kv_namespaces +delete config.images +delete config.previews + +await Bun.write(path, JSON.stringify(config)) diff --git a/packages/posts/src/components/Counter.astro b/packages/posts/src/components/Counter.astro new file mode 100644 index 00000000000..b395374bcf4 --- /dev/null +++ b/packages/posts/src/components/Counter.astro @@ -0,0 +1,45 @@ +
+ Clicks + 0 + +
+ + + + diff --git a/packages/posts/src/content.config.ts b/packages/posts/src/content.config.ts new file mode 100644 index 00000000000..6f8f9166b90 --- /dev/null +++ b/packages/posts/src/content.config.ts @@ -0,0 +1,14 @@ +import { defineCollection } from "astro:content" +import { glob } from "astro/loaders" +import { z } from "astro/zod" + +const posts = defineCollection({ + loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }), + schema: z.object({ + title: z.string(), + description: z.string(), + published: z.coerce.date(), + }), +}) + +export const collections = { posts } diff --git a/packages/posts/src/content/posts/hello.mdx b/packages/posts/src/content/posts/hello.mdx new file mode 100644 index 00000000000..9a480298b32 --- /dev/null +++ b/packages/posts/src/content/posts/hello.mdx @@ -0,0 +1,17 @@ +--- +title: "Example one" +description: "Markdown for the words, components for everything else." +published: 2026-08-20 +--- + +import Counter from "../../components/Counter.astro" + +This is a post written in MDX. Most of it is ordinary Markdown, rendered to static HTML with no client-side JavaScript. + +## Interactive when it matters + +Components can appear directly between paragraphs. This one is an Astro component with a tiny browser script: + + + +The component could also come from another workspace package or use React, Solid, Svelte, or Vue when the interaction calls for it. diff --git a/packages/posts/src/content/posts/inside-the-loop.mdx b/packages/posts/src/content/posts/inside-the-loop.mdx new file mode 100644 index 00000000000..976e3c2b5d9 --- /dev/null +++ b/packages/posts/src/content/posts/inside-the-loop.mdx @@ -0,0 +1,9 @@ +--- +title: "Lorem ipsum" +description: "A closer look at what happens between a prompt and a result." +published: 2026-08-07 +--- + +An agent alternates between reasoning, acting, and observing. The useful part is not any single step, but how quickly the loop incorporates new information. + +Interactive explanations let us inspect that process instead of reducing it to a static diagram. diff --git a/packages/posts/src/content/posts/tools-should-disappear.mdx b/packages/posts/src/content/posts/tools-should-disappear.mdx new file mode 100644 index 00000000000..cda666bb904 --- /dev/null +++ b/packages/posts/src/content/posts/tools-should-disappear.mdx @@ -0,0 +1,9 @@ +--- +title: "Example two" +description: "The best interface gets out of the way of the work." +published: 2026-08-14 +--- + +Software should make difficult work feel direct. Every control has to earn the attention it asks for. + +The goal is not fewer capabilities. It is less distance between intent and result. diff --git a/packages/posts/src/layouts/Layout.astro b/packages/posts/src/layouts/Layout.astro new file mode 100644 index 00000000000..3e253e1e75e --- /dev/null +++ b/packages/posts/src/layouts/Layout.astro @@ -0,0 +1,32 @@ +--- +import "@fontsource/commit-mono/400.css" +import "@fontsource/commit-mono/500.css" +import "../styles/global.css" + +interface Props { + title?: string + description?: string +} + +const title = Astro.props.title ? `${Astro.props.title} | OpenCode Posts` : "OpenCode Posts" +const description = Astro.props.description ?? "Notes from the OpenCode team." +--- + + + + + + + + + {title} + + +
+ + +
+ + diff --git a/packages/posts/src/pages/[...slug].astro b/packages/posts/src/pages/[...slug].astro new file mode 100644 index 00000000000..c1bc6305f34 --- /dev/null +++ b/packages/posts/src/pages/[...slug].astro @@ -0,0 +1,27 @@ +--- +import { getCollection, render } from "astro:content" +import Layout from "../layouts/Layout.astro" + +export const prerender = true + +export async function getStaticPaths() { + return (await getCollection("posts")).map((post) => ({ params: { slug: post.id }, props: { post } })) +} + +const post = Astro.props.post +const rendered = await render(post) +const Content = rendered.Content +--- + + +
+
+ +

{post.data.title}

+

{post.data.description}

+
+ +
+
diff --git a/packages/posts/src/pages/index.astro b/packages/posts/src/pages/index.astro new file mode 100644 index 00000000000..661657371ad --- /dev/null +++ b/packages/posts/src/pages/index.astro @@ -0,0 +1,25 @@ +--- +import { getCollection } from "astro:content" +import Layout from "../layouts/Layout.astro" + +export const prerender = true + +const posts = (await getCollection("posts")).sort( + (a, b) => b.data.published.valueOf() - a.data.published.valueOf(), +) +--- + + +
    + {posts.map((post) => ( +
  1. + + {post.data.title} + + +
  2. + ))} +
+
diff --git a/packages/posts/src/styles/global.css b/packages/posts/src/styles/global.css new file mode 100644 index 00000000000..f4739f5a38a --- /dev/null +++ b/packages/posts/src/styles/global.css @@ -0,0 +1,139 @@ +:root { + color-scheme: dark; + font-family: "Commit Mono", ui-monospace, monospace; + color: var(--foreground); + background: var(--background); + font-synthesis: none; + --foreground: #f1efe8; + --background: #131010; + --muted: rgb(241 239 232 / 45%); + --border: rgb(241 239 232 / 12%); + --code: rgb(255 255 255 / 6%); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: var(--background); + font-size: 18px; + line-height: 1.75; + letter-spacing: 0.02em; +} + +main { + width: min(100% - 2rem, 672px); + margin-inline: auto; + padding-block: 4rem; +} + +.site-logo { + display: block; + width: 8.75rem; + margin-bottom: 5rem; +} + +.site-logo img { + display: block; + width: 100%; + height: auto; +} + +h1, +h2 { + font-weight: 500; + letter-spacing: 0; +} + +h1 { + margin: 0 0 1rem; + color: #fff; + font-size: clamp(2rem, 8vw, 3rem); + line-height: 1.15; +} + +h2 { + margin: 3rem 0 1rem; + font-size: 1.25rem; +} + +p { + margin-block: 0 1.25rem; +} + +a { + color: inherit; + text-underline-offset: 0.2em; +} + +time, +.description { + color: var(--muted); +} + +.description { + font-size: 1.05rem; +} + +.post-header { + margin-bottom: 4rem; +} + +.post-header time { + display: block; + margin-bottom: 1rem; + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.post-list { + margin: 0; + padding: 0; + list-style: none; +} + +.post-list a { + display: grid; + grid-template-columns: 1fr auto; + align-items: baseline; + gap: 1rem; + margin-inline: -1.25rem; + padding: 1.1rem 1.25rem; + text-decoration: none; + transition: background-color 120ms ease; +} + +.post-list a:hover { + background: rgb(255 255 255 / 4%); +} + +.post-list span:first-child { + color: #fff; + font-size: 1.125rem; + font-weight: 500; +} + +.post-list time { + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +code { + padding: 0.15em 0.35em; + background: var(--code); + font: inherit; + font-size: 0.9em; +} + +@media (max-width: 480px) { + .post-list a { + grid-template-columns: 1fr; + gap: 0.25rem; + } +} diff --git a/packages/posts/tsconfig.json b/packages/posts/tsconfig.json new file mode 100644 index 00000000000..147cce17476 --- /dev/null +++ b/packages/posts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "types": ["bun"] + } +} diff --git a/packages/posts/wrangler.jsonc b/packages/posts/wrangler.jsonc new file mode 100644 index 00000000000..134a8a06633 --- /dev/null +++ b/packages/posts/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "opencode-posts", + "compatibility_date": "2026-07-25", + "workers_dev": false, + "preview_urls": false, + "routes": [ + { + "pattern": "opencode.ai/posts*", + "zone_name": "opencode.ai" + } + ] +}