feat(posts): add Astro posts site (#43720)

This commit is contained in:
Dax 2026-08-20 16:37:28 -04:00 committed by GitHub
parent ebc2504ef3
commit cc15c2a488
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 437 additions and 0 deletions

37
.github/workflows/deploy-posts.yml vendored Normal file
View file

@ -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 }}

4
packages/posts/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.astro/
dist/
node_modules/
.wrangler/

View file

@ -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,
},
},
})

View file

@ -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"
}
}

View file

@ -0,0 +1,10 @@
<svg width="641" height="115" viewBox="0 0 641 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M49.2857 32.8573H16.4286V82.143H49.2857V32.8573ZM65.7143 98.5716H0V16.4287H65.7143V98.5716Z" fill="white"/>
<path d="M98.5692 82.143H131.426V32.8573H98.5692V82.143ZM147.855 98.5716H98.5692V115H82.1406V16.4287H147.855V98.5716Z" fill="white"/>
<path d="M230.003 65.7144H180.718V82.143H230.003V98.5716H164.289V16.4287H230.003V65.7144ZM180.718 49.2859H213.575V32.8573H180.718V49.2859Z" fill="white"/>
<path d="M295.715 32.8573H262.858V98.5716H246.43V16.4287H295.715V32.8573ZM312.144 98.5716H295.715V32.8573H312.144V98.5716Z" fill="white"/>
<path d="M394.285 32.8573H344.999V82.143H394.285V98.5716H328.57V16.4287H394.285V32.8573Z" fill="white"/>
<path d="M459.997 32.8573H427.14V82.143H459.997V32.8573ZM476.425 98.5716H410.711V16.4287H476.425V98.5716Z" fill="white"/>
<path d="M542.145 32.8571H509.288V82.1429H542.145V32.8571ZM558.574 98.5714H492.859V16.4286H542.145V0H558.574V98.5714Z" fill="white"/>
<path d="M591.429 32.8573V49.2859H624.286V32.8573H591.429ZM640.714 65.7144H591.429V82.143H640.714V98.5716H575V16.4287H640.714V65.7144Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -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))

View file

@ -0,0 +1,45 @@
<div class="counter" data-counter>
<span>Clicks</span>
<output>0</output>
<button type="button">Increment</button>
</div>
<script>
document.querySelectorAll<HTMLElement>("[data-counter]").forEach((counter) => {
const output = counter.querySelector("output")
counter.querySelector("button")?.addEventListener("click", () => {
if (output) output.value = String(Number(output.value) + 1)
})
})
</script>
<style>
.counter {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0.75rem 1rem;
margin-block: 2.5rem;
padding: 1rem;
border: 1px solid var(--border);
background: var(--code);
}
output {
color: var(--muted);
}
button {
grid-column: 1 / -1;
padding: 0.65rem 1rem;
border: 0;
color: var(--background);
background: var(--foreground);
font: inherit;
cursor: pointer;
}
button:hover {
opacity: 0.85;
}
</style>

View file

@ -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 }

View file

@ -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:
<Counter />
The component could also come from another workspace package or use React, Solid, Svelte, or Vue when the interaction calls for it.

View file

@ -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.

View file

@ -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.

View file

@ -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."
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
<meta name="theme-color" content="#131010" />
<title>{title}</title>
</head>
<body>
<main>
<a class="site-logo" href="https://opencode.ai/" aria-label="OpenCode home">
<img src={`${import.meta.env.BASE_URL}/logo-dark.svg`} alt="OpenCode" />
</a>
<slot />
</main>
</body>
</html>

View file

@ -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
---
<Layout title={post.data.title} description={post.data.description}>
<article>
<div class="post-header">
<time datetime={post.data.published.toISOString()}>
{post.data.published.toLocaleDateString("en", { month: "long", day: "numeric", year: "numeric" })}
</time>
<h1>{post.data.title}</h1>
<p class="description">{post.data.description}</p>
</div>
<Content />
</article>
</Layout>

View file

@ -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(),
)
---
<Layout>
<ol class="post-list">
{posts.map((post) => (
<li>
<a href={`${import.meta.env.BASE_URL}/${post.id}/`}>
<span>{post.data.title}</span>
<time datetime={post.data.published.toISOString()}>
{post.data.published.toLocaleDateString("en", { month: "short", day: "numeric", year: "numeric" })}
</time>
</a>
</li>
))}
</ol>
</Layout>

View file

@ -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;
}
}

View file

@ -0,0 +1,6 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"types": ["bun"]
}
}

View file

@ -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"
}
]
}