|
| 1 | +--- |
| 2 | +title: "Deploying Explainer to production: a complete guide" |
| 3 | +description: From local development to production deployment — learn how to ship your Explainer documentation to Cloudflare Pages, Vercel, or any static hosting provider with CI/CD automation. |
| 4 | +short_description: "Ship your docs to production with Cloudflare, Vercel, or Docker." |
| 5 | +date: 2026-03-16 |
| 6 | +tags: [guide, deployment, ci-cd, cloudflare] |
| 7 | +status: published |
| 8 | +author: leadcode_dev |
| 9 | +--- |
| 10 | + |
| 11 | +You've written your documentation, customized the theme, and everything looks great locally. Now it's time to share it with the world. In this guide, we'll cover the full deployment story — from build configuration to CI/CD pipelines. |
| 12 | + |
| 13 | +## Understanding the build output |
| 14 | + |
| 15 | +Explainer apps are standard **Astro static sites**. Running `pnpm build` produces a `dist/` directory with plain HTML, CSS, and JavaScript files. No server runtime required — you can host the output anywhere that serves static files. |
| 16 | + |
| 17 | +```bash |
| 18 | +# Build all three apps |
| 19 | +pnpm build |
| 20 | + |
| 21 | +# Or build individually |
| 22 | +pnpm --filter @explainer/docs build |
| 23 | +pnpm --filter @explainer/blog build |
| 24 | +pnpm --filter @explainer/website build |
| 25 | +``` |
| 26 | + |
| 27 | +:::callout{variant="info" title="Independent apps"} |
| 28 | +Each app builds independently with its own `dist/` output. You can deploy them to different providers or subdomains — they don't need to live on the same server. |
| 29 | +::: |
| 30 | + |
| 31 | +## Environment variables |
| 32 | + |
| 33 | +Before building for production, configure your cross-app URLs. These ensure navbar links point to the correct domains: |
| 34 | + |
| 35 | +```bash title=".env" |
| 36 | +PUBLIC_WEBSITE_URL=https://explainer.dev |
| 37 | +PUBLIC_DOCS_URL=https://docs.explainer.dev |
| 38 | +PUBLIC_BLOG_URL=https://blog.explainer.dev |
| 39 | +``` |
| 40 | + |
| 41 | +For the contributors section, you can optionally provide a GitHub token for higher API rate limits: |
| 42 | + |
| 43 | +```bash title=".env" |
| 44 | +GITHUB_TOKEN=ghp_xxxxxxxxxxxx |
| 45 | +``` |
| 46 | + |
| 47 | +:::callout{variant="success"} |
| 48 | +Without a token, the GitHub API allows 60 requests per hour — more than enough for a single build. The token is mainly useful during active development when you're rebuilding frequently. |
| 49 | +::: |
| 50 | + |
| 51 | +## Deploying to Cloudflare Pages |
| 52 | + |
| 53 | +Cloudflare Pages is our recommended deployment target. It offers a generous free tier, global CDN, and automatic preview deployments. |
| 54 | + |
| 55 | +::::step-group |
| 56 | +:::step{title="Create Cloudflare Pages projects"} |
| 57 | +Create three projects in your Cloudflare dashboard — one for each app: |
| 58 | +- `explainer-docs` |
| 59 | +- `explainer-blog` |
| 60 | +- `explainer-website` |
| 61 | + |
| 62 | +No build configuration needed in Cloudflare — we handle builds in GitHub Actions. |
| 63 | +::: |
| 64 | + |
| 65 | +:::step{title="Configure secrets"} |
| 66 | +In your GitHub repository settings, add these secrets: |
| 67 | +- `CLOUDFLARE_API_TOKEN` — your Cloudflare API token with Pages edit permission |
| 68 | +- `CLOUDFLARE_ACCOUNT_ID` — your Cloudflare account ID |
| 69 | + |
| 70 | +And these variables: |
| 71 | +- `PUBLIC_DOCS_URL`, `PUBLIC_BLOG_URL`, `PUBLIC_WEBSITE_URL` — your production URLs |
| 72 | +- `DEPLOY_TARGET` — set to `cloudflare` |
| 73 | +::: |
| 74 | + |
| 75 | +:::step{title="Push to main"} |
| 76 | +The included GitHub Actions workflow handles everything automatically. On each push to `main`, it: |
| 77 | +1. Detects which apps have changed (path filtering) |
| 78 | +2. Builds only the affected apps |
| 79 | +3. Deploys to Cloudflare Pages |
| 80 | + |
| 81 | +```yaml title=".github/workflows/deploy.yml" |
| 82 | +- run: pnpm --filter @explainer/docs build |
| 83 | + env: |
| 84 | + PUBLIC_DOCS_URL: ${{ vars.PUBLIC_DOCS_URL }} |
| 85 | + PUBLIC_BLOG_URL: ${{ vars.PUBLIC_BLOG_URL }} |
| 86 | + PUBLIC_WEBSITE_URL: ${{ vars.PUBLIC_WEBSITE_URL }} |
| 87 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 88 | +``` |
| 89 | +::: |
| 90 | +:::: |
| 91 | +
|
| 92 | +## Deploying to Vercel |
| 93 | +
|
| 94 | +Vercel's zero-config approach works well with Explainer. Each app can be imported as a separate Vercel project. |
| 95 | +
|
| 96 | +::::step-group |
| 97 | +:::step{title="Import the repository"} |
| 98 | +In the Vercel dashboard, import your repository three times — once per app. For each project, configure: |
| 99 | +
|
| 100 | +- **Root directory**: `apps/docs`, `apps/blog`, or `apps/website` |
| 101 | +- **Build command**: `cd ../.. && pnpm build --filter @explainer/docs` |
| 102 | +- **Output directory**: `dist` |
| 103 | +::: |
| 104 | + |
| 105 | +:::step{title="Set environment variables"} |
| 106 | +Add `PUBLIC_DOCS_URL`, `PUBLIC_BLOG_URL`, `PUBLIC_WEBSITE_URL`, and optionally `GITHUB_TOKEN` in each project's environment settings. |
| 107 | +::: |
| 108 | + |
| 109 | +:::step{title="Configure custom domains"} |
| 110 | +Assign your subdomains to each Vercel project: `docs.explainer.dev`, `blog.explainer.dev`, and `explainer.dev`. |
| 111 | +::: |
| 112 | +:::: |
| 113 | + |
| 114 | +## Smart CI/CD with path filtering |
| 115 | + |
| 116 | +The monorepo workflow includes intelligent path filtering — only apps with actual changes get rebuilt and deployed: |
| 117 | + |
| 118 | +```yaml |
| 119 | +- uses: dorny/paths-filter@v4 |
| 120 | + with: |
| 121 | + filters: | |
| 122 | + docs: |
| 123 | + - 'apps/docs/**' |
| 124 | + - 'packages/**' |
| 125 | + - 'pnpm-lock.yaml' |
| 126 | + blog: |
| 127 | + - 'apps/blog/**' |
| 128 | + - 'packages/**' |
| 129 | + - 'pnpm-lock.yaml' |
| 130 | +``` |
| 131 | + |
| 132 | +:::callout{variant="warning" title="Shared packages trigger all builds"} |
| 133 | +Changes to `packages/**` trigger rebuilds for all apps, since shared UI or MDX changes can affect any app. This is intentional — it ensures consistency across your documentation ecosystem. |
| 134 | +::: |
| 135 | + |
| 136 | +## Custom domains and DNS |
| 137 | + |
| 138 | +A typical setup uses subdomains: |
| 139 | + |
| 140 | +| App | Domain | DNS Record | |
| 141 | +|-----|--------|------------| |
| 142 | +| Website | `explainer.dev` | `CNAME` to provider | |
| 143 | +| Docs | `docs.explainer.dev` | `CNAME` to provider | |
| 144 | +| Blog | `blog.explainer.dev` | `CNAME` to provider | |
| 145 | + |
| 146 | +Both Cloudflare Pages and Vercel handle SSL certificates automatically. |
| 147 | + |
| 148 | +## Docker deployment |
| 149 | + |
| 150 | +For self-hosted environments, Explainer supports Docker deployment with a multi-stage build: |
| 151 | + |
| 152 | +```dockerfile title="Dockerfile" |
| 153 | +FROM node:22-alpine AS build |
| 154 | +RUN corepack enable |
| 155 | +WORKDIR /app |
| 156 | +COPY . . |
| 157 | +RUN pnpm install --frozen-lockfile |
| 158 | +RUN pnpm --filter @explainer/docs build |
| 159 | +
|
| 160 | +FROM nginx:alpine |
| 161 | +COPY --from=build /app/apps/docs/dist /usr/share/nginx/html |
| 162 | +``` |
| 163 | + |
| 164 | +You can serve all three apps behind a reverse proxy (nginx, Caddy, Traefik) with path-based or subdomain routing. |
| 165 | + |
| 166 | +## Performance checklist |
| 167 | + |
| 168 | +Before going live, verify these optimizations are in place: |
| 169 | + |
| 170 | +::::card-group{cols=2} |
| 171 | +:::card{label="Pagefind search" icon="lucide:search"} |
| 172 | +Search indexes are generated at build time. Verify the `dist/pagefind/` directory exists after building docs. |
| 173 | +::: |
| 174 | + |
| 175 | +:::card{label="OG thumbnails" icon="lucide:image"} |
| 176 | +Every page gets an auto-generated Open Graph image. Test with the [Twitter Card Validator](https://cards-dev.twitter.com/validator). |
| 177 | +::: |
| 178 | + |
| 179 | +:::card{label="RSS feed" icon="lucide:rss"} |
| 180 | +The blog generates RSS feeds per locale at `/{locale}/rss.xml`. Test with a feed reader. |
| 181 | +::: |
| 182 | + |
| 183 | +:::card{label="Sitemap" icon="lucide:map"} |
| 184 | +Astro generates a sitemap automatically. Verify it's accessible at `/sitemap-index.xml`. |
| 185 | +::: |
| 186 | +:::: |
| 187 | + |
| 188 | +## What's next |
| 189 | + |
| 190 | +With your documentation deployed, you can focus on what matters — writing great content. The CI/CD pipeline ensures every push to `main` gets your changes live within minutes. |
| 191 | + |
| 192 | +:::callout{variant="note" title="Need help?"} |
| 193 | +Check out the [deployment documentation](/en/explainer/deployment/docker) for detailed provider-specific guides, or open an issue on GitHub if you run into any problems. |
| 194 | +::: |
| 195 | + |
| 196 | +Happy shipping! |
0 commit comments