Skip to main content

API at build time with github Action

· 4 min read ·
Docux
Curious explorer, a bit of a mad experimenter, and a bit of a contributor.

Developer Development License: MIT AI 50%

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

📁.github
📁workflows
main.yml
📁static
📁json
photos-data.json
📁src
📁components
📁PhotosGrid
index.js
📁pages
photos.jsx

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:

  1. Fetches the data with curl using a reliable API (Lorem Picsum).
  2. Saves the JSON to static/json/photos-data.json.
  3. Builds the Docusaurus site.
  4. Deploys to GitHub Pages (gh-pages branch).

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 schedule trigger (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 limit prop
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:

Loading photos...

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 schedule in 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.

Back to top