diff --git a/.github/workflows/docs-skills-artifact.yml b/.github/workflows/docs-skills-artifact.yml
new file mode 100644
index 000000000000..a98a1daff9f2
--- /dev/null
+++ b/.github/workflows/docs-skills-artifact.yml
@@ -0,0 +1,68 @@
+name: Build docs skills artifact
+
+on:
+ schedule:
+ - cron: '0 6 * * *' # Daily at 6am UTC
+ workflow_dispatch:
+
+concurrency:
+ group: docs-skills-artifact
+ cancel-in-progress: true
+
+env:
+ NODE_OPTIONS: '--max_old_space_size=8192'
+
+jobs:
+ build-docs-artifact:
+ name: Build & upload docs zip
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@c5ba7f7862a0f64c1b1a05fbac13e0b8e86ba08c # v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+
+ - name: Cache Gatsby
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: |
+ .cache
+ public
+ key: gatsby-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'gatsby-config.js') }}
+ restore-keys: |
+ gatsby-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build site
+ run: pnpm build
+ env:
+ GATSBY_MINIMAL: 'true'
+ GENERATE_DOCS_MD: 'true'
+ GATSBY_SQUEAK_API_HOST: ${{ secrets.GATSBY_SQUEAK_API_HOST }}
+ CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
+ CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
+
+ - name: Create docs zip
+ run: |
+ cd public
+ find docs -name '*.md' | zip -@ -r ../posthog-docs-md.zip llms.txt
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: posthog-docs-md
+ path: posthog-docs-md.zip
+ retention-days: 30
diff --git a/gatsby/onPostBuild.ts b/gatsby/onPostBuild.ts
index f350f3b39cac..eebc7c75f983 100644
--- a/gatsby/onPostBuild.ts
+++ b/gatsby/onPostBuild.ts
@@ -486,7 +486,31 @@ const createOrUpdateStrapiPosts = async (posts, roadmaps) => {
}
export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter }) => {
- if (process.env.GATSBY_MINIMAL === 'true') return
+ if (process.env.GATSBY_MINIMAL === 'true') {
+ if (process.env.GENERATE_DOCS_MD === 'true') {
+ // Minimal build with GENERATE_DOCS_MD: only convert HTML to markdown and generate llms.txt
+ const markdownPathsRegex = `/^/(${MARKDOWN_CONTENT_PATHS.map((p) => p.replace('/', '')).join('|')})/`
+ const docsQuery = (await graphql(`
+ query {
+ allMdx(filter: { fields: { slug: { regex: "${markdownPathsRegex}" } } }) {
+ nodes {
+ fields {
+ slug
+ }
+ frontmatter {
+ title
+ }
+ }
+ }
+ }
+ `)) as { data: { allMdx: { nodes: Array<{ fields: { slug: string }; frontmatter: { title: string } }> } } }
+
+ const filteredPages = await generateRawMarkdownPages(docsQuery.data.allMdx.nodes)
+ const docsPages = filteredPages.filter((page) => page.fields.slug.startsWith('/docs'))
+ generateLlmsTxt(docsPages)
+ }
+ return
+ }
// Generate API spec markdown files first
try {
const openApiSpecUrl = process.env.POSTHOG_OPEN_API_SPEC_URL || 'https://app.posthog.com/api/schema/'
diff --git a/src/components/ReaderView/index.tsx b/src/components/ReaderView/index.tsx
index b0933f748a5c..499395b4b9cf 100644
--- a/src/components/ReaderView/index.tsx
+++ b/src/components/ReaderView/index.tsx
@@ -9,6 +9,8 @@ import {
IconRefresh,
IconClockRewind,
IconTextWidthFixed,
+ IconDownload,
+ IconCheck,
} from '@posthog/icons'
import ScrollArea from 'components/RadixUI/ScrollArea'
import { Select } from '../RadixUI/Select'
@@ -35,6 +37,7 @@ import SearchProvider from 'components/Editor/SearchProvider'
import { useLocation } from '@reach/router'
import { getProseClasses, isMarkdownContentPath } from '../../constants'
import { useWindow } from '../../context/Window'
+import { useToast } from '../../context/Toast'
import { MenuItem, useApp } from '../../context/App'
import { Questions } from 'components/Squeak'
import { navigate } from 'gatsby'
@@ -236,6 +239,63 @@ const EditOnGitHubButton = ({ filePath, sourceInstanceName }: { filePath?: strin
)
}
+const EXCLUDED_DOCS_PREFIXES = ['/docs/libraries/', '/docs/api/', '/docs/endpoints/', '/docs/open-api-spec/']
+
+const InstallSkillButton = ({ pathname }: { pathname: string }) => {
+ const { addToast } = useToast()
+ const [popoverOpen, setPopoverOpen] = React.useState(false)
+
+ if (!pathname.startsWith('/docs/')) return null
+ if (EXCLUDED_DOCS_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return null
+
+ const segments = pathname
+ .replace(/^\/docs\//, '')
+ .replace(/\/$/, '')
+ .split('/')
+ const firstSegment = segments[0]
+ if (!firstSegment) return null
+
+ const cleanSlug = firstSegment.startsWith('posthog-') ? firstSegment.slice(8) : firstSegment
+ const skillId = `posthog-docs-${cleanSlug}`
+ const downloadUrl = `https://github.com/PostHog/context-mill/releases/latest/download/${skillId}.zip`
+
+ const handleInstall = () => {
+ setPopoverOpen(false)
+ addToast({
+ description: (
+
+
+ Unzip into .claude/skills/ in your project to use as AI context
+
+ ),
+ duration: 5000,
+ })
+ }
+
+ return (
+
+ } />
+
+ }
+ dataScheme="secondary"
+ open={popoverOpen}
+ onOpenChange={setPopoverOpen}
+ >
+
+
+ Install as a skill
+
+
+ )
+}
+
const EditHistoryPopover = ({ commits }: { commits: any[] }) => {
if (!commits?.length || commits.length === 0) {
return null
@@ -1084,6 +1144,7 @@ function ReaderViewContent({
} ${websiteMode && showSidebar && isTocVisible ? '@6xl:bg-primary pl-0 box-content' : ''}`}
animate={showSidebar && isTocVisible ? 'open' : 'closed'}
>
+