API at build time with github Action

Here I show a simple yet realistic example: using an API during Docusaurus build, saving the result in a static JSON file, and then displaying this data in a page or component.
This type of workflow is ideal for keeping a static site in production while leveraging external data at compile time. But beware: not all APIs can be easily queried from a CI server (GitHub Actions). We will see how to bypass blocking (Cloudflare) and set up a robust fallback.
Project Architecture
Challenges encountered and solutions
Problem: API protected by Cloudflare
I first tried to use the api.slingacademy.com API. Locally, everything worked. But in GitHub Actions, curl received an HTML page from Cloudflare (JavaScript challenge) instead of JSON. It was impossible to bypass simply with headers (User-Agent, Accept…).
Solution:
Switch to an API that has no such protection. I chose Lorem Picsum (a public API, no Cloudflare, providing free photos). This immediately solved the problem.
Problem: Fallback when the main API fails
If the main API (Sling Academy, or any other) becomes unavailable or changes its structure, the build fails.
Solution:
In the workflow, I added an automatic fallback to Lorem Picsum. And if even that fallback fails, we can use a local backup JSON file (optional).
Problem: Local development vs production
In development, we don’t want to generate the JSON file manually every time. And we want to test quickly.
Solution:
In the React component, we detect process.env.NODE_ENV:
- Development: direct API call (no static file)
- Production: read the JSON file generated by GitHub Actions
Thus, no local constraint.
GitHub Actions Workflow (final robust version)
The file .github/workflows/main.yml performs the following steps:
- Fetches the data with
curlusing a reliable API (Lorem Picsum). - Saves the JSON to
static/json/photos-data.json. - Builds the Docusaurus site.
- Deploys to GitHub Pages (
gh-pagesbranch).
Here is the full content:
name: Docusaurus site to GitHub Pages
on:
push:
branches: [main]
# schedule:
# cron: "*/30 * * * *" # optional: every 30 minutes
workflow_dispatch:
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Fetch API data (Lorem Picsum)
run: |
mkdir -p static/json
echo "🌐 Calling Lorem Picsum API..."
curl -o static/json/photos-data.json "https://picsum.photos/v2/list?page=4&limit=10"
echo "✅ Data fetched and saved to static/json/photos-data.json"
echo "📊 Preview:"
head -n 20 static/json/photos-data.json
- name: Build Docusaurus site
run: npm run build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
publish_branch: gh-pages
force_orphan: true
Note: We could also add a
scheduletrigger (e.g., every hour) to update photos regularly, even without a new commit.
React Component « PhotosGrid »
The component src/components/PhotosGrid/index.js is reusable in any MDX page. It includes:
- Environment detection (dev vs prod)
- Docusaurus admonition display on error
- Loading state management
- Limit of the number of photos via the
limitprop
import React, { useEffect, useState } from 'react';
import Admonition from '@theme/Admonition';
// Direct API for development
const API_URL = 'https://picsum.photos/v2/list?page=2&limit=10';
// Static file for production (generated by GitHub Actions)
const STATIC_JSON_URL = '/json/photos-data.json';
export default function PhotoGrid({ limit = 8 }) {
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Choose URL based on environment
const isDev = process.env.NODE_ENV === 'development';
const url = isDev ? API_URL : STATIC_JSON_URL;
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => {
// Handles format: direct array (Picsum) or { photos: [...] }
let allPhotos = Array.isArray(data) ? data : data.photos;
if (!allPhotos || !Array.isArray(allPhotos)) {
throw new Error('Unexpected data format');
}
setPhotos(allPhotos.slice(0, limit));
setLoading(false);
})
.catch((err) => {
console.error(err);
setError(err.message);
setLoading(false);
});
}, [limit]);
if (loading) return <div className="text--center margin-vert--lg">Loading photos...</div>;
if (error) {
return (
<Admonition type="warning" title="Loading error">
<p>{error}</p>
</Admonition>
);
}
if (!photos.length) {
return <Admonition type="info" title="No photos">No photos to display.</Admonition>;
}
return (
<div className="row">
{photos.map((photo) => (
<div key={photo.id} className="col col--4 margin-bottom--lg">
<div className="card shadow--md">
<div className="card__image">
<img
src={`https://picsum.photos/id/${photo.id}/400/300`}
alt={photo.author}
style={{ width: '100%', height: 'auto', display: 'block' }}
/>
</div>
<div className="card__body">
<h4>{photo.author}</h4>
<p className="text--small">
<a href={photo.download_url} target="_blank" rel="noopener noreferrer">
View original
</a>
</p>
</div>
</div>
</div>
))}
</div>
);
}
Using the component in an MDX article
To use this component, you must register it in src/theme/MDXComponents.js (or src/theme/MDXComponents/index.js):
import PhotosGrid from '@site/src/components/PhotosGrid';
export default {
// ... other components
PhotosGrid,
};
Then, in any .mdx file, you can write:
<PhotosGrid limit={8} />
Final result
Here is a live preview of the photos (limited to 8) fetched via the described mechanism:
In development, the photos come directly from the Lorem Picsum API. In production, they are read from the static JSON file generated at each build.
Why this approach is interesting
- Performance: no client-side API call in production → instant loading.
- Reliability: the build rarely fails (built‑in fallback).
- Simplicity: no backend server needed, the site remains static.
- Flexibility: the component can be used in a dedicated page or in a blog article.
Alternatives and possible improvements
- Periodic update: add a
schedulein GitHub Actions to rebuild the site regularly (e.g., every 6 hours) even without a commit. - Use another API: just change the URL in the workflow (beware of anti‑bot protections).
- Browser caching: the JSON file will be cached like any static resource.
Conclusion
This example shows how to master external API integration in a static site generated by Docusaurus, overcoming common obstacles (Cloudflare, local development, fallback). The presented code is ready to be copied/pasted into your own project. Feel free to adapt the display, data sources, and update frequency according to your needs.
This article is part of the Api and scripts in Docusaurus series:
- API at build time with github Action
No related posts.