CSV data flowing through a Remark plugin into a rendered Markdown table.

I Turned My Blog's CSV Table Helper into a Remark Plugin

How I extracted a custom Astro Markdown plugin into remark-csv-table, a reusable Remark plugin that turns local CSV directives into GFM tables at build time.

 Published 

 5 min read

Writing a table directly in Markdown works for a few rows, but it becomes awkward when the data changes often or comes from a spreadsheet. I wanted the writing surface to stay readable while keeping the structured data in a format that is easy to edit, validate, and reuse.

That led to remark-csv-table, a Remark plugin that converts local CSV files referenced by a Markdown directive into GitHub Flavored Markdown (GFM) tables during the build. It started as a custom plugin in this Astro site and is now a published package that other Remark pipelines can use.

The Markdown Table Problem

Markdown tables keep content and presentation together, but they are not a great editing format for larger datasets. Adding a column means adjusting every row. Copying data from a spreadsheet can introduce alignment mistakes. Reviewing changes in a long table is also harder than reviewing a focused data file.

The workflow I wanted was simple: keep a post’s data in a nearby CSV file and let the Markdown refer to it.

::csv{src="./model-results.csv"}

At build time, the directive becomes a regular GFM table. The source article remains focused on the explanation, while the data remains in a CSV file that can be opened in a spreadsheet or edited as plain text.

From A Site Helper To A Package

My articles have become increasingly data-heavy, with longer tables and more content in the cells. Eventually, I decided writing the tables directly into the Markdown source for the articles was consuming more time than it was worth. I decided to store the data in CSV and inject that into the Markdown as part of the rendering process.

The original implementation lived in this site’s Markdown pipeline as remark-csv-tables.ts. It did more than parse a file: it resolved each src value relative to the current Markdown file, rejected paths outside the post directory, required a non-empty header and at least one data row, and replaced the directive with an mdast table node.

Those checks are important. A directive should not be able to read an arbitrary file from a build machine, and a broken CSV should fail clearly rather than silently produce a malformed article.

Once the plugin was useful in more than one place, keeping it tied to this site’s directory structure stopped making sense. The reusable version exposes a contentDirectory option instead of assuming this repository’s content path.

Once I got the plugin working, I realized it would be potentially helpful to other developers or even useful to myself on future projects. Also, I have not had a chance to publish my own public package to npm yet, so this seemed like a good candidate for a learning experience to do that. The main thing I learned from creating this package is that publishing is the easy part, and the hard part is getting the project ready for the first release.

Install And Configure remark-csv-table

remark-csv-table is ESM-only and requires Node 24 or newer. It works in a Remark pipeline that also includes remark-directive for the ::csv syntax and remark-gfm for GFM table output.

bun add remark-csv-table remark-directive remark-gfm unified remark-parse remark-stringify

Here is a minimal Unified pipeline:

import { unified } from "unified";
import remarkDirective from "remark-directive";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkStringify from "remark-stringify";
import remarkCsvTable from "remark-csv-table";

const result = await unified()
  .use(remarkParse)
  .use(remarkDirective)
  .use(remarkGfm)
  .use(remarkCsvTable, { contentDirectory: "content" })
  .use(remarkStringify)
  .process({
    path: "content/posts/example.md",
    value: '::csv{src="./table.csv"}',
  });

The CSV path must remain inside both the Markdown file’s directory and contentDirectory. By default, contentDirectory is the current working directory. CSV files need a header and at least one data row, headers cannot be empty, and cells support single-line inline Markdown.

Using It In Astro

Astro can load the package in the same Remark plugin list as other Markdown transforms. This site imports its configured unified processor from @astrojs/markdown-remark and includes the plugin after remark-directive.

import { unified } from "@astrojs/markdown-remark";
import remarkDirective from "remark-directive";
import remarkCsvTable from "remark-csv-table";

export const markdownConfig = {
  processor: unified({
    remarkPlugins: [remarkDirective, remarkCsvTable],
  }),
};

With that configuration, an article and its CSV can stay together:

src/data/blog/example-post/
  index.md
  model-results.csv
The following results compare the available configurations.

::csv{src="./model-results.csv"}

This is deliberately a build-time transformation. Readers receive normal table HTML, not a client-side CSV parser or a network request for the source data.

What Changed On This Site

The extraction let this site remove its custom remark-csv-tables.ts file and replace it with the published package in the Markdown configuration. Existing article directives keep the same ::csv{src="./file.csv"} syntax, so the content authoring workflow stays the same.

The difference is maintenance scope. The CSV-to-table behavior now has its own package, documentation, tests, and release cycle instead of being coupled to one Astro project. That makes it useful to other sites while keeping this site’s Markdown configuration smaller.

Try The Plugin

If you keep structured data beside Markdown content, install remark-csv-table on npm and see the source at github.com/h93xV2/remark-csv-table. The package is intended for the specific case where local, build-time CSV tables are easier to maintain than hand-written Markdown tables.

Install remark-csv-table from npm

Frequently Asked Questions

What does remark-csv-table do?

remark-csv-table is a Remark plugin that replaces a ::csv directive pointing to a local CSV file with a GitHub Flavored Markdown table during the build.

What plugins does remark-csv-table require?

Use it in a Remark pipeline with remark-directive, which parses the ::csv directive, and remark-gfm, which supports GFM table output.

Can a CSV directive read any file on my machine?

No. The CSV file must be inside both the current Markdown file's directory and the configured contentDirectory. This prevents a directive from escaping the intended content area.

Can CSV cells contain Markdown?

Yes. Cells support single-line inline Markdown. Multiline cells are rejected.


Similar Posts

Long exposure shot of vehicle lights passing by on a dark street

Designing a Dynamic Table of Contents

A short dive into designing a dynamic table of contents for blog posts.


profile

I am a software engineer based in Southern California. I share hands-on experiments, benchmarks, and lessons from building software, running local AI, and maintaining home infrastructure.

More about the site and its author