Add an Astro content-collection blog to the marketing site: - /blog index listing posts and /blog/[slug] prerendered post pages - BlogPostLayout with prose styling and a top-of-post author card (round photo avatar, name, role + date) linking to the author's X - authorHandle/authorRole/authorAvatar fields on the blog schema with sensible defaults - Blog nav links in the homepage header and footer First post: "Why MCP didn't work, what's next".
Unified · Split
Binary file not shown.
| @@ -0,0 +1,24 @@ | |||
| 1 | import { defineCollection, z } from "astro:content"; | ||
| 2 | import { glob } from "astro/loaders"; | ||
| 3 | |||
| 4 | // Blog posts authored as Markdown under src/content/blog. The glob loader | ||
| 5 | // turns each file into a collection entry whose `id` is the slug used in the | ||
| 6 | // /blog/[...slug] route (filename without extension). | ||
| 7 | const blog = defineCollection({ | ||
| 8 | loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), | ||
| 9 | schema: z.object({ | ||
| 10 | title: z.string(), | ||
| 11 | description: z.string(), | ||
| 12 | date: z.coerce.date(), | ||
| 13 | author: z.string().default("Rhys Sullivan"), | ||
| 14 | // X/Twitter handle (without the leading @), a one-line role, and a round | ||
| 15 | // avatar image, used by the author card at the top of each post. The | ||
| 16 | // avatar path is served from public/ (e.g. /authors/rhys-sullivan.png). | ||
| 17 | authorHandle: z.string().default("RhysSullivan"), | ||
| 18 | authorRole: z.string().default("Founder of Executor"), | ||
| 19 | authorAvatar: z.string().default("/authors/rhys-sullivan.png"), | ||
| 20 | draft: z.boolean().default(false), | ||
| 21 | }), | ||
| 22 | }); | ||
| 23 | |||
| 24 | export const collections = { blog }; | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | --- | ||
| 2 | title: "Why MCP didn't work, what's next" | ||
| 3 | description: "MCP shipped before anyone knew how to wire tools to agents, and it caught the blame for it. What actually went wrong, why CLIs aren't the answer, and where MCP goes from here." | ||
| 4 | date: 2026-06-25 | ||
| 5 | author: "Rhys Sullivan" | ||
| 6 | --- | ||
| 7 | |||
| 8 | MCP launched when the best models we had were Claude 3.5 Sonnet and GPT-4o. At the time, we did not know how to build with these tools. We were paranoid about security, context bloat, and granting access to models. | ||
| 9 | |||
| 10 | Consequently, early MCP servers were useless. Vercel's launch MCP server had only 7 tools and limited utility. Paranoid about security, companies restricted access to the point of making the servers unviable. | ||
| 11 | |||
| 12 | When Claude Code launched in February, developers realized agents just needed a bash tool. Giving agents bash access let them chain commands, filter output, and dynamically install CLIs. This shift led to a common belief: the fewer tools a model has, the better it performs. That belief is false. The GitHub CLI contains thousands of tools, especially since a model can call the API directly using `gh api`. | ||
| 13 | |||
| 14 | Compare using MCP to using a CLI. With a CLI, you tell the agent to install the package and use it immediately. Most MCP harnesses require a full client restart to register a new tool, creating significant friction. Bash benefits from 37 years of development on stable primitives, while developers rushed out most MCP implementations in a month. | ||
| 15 | |||
| 16 | MCP is in an awkward state. Vercel added `vercel api` to its CLI, giving agents access to the entire API. Yet Vercel's MCP server remains restricted to 20 tools, most of which are read-only. Additionally, only 16 clients are authorized to integrate with it. This friction makes no sense when developers routinely run the CLI in environments with skipped permissions. | ||
| 17 | |||
| 18 | Should we abandon MCP for CLIs? I disagree. Most CLIs merely wrap APIs. Focusing on CLI authentication while neglecting API authentication introduces unnecessary statefulness. An agent reading emails should not spin up a container to run the Google Workspace CLI when lazy tool loading or code mode works. | ||
| 19 | |||
| 20 | CLIs also fail to map the action space. You can convert an MCP tool into a CLI, but you cannot easily reverse the process. CLIs also lack the explicit annotations for destructive actions found in MCP or OpenAPI specifications, though improving LLMs make this less critical. | ||
| 21 | |||
| 22 | ## Myths surrounding MCP today | ||
| 23 | |||
| 24 | **"Lots of tools bloat the context."** This is only true for poorly implemented clients. Most modern clients use lazy loading, letting the model discover tools on demand. | ||
| 25 | |||
| 26 | **"Poor support of the spec."** While the spec is not fully supported everywhere, developers can work around missing features. We must solve this adoption chicken-and-egg problem. | ||
| 27 | |||
| 28 | **"Auth sucks."** Some clients have poor authorization support. However, in Claude Code, I never get signed out. | ||
| 29 | |||
| 30 | **"We should just be calling the APIs directly."** I agree. I founded [executor.sh](https://executor.sh) on this premise. Harnesses should support any integration method, whether it is MCP, CLI, API, or GraphQL. Executor functions by accepting any input format and converting it into a standard tool catalog. | ||
| 31 | |||
| 32 | ## Where do we go from here? | ||
| 33 | |||
| 34 | Harness builders like Codex, Claude Code, and OpenCode will decide whether to commit to MCP. If these teams collaborate on an alternative specification with matching adoption, we should welcome it. From my perspective, MCP's broad adoption has solved the hardest challenge. Now we must focus on refining the implementation. | ||
| 35 | |||
| 36 | I am optimistic about the future of MCP. Stateless elicitation is powerful, and client adoption of code mode is rising. MCP applications will unlock new interaction patterns over the next six months. Skill loading through MCP will also provide clear context on how to call remote sources. | ||
| 37 | |||
| 38 | Ideally, companies will support CIMD OAuth for both their MCP servers and OpenAPI specifications. Developers can use the OpenAPI spec for raw data access and bulk processing, while the MCP server provides a curated experience using an `api` tool modeled after the Vercel and GitHub CLIs. | ||
| 39 | |||
| 40 | ## Some closing thoughts | ||
| 41 | |||
| 42 | Avoid plugins. They create vendor lock-in and portability issues, resulting in closed ecosystems. | ||
| 43 | |||
| 44 | This technology is new, and criticizing it is easier than building it. MCP has succeeded in driving widespread adoption. | ||
| 45 | |||
| 46 | Ultimately, most MCP servers and CLIs are just wrappers for direct API calls. | ||
| @@ -0,0 +1,182 @@ | |||
| 1 | --- | ||
| 2 | import Layout from "./Layout.astro"; | ||
| 3 | |||
| 4 | interface Props { | ||
| 5 | title: string; | ||
| 6 | description: string; | ||
| 7 | date: Date; | ||
| 8 | author: string; | ||
| 9 | authorHandle: string; | ||
| 10 | authorRole: string; | ||
| 11 | authorAvatar: string; | ||
| 12 | } | ||
| 13 | |||
| 14 | const { | ||
| 15 | title, | ||
| 16 | description, | ||
| 17 | date, | ||
| 18 | author, | ||
| 19 | authorHandle, | ||
| 20 | authorRole, | ||
| 21 | authorAvatar, | ||
| 22 | } = Astro.props; | ||
| 23 | |||
| 24 | const GH = "https://github.com/RhysSullivan/executor"; | ||
| 25 | const authorX = `https://x.com/${authorHandle}`; | ||
| 26 | |||
| 27 | const formattedDate = date.toLocaleDateString("en-US", { | ||
| 28 | year: "numeric", | ||
| 29 | month: "long", | ||
| 30 | day: "numeric", | ||
| 31 | // Frontmatter dates are bare YYYY-MM-DD (parsed as UTC midnight); format in | ||
| 32 | // UTC so the displayed day matches the date the author wrote, regardless of | ||
| 33 | // the build/runtime timezone. | ||
| 34 | timeZone: "UTC", | ||
| 35 | }); | ||
| 36 | const isoDate = date.toISOString(); | ||
| 37 | --- | ||
| 38 | |||
| 39 | <Layout title={`${title} — Executor`} description={description}> | ||
| 40 | <main class="bg-soft relative min-h-dvh antialiased"> | ||
| 41 | {/* Graph-paper pattern + fade behind the header, matching the homepage */} | ||
| 42 | <div | ||
| 43 | class="pattern-bg pattern-graph" | ||
| 44 | style="position:absolute;top:0;left:0;right:0;height:60vh;z-index:1;pointer-events:none;" | ||
| 45 | > | ||
| 46 | </div> | ||
| 47 | <div | ||
| 48 | style="position:absolute;top:0;left:0;right:0;height:60vh;z-index:2;pointer-events:none;background:linear-gradient(180deg,transparent 0%,transparent 55%,var(--color-surface) 100%);" | ||
| 49 | > | ||
| 50 | </div> | ||
| 51 | |||
| 52 | <div class="relative z-10"> | ||
| 53 | {/* ─── NAV ─── */} | ||
| 54 | <nav> | ||
| 55 | <div | ||
| 56 | class="mx-auto flex h-14 max-w-[1200px] items-center justify-between px-6 sm:px-10" | ||
| 57 | > | ||
| 58 | <a href="/" class="flex items-center gap-2.5"> | ||
| 59 | <img src="/favicon-192.png" alt="" class="h-5 w-5" /> | ||
| 60 | <span class="text-[15px] font-semibold tracking-[-0.01em] text-ink" | ||
| 61 | >executor</span | ||
| 62 | > | ||
| 63 | </a> | ||
| 64 | <div class="flex items-center gap-5"> | ||
| 65 | <a | ||
| 66 | href="/blog" | ||
| 67 | class="text-[14px] font-medium text-ink-2 transition-colors hover:text-ink" | ||
| 68 | >Blog</a | ||
| 69 | > | ||
| 70 | <a | ||
| 71 | href="https://executor.sh/docs" | ||
| 72 | class="hidden text-[14px] font-medium text-ink-2 transition-colors hover:text-ink sm:inline" | ||
| 73 | >Docs</a | ||
| 74 | > | ||
| 75 | <a | ||
| 76 | href={GH} | ||
| 77 | class="text-[14px] font-medium text-ink-2 transition-colors hover:text-ink" | ||
| 78 | >GitHub</a | ||
| 79 | > | ||
| 80 | <a href="/#install" class="btn-primary">Get started →</a> | ||
| 81 | </div> | ||
| 82 | </div> | ||
| 83 | </nav> | ||
| 84 | |||
| 85 | {/* ─── HEADER ─── */} | ||
| 86 | <section class="px-6 pt-16 pb-10 sm:px-10 sm:pt-24 sm:pb-14"> | ||
| 87 | <div class="mx-auto max-w-[720px]"> | ||
| 88 | <a | ||
| 89 | href="/blog" | ||
| 90 | class="eyebrow inline-flex items-center gap-1.5 transition-colors hover:text-ink-2" | ||
| 91 | >← Blog</a | ||
| 92 | > | ||
| 93 | <h1 | ||
| 94 | class="mt-5 text-[clamp(2.2rem,5vw,3.6rem)] font-semibold leading-[1.05] tracking-[-0.025em] text-ink text-balance" | ||
| 95 | > | ||
| 96 | {title} | ||
| 97 | </h1> | ||
| 98 | <p class="mt-5 text-[17px] leading-[1.6] text-ink-2 text-pretty"> | ||
| 99 | {description} | ||
| 100 | </p> | ||
| 101 | <div class="mt-8 flex items-center gap-3"> | ||
| 102 | <a | ||
| 103 | href={authorX} | ||
| 104 | target="_blank" | ||
| 105 | rel="noopener noreferrer" | ||
| 106 | class="shrink-0" | ||
| 107 | aria-label={`${author} on X`} | ||
| 108 | > | ||
| 109 | <img | ||
| 110 | src={authorAvatar} | ||
| 111 | alt={author} | ||
| 112 | width="44" | ||
| 113 | height="44" | ||
| 114 | loading="lazy" | ||
| 115 | class="h-11 w-11 rounded-full object-cover ring-1 ring-rule" | ||
| 116 | /> | ||
| 117 | </a> | ||
| 118 | <div class="min-w-0"> | ||
| 119 | <a | ||
| 120 | href={authorX} | ||
| 121 | target="_blank" | ||
| 122 | rel="noopener noreferrer" | ||
| 123 | class="text-[15px] font-semibold text-ink transition-colors hover:text-accent" | ||
| 124 | >{author}</a | ||
| 125 | > | ||
| 126 | <div class="mt-0.5 text-[13px] leading-snug text-ink-3"> | ||
| 127 | {authorRole} · <time datetime={isoDate}>{formattedDate}</time> | ||
| 128 | </div> | ||
| 129 | </div> | ||
| 130 | </div> | ||
| 131 | </div> | ||
| 132 | </section> | ||
| 133 | |||
| 134 | {/* ─── ARTICLE ─── */} | ||
| 135 | <section class="px-6 pt-2 pb-20 sm:px-10 sm:pb-28"> | ||
| 136 | <article class="blog-prose mx-auto max-w-[720px]"> | ||
| 137 | <slot /> | ||
| 138 | </article> | ||
| 139 | </section> | ||
| 140 | |||
| 141 | {/* ─── BOTTOM CTA ─── */} | ||
| 142 | <section class="border-t border-rule py-16 sm:py-20"> | ||
| 143 | <div class="mx-auto max-w-[720px] px-6 text-center sm:px-10"> | ||
| 144 | <h2 class="section-title mx-auto mb-7 max-w-[20ch]"> | ||
| 145 | Connect any agent to <span class="serif-italic">everything</span>. | ||
| 146 | </h2> | ||
| 147 | <div class="flex flex-wrap items-center justify-center gap-3"> | ||
| 148 | <a href="/#install" class="btn-primary btn-primary--hero" | ||
| 149 | >Get started →</a | ||
| 150 | > | ||
| 151 | <a href={GH} class="btn-secondary">Star on GitHub</a> | ||
| 152 | </div> | ||
| 153 | </div> | ||
| 154 | </section> | ||
| 155 | |||
| 156 | {/* ─── FOOTER ─── */} | ||
| 157 | <footer class="border-t border-rule py-10"> | ||
| 158 | <div | ||
| 159 | class="mx-auto flex max-w-[1200px] flex-col items-start justify-between gap-6 px-6 sm:flex-row sm:items-center sm:px-10" | ||
| 160 | > | ||
| 161 | <div class="flex items-center gap-2.5"> | ||
| 162 | <img src="/favicon-192.png" alt="" class="h-4 w-4 opacity-80" /> | ||
| 163 | <span class="text-[13px] font-medium text-ink-2">executor</span> | ||
| 164 | <span class="ml-2 text-[12px] text-ink-3" | ||
| 165 | >© {new Date().getFullYear()}</span | ||
| 166 | > | ||
| 167 | </div> | ||
| 168 | <div class="flex items-center gap-6 text-[13px] text-ink-2"> | ||
| 169 | <a href="/blog" class="transition-colors hover:text-ink">Blog</a> | ||
| 170 | <a | ||
| 171 | href="https://executor.sh/docs" | ||
| 172 | class="transition-colors hover:text-ink">Docs</a | ||
| 173 | > | ||
| 174 | <a href={GH} class="transition-colors hover:text-ink">GitHub</a> | ||
| 175 | <a href="/privacy" class="transition-colors hover:text-ink">Privacy</a> | ||
| 176 | <a href="/terms" class="transition-colors hover:text-ink">Terms</a> | ||
| 177 | </div> | ||
| 178 | </div> | ||
| 179 | </footer> | ||
| 180 | </div> | ||
| 181 | </main> | ||
| 182 | </Layout> | ||
| @@ -0,0 +1,28 @@ | |||
| 1 | --- | ||
| 2 | export const prerender = true; | ||
| 3 | import { getCollection, render } from "astro:content"; | ||
| 4 | import BlogPostLayout from "../../layouts/BlogPostLayout.astro"; | ||
| 5 | |||
| 6 | export async function getStaticPaths() { | ||
| 7 | const posts = await getCollection("blog", ({ data }) => !data.draft); | ||
| 8 | return posts.map((post) => ({ | ||
| 9 | params: { slug: post.id }, | ||
| 10 | props: { post }, | ||
| 11 | })); | ||
| 12 | } | ||
| 13 | |||
| 14 | const { post } = Astro.props; | ||
| 15 | const { Content } = await render(post); | ||
| 16 | --- | ||
| 17 | |||
| 18 | <BlogPostLayout | ||
| 19 | title={post.data.title} | ||
| 20 | description={post.data.description} | ||
| 21 | date={post.data.date} | ||
| 22 | author={post.data.author} | ||
| 23 | authorHandle={post.data.authorHandle} | ||
| 24 | authorRole={post.data.authorRole} | ||
| 25 | authorAvatar={post.data.authorAvatar} | ||
| 26 | > | ||
| 27 | <Content /> | ||
| 28 | </BlogPostLayout> | ||
| @@ -0,0 +1,144 @@ | |||
| 1 | --- | ||
| 2 | export const prerender = true; | ||
| 3 | import Layout from "../../layouts/Layout.astro"; | ||
| 4 | import { getCollection } from "astro:content"; | ||
| 5 | |||
| 6 | const GH = "https://github.com/RhysSullivan/executor"; | ||
| 7 | |||
| 8 | const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort( | ||
| 9 | (a, b) => b.data.date.getTime() - a.data.date.getTime(), | ||
| 10 | ); | ||
| 11 | |||
| 12 | const fmt = (date: Date) => | ||
| 13 | date.toLocaleDateString("en-US", { | ||
| 14 | year: "numeric", | ||
| 15 | month: "long", | ||
| 16 | day: "numeric", | ||
| 17 | }); | ||
| 18 | --- | ||
| 19 | |||
| 20 | <Layout | ||
| 21 | title="Blog — Executor" | ||
| 22 | description="Notes on agents, tools, and the integration layer underneath them, from the team building Executor." | ||
| 23 | > | ||
| 24 | <main class="bg-soft relative min-h-dvh antialiased"> | ||
| 25 | <div | ||
| 26 | class="pattern-bg pattern-graph" | ||
| 27 | style="position:absolute;top:0;left:0;right:0;height:60vh;z-index:1;pointer-events:none;" | ||
| 28 | > | ||
| 29 | </div> | ||
| 30 | <div | ||
| 31 | style="position:absolute;top:0;left:0;right:0;height:60vh;z-index:2;pointer-events:none;background:linear-gradient(180deg,transparent 0%,transparent 55%,var(--color-surface) 100%);" | ||
| 32 | > | ||
| 33 | </div> | ||
| 34 | |||
| 35 | <div class="relative z-10"> | ||
| 36 | {/* ─── NAV ─── */} | ||
| 37 | <nav> | ||
| 38 | <div | ||
| 39 | class="mx-auto flex h-14 max-w-[1200px] items-center justify-between px-6 sm:px-10" | ||
| 40 | > | ||
| 41 | <a href="/" class="flex items-center gap-2.5"> | ||
| 42 | <img src="/favicon-192.png" alt="" class="h-5 w-5" /> | ||
| 43 | <span class="text-[15px] font-semibold tracking-[-0.01em] text-ink" | ||
| 44 | >executor</span | ||
| 45 | > | ||
| 46 | </a> | ||
| 47 | <div class="flex items-center gap-5"> | ||
| 48 | <a | ||
| 49 | href="/blog" | ||
| 50 | class="text-[14px] font-medium text-ink transition-colors">Blog</a | ||
| 51 | > | ||
| 52 | <a | ||
| 53 | href="https://executor.sh/docs" | ||
| 54 | class="hidden text-[14px] font-medium text-ink-2 transition-colors hover:text-ink sm:inline" | ||
| 55 | >Docs</a | ||
| 56 | > | ||
| 57 | <a | ||
| 58 | href={GH} | ||
| 59 | class="text-[14px] font-medium text-ink-2 transition-colors hover:text-ink" | ||
| 60 | >GitHub</a | ||
| 61 | > | ||
| 62 | <a href="/#install" class="btn-primary">Get started →</a> | ||
| 63 | </div> | ||
| 64 | </div> | ||
| 65 | </nav> | ||
| 66 | |||
| 67 | {/* ─── HEADER ─── */} | ||
| 68 | <section class="px-6 pt-16 pb-10 sm:px-10 sm:pt-24 sm:pb-12"> | ||
| 69 | <div class="mx-auto max-w-[760px]"> | ||
| 70 | <div class="eyebrow mb-4">Blog</div> | ||
| 71 | <h1 | ||
| 72 | class="text-[clamp(2.4rem,5.6vw,4rem)] font-semibold leading-[1.02] tracking-[-0.025em] text-ink" | ||
| 73 | > | ||
| 74 | Field notes on <span class="serif-italic">agents</span> and tools. | ||
| 75 | </h1> | ||
| 76 | <p class="mt-5 max-w-[54ch] text-[17px] leading-[1.6] text-ink-2 text-pretty"> | ||
| 77 | Thinking from the team building Executor, the integration layer that | ||
| 78 | connects any agent to everything. | ||
| 79 | </p> | ||
| 80 | </div> | ||
| 81 | </section> | ||
| 82 | |||
| 83 | {/* ─── POST LIST ─── */} | ||
| 84 | <section class="px-6 pb-24 sm:px-10 sm:pb-32"> | ||
| 85 | <div class="mx-auto max-w-[760px]"> | ||
| 86 | { | ||
| 87 | posts.length === 0 ? ( | ||
| 88 | <p class="text-[15px] text-ink-3">No posts yet. Check back soon.</p> | ||
| 89 | ) : ( | ||
| 90 | <ul class="flex flex-col gap-4"> | ||
| 91 | {posts.map((post) => ( | ||
| 92 | <li> | ||
| 93 | <a | ||
| 94 | href={`/blog/${post.id}`} | ||
| 95 | class="surface-card group block p-6 transition-colors sm:p-7" | ||
| 96 | > | ||
| 97 | <div class="mb-2 font-mono text-[12px] uppercase tracking-[0.14em] text-ink-3"> | ||
| 98 | {fmt(post.data.date)} | ||
| 99 | </div> | ||
| 100 | <h2 class="text-[20px] font-semibold tracking-[-0.01em] text-ink sm:text-[22px]"> | ||
| 101 | {post.data.title} | ||
| 102 | </h2> | ||
| 103 | <p class="mt-2 text-[14.5px] leading-[1.55] text-ink-2 text-pretty"> | ||
| 104 | {post.data.description} | ||
| 105 | </p> | ||
| 106 | <span class="mt-4 inline-flex items-center gap-1 text-[13.5px] font-medium text-ink-2 transition-colors group-hover:text-ink"> | ||
| 107 | Read post → | ||
| 108 | </span> | ||
| 109 | </a> | ||
| 110 | </li> | ||
| 111 | ))} | ||
| 112 | </ul> | ||
| 113 | ) | ||
| 114 | } | ||
| 115 | </div> | ||
| 116 | </section> | ||
| 117 | |||
| 118 | {/* ─── FOOTER ─── */} | ||
| 119 | <footer class="border-t border-rule py-10"> | ||
| 120 | <div | ||
| 121 | class="mx-auto flex max-w-[1200px] flex-col items-start justify-between gap-6 px-6 sm:flex-row sm:items-center sm:px-10" | ||
| 122 | > | ||
| 123 | <div class="flex items-center gap-2.5"> | ||
| 124 | <img src="/favicon-192.png" alt="" class="h-4 w-4 opacity-80" /> | ||
| 125 | <span class="text-[13px] font-medium text-ink-2">executor</span> | ||
| 126 | <span class="ml-2 text-[12px] text-ink-3" | ||
| 127 | >© {new Date().getFullYear()}</span | ||
| 128 | > | ||
| 129 | </div> | ||
| 130 | <div class="flex items-center gap-6 text-[13px] text-ink-2"> | ||
| 131 | <a href="/blog" class="transition-colors hover:text-ink">Blog</a> | ||
| 132 | <a | ||
| 133 | href="https://executor.sh/docs" | ||
| 134 | class="transition-colors hover:text-ink">Docs</a | ||
| 135 | > | ||
| 136 | <a href={GH} class="transition-colors hover:text-ink">GitHub</a> | ||
| 137 | <a href="/privacy" class="transition-colors hover:text-ink">Privacy</a> | ||
| 138 | <a href="/terms" class="transition-colors hover:text-ink">Terms</a> | ||
| 139 | </div> | ||
| 140 | </div> | ||
| 141 | </footer> | ||
| 142 | </div> | ||
| 143 | </main> | ||
| 144 | </Layout> | ||
| @@ -83,6 +83,11 @@ Source (and the place to start if something breaks): https://github.com/RhysSull · 82 unmodified lines | |||
| 83 | class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline" | 83 | class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline" |
| 84 | >Docs</a | 84 | >Docs</a |
| 85 | > | 85 | > |
| 86 | <a | ||
| 87 | href="/blog" | ||
| 88 | class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline" | ||
| 89 | >Blog</a | ||
| 90 | > | ||
| 86 | <a | 91 | <a |
| 87 | href={GH} | 92 | href={GH} |
| 88 | class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors" | 93 | class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors" |
| @@ -762,6 +767,7 @@ Source (and the place to start if something breaks): https://github.com/RhysSull · 673 unmodified lines | |||
| 762 | <a href="https://executor.sh/docs" class="hover:text-ink transition-colors" | 767 | <a href="https://executor.sh/docs" class="hover:text-ink transition-colors" |
| 763 | >Docs</a | 768 | >Docs</a |
| 764 | > | 769 | > |
| 770 | <a href="/blog" class="hover:text-ink transition-colors">Blog</a> | ||
| 765 | <a href={GH} class="hover:text-ink transition-colors">GitHub</a> | 771 | <a href={GH} class="hover:text-ink transition-colors">GitHub</a> |
| 766 | <a href="/privacy" class="hover:text-ink transition-colors">Privacy</a> | 772 | <a href="/privacy" class="hover:text-ink transition-colors">Privacy</a> |
| 767 | <a href="/terms" class="hover:text-ink transition-colors">Terms</a> | 773 | <a href="/terms" class="hover:text-ink transition-colors">Terms</a> |
| @@ -1519,3 +1519,137 @@ body { · 1518 unmodified lines | |||
| 1519 | transition: none; | 1519 | transition: none; |
| 1520 | } | 1520 | } |
| 1521 | } | 1521 | } |
| 1522 | |||
| 1523 | /* ════════════════════════════════════════════════════════════════ | ||
| 1524 | BLOG: editorial prose for Markdown-rendered posts (/blog/[slug]). | ||
| 1525 | Reads off the same tokens as the rest of the site (ink / accent / | ||
| 1526 | rule / surface) so posts stay on-brand without a separate palette. | ||
| 1527 | ═══════════════════════════════════════════════════════════════ */ | ||
| 1528 | .blog-prose { | ||
| 1529 | color: color-mix(in srgb, var(--color-ink) 88%, transparent); | ||
| 1530 | font-size: 17px; | ||
| 1531 | line-height: 1.75; | ||
| 1532 | letter-spacing: -0.011em; | ||
| 1533 | } | ||
| 1534 | .blog-prose > :first-child { | ||
| 1535 | margin-top: 0; | ||
| 1536 | } | ||
| 1537 | .blog-prose p { | ||
| 1538 | margin: 1.25rem 0; | ||
| 1539 | text-wrap: pretty; | ||
| 1540 | } | ||
| 1541 | .blog-prose h2 { | ||
| 1542 | margin-top: 2.75rem; | ||
| 1543 | margin-bottom: 0.9rem; | ||
| 1544 | font-family: var(--font-sans); | ||
| 1545 | font-size: clamp(1.4rem, 2.6vw, 1.7rem); | ||
| 1546 | font-weight: 600; | ||
| 1547 | letter-spacing: -0.02em; | ||
| 1548 | line-height: 1.2; | ||
| 1549 | color: var(--color-ink); | ||
| 1550 | } | ||
| 1551 | .blog-prose h3 { | ||
| 1552 | margin-top: 2rem; | ||
| 1553 | margin-bottom: 0.5rem; | ||
| 1554 | font-size: 1.18rem; | ||
| 1555 | font-weight: 600; | ||
| 1556 | letter-spacing: -0.01em; | ||
| 1557 | color: var(--color-ink); | ||
| 1558 | } | ||
| 1559 | .blog-prose ul, | ||
| 1560 | .blog-prose ol { | ||
| 1561 | margin: 1.25rem 0; | ||
| 1562 | padding-left: 0; | ||
| 1563 | list-style: none; | ||
| 1564 | } | ||
| 1565 | .blog-prose li { | ||
| 1566 | position: relative; | ||
| 1567 | margin: 0.5rem 0; | ||
| 1568 | padding-left: 1.4rem; | ||
| 1569 | } | ||
| 1570 | .blog-prose ul > li::before { | ||
| 1571 | content: ""; | ||
| 1572 | position: absolute; | ||
| 1573 | left: 0.2rem; | ||
| 1574 | top: 0.78rem; | ||
| 1575 | width: 5px; | ||
| 1576 | height: 5px; | ||
| 1577 | border-radius: 50%; | ||
| 1578 | background: var(--color-accent); | ||
| 1579 | opacity: 0.75; | ||
| 1580 | } | ||
| 1581 | .blog-prose ol { | ||
| 1582 | counter-reset: blog-ol; | ||
| 1583 | } | ||
| 1584 | .blog-prose ol > li { | ||
| 1585 | counter-increment: blog-ol; | ||
| 1586 | } | ||
| 1587 | .blog-prose ol > li::before { | ||
| 1588 | content: counter(blog-ol) "."; | ||
| 1589 | position: absolute; | ||
| 1590 | left: 0; | ||
| 1591 | top: 0; | ||
| 1592 | font-variant-numeric: tabular-nums; | ||
| 1593 | font-weight: 600; | ||
| 1594 | color: var(--color-accent); | ||
| 1595 | } | ||
| 1596 | .blog-prose a { | ||
| 1597 | color: var(--color-accent); | ||
| 1598 | text-decoration: underline; | ||
| 1599 | text-decoration-color: color-mix(in srgb, var(--color-accent) 35%, transparent); | ||
| 1600 | text-underline-offset: 3px; | ||
| 1601 | transition: | ||
| 1602 | color 0.15s ease, | ||
| 1603 | text-decoration-color 0.15s ease; | ||
| 1604 | } | ||
| 1605 | .blog-prose a:hover { | ||
| 1606 | color: var(--color-accent-hover); | ||
| 1607 | text-decoration-color: var(--color-accent-hover); | ||
| 1608 | } | ||
| 1609 | .blog-prose strong { | ||
| 1610 | color: var(--color-ink); | ||
| 1611 | font-weight: 600; | ||
| 1612 | } | ||
| 1613 | .blog-prose em { | ||
| 1614 | font-style: italic; | ||
| 1615 | } | ||
| 1616 | .blog-prose blockquote { | ||
| 1617 | margin: 1.5rem 0; | ||
| 1618 | padding: 0.25rem 0 0.25rem 1.25rem; | ||
| 1619 | border-left: 2px solid color-mix(in srgb, var(--color-accent) 55%, transparent); | ||
| 1620 | color: var(--color-ink-2); | ||
| 1621 | font-style: italic; | ||
| 1622 | } | ||
| 1623 | .blog-prose code { | ||
| 1624 | font-family: var(--font-mono); | ||
| 1625 | font-size: 0.86em; | ||
| 1626 | background: var(--color-surface-2); | ||
| 1627 | border: 1px solid var(--color-rule); | ||
| 1628 | border-radius: 5px; | ||
| 1629 | padding: 0.12em 0.4em; | ||
| 1630 | } | ||
| 1631 | .blog-prose pre { | ||
| 1632 | margin: 1.5rem 0; | ||
| 1633 | padding: 1.1rem 1.25rem; | ||
| 1634 | background: var(--color-surface-2); | ||
| 1635 | border: 1px solid var(--color-rule); | ||
| 1636 | border-radius: 12px; | ||
| 1637 | overflow-x: auto; | ||
| 1638 | font-size: 14px; | ||
| 1639 | line-height: 1.6; | ||
| 1640 | } | ||
| 1641 | .blog-prose pre code { | ||
| 1642 | background: transparent; | ||
| 1643 | border: 0; | ||
| 1644 | padding: 0; | ||
| 1645 | font-size: inherit; | ||
| 1646 | } | ||
| 1647 | .blog-prose hr { | ||
| 1648 | margin: 2.5rem 0; | ||
| 1649 | border: 0; | ||
| 1650 | border-top: 1px solid var(--color-rule); | ||
| 1651 | } | ||
| 1652 | .blog-prose h2 + p, | ||
| 1653 | .blog-prose h3 + p { | ||
| 1654 | margin-top: 0; | ||
| 1655 | } | ||