Powering a Static Site with csv-api
Use csv-api as a lightweight CMS backend for Jekyll, Hugo, or any static site generator.
The problem with static site data
Static site generators like Jekyll, Hugo, and Eleventy are great for performance and simplicity. But what happens when you need dynamic data — a team directory, an event list, a product catalog, or a FAQ page that non-developers need to update?
Traditionally, you'd hardcode the data in YAML/JSON/Markdown files, use a headless CMS, or set up a full backend. csv-api offers a simpler middle ground: maintain your data in a spreadsheet, upload it once, and fetch it at build time or client-side.
Approach 1: Fetch at build time
Most static site generators support data fetching during the build process. This means your API data gets baked into static HTML — no client-side JavaScript needed, no API key exposed.
Jekyll (_plugins/csv_api.rb):
require "net/http"
require "json"
module Jekyll
class CsvApiGenerator < Generator
def generate(site)
uri = URI("https://csv-api.com/api/v1/datasets/YOUR_ID/records?per_page=100")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV['CSV_API_KEY']}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http|
http.request(req)
}
data = JSON.parse(res.body)
site.data["team_members"] = data["data"]
end
end
end
Then use it in a template (team.html):
{% for member in site.data.team_members %}
<div class="team-card">
<h3>{{ member.name }}</h3>
<p>{{ member.role }}</p>
<p>{{ member.bio }}</p>
</div>
{% endfor %}
Approach 2: Hugo data templates
Hugo can fetch remote data with the getJSON function. However, for authenticated APIs, it's easier to use a build script that fetches the data and saves it as a JSON file:
fetch-data.sh (run before hugo build):
#!/bin/bash curl -s -H "Authorization: Bearer $CSV_API_KEY" \ "https://csv-api.com/api/v1/datasets/YOUR_ID/records?per_page=100" \ | jq '.data' > data/events.json
Then reference in your Hugo template:
{{ range $.Site.Data.events }}
<article>
<h2>{{ .title }}</h2>
<time>{{ .date }}</time>
<p>{{ .description }}</p>
</article>
{{ end }}
Approach 3: Client-side fetching
For data that changes frequently, you can fetch it client-side. This works with any static site since it's just JavaScript. csv-api includes CORS headers, so cross-origin requests work out of the box.
<div id="faq-list">Loading...</div>
<script>
fetch("https://csv-api.com/api/v1/datasets/YOUR_ID/records?sort=order&per_page=100", {
headers: { "Authorization": "Bearer YOUR_API_KEY" }
})
.then(res => res.json())
.then(({ data }) => {
document.getElementById("faq-list").innerHTML = data.map(faq =>
`<details>
<summary>${faq.question}</summary>
<p>${faq.answer}</p>
</details>`
).join("");
});
</script>
Remember: Client-side fetching exposes your API key to anyone viewing the page source. This is acceptable for public, read-only data. For sensitive data, use build-time fetching or a server-side proxy.
Real-world use case: csv-api.com itself
We practice what we preach. The csv-api landing page loads its feature cards and workflow sections from csv-api datasets. The data is stored in CSV-backed dynamic tables and rendered at page load — with a hardcoded fallback in case the datasets aren't available.
This means updating the landing page content is as simple as editing rows in the dataset — no code changes, no deploy needed.
Tips for static site integration
-
Use
fieldsto minimize payload — only request the columns your templates actually use. -
Add a
sortcolumn to your CSV if you need to control display order (e.g.,?sort=order). -
Use a boolean column for visibility — add a
publishedcolumn and filter with?filter[published]=trueto draft and publish content. - Set up a CI/CD webhook to rebuild your site when data changes, keeping the static output fresh.