Day 7 - Scaffolding a Documentation and Example Site for the `floe` package using GitHub Pages

A sneak peek at the upcoming floe package

Written by Chris on December 7th, 2025

This post is part of the 'The 12 Days of Full Stack Dev' series, an adventure through full stack fintech app development. Join me through the first 12 days of December as we develop a variety of new features, fixes, and components within the Full Stack Craft fintech family. For the full list of days and topics in this series, see the original post: The 12 Days of Full Stack Dev.

By the end of this 12 days series, I plan to release a new open source package called floe. This package aims to simplify option flow based calculations and analysis as a zero dependency npm package written in TypeScript.

With many brokers now providing option flow data via their APIs, I wanted to create a package that makes it easy to work with this data in a consistent way across different platforms. I have pretty good faith in this package and think it could really take off over the coming years as more and more brokers provide customers with the ability to access option flow data.

Anyway, any good npm package needs documentation and examples, so today I focused on setting up a documentation and example site using GitHub Pages.

There's way too much code for this challenge to show it all here, so I'll just show the highlights.

Markdown Based Documentation

First off, we have a markdown based documentation pages structure in the docs/ folder. Each markdown file corresponds to a page on the documentation site. Mostly, this is controlled by a simple lib/markdown.ts file:

import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { remark } from "remark";
import html from "remark-html";

const docsDirectory = path.join(process.cwd(), "content/docs");
const examplesDirectory = path.join(process.cwd(), "content/examples");

export interface DocMeta {
  slug: string;
  title: string;
  description?: string;
  order?: number;
}

export interface DocContent extends DocMeta {
  contentHtml: string;
}

function getMarkdownFiles(directory: string): DocMeta[] {
  if (!fs.existsSync(directory)) {
    return [];
  }
  
  const fileNames = fs.readdirSync(directory);
  const allDocs = fileNames
    .filter((fileName) => fileName.endsWith(".md"))
    .map((fileName) => {
      const slug = fileName.replace(/\.md$/, "");
      const fullPath = path.join(directory, fileName);
      const fileContents = fs.readFileSync(fullPath, "utf8");
      const { data } = matter(fileContents);

      return {
        slug,
        title: data.title || slug,
        description: data.description,
        order: data.order || 999,
      };
    });

  return allDocs.sort((a, b) => (a.order || 999) - (b.order || 999));
}

async function getMarkdownContent(directory: string, slug: string): Promise<DocContent | null> {
  const fullPath = path.join(directory, `${slug}.md`);
  
  if (!fs.existsSync(fullPath)) {
    return null;
  }

  const fileContents = fs.readFileSync(fullPath, "utf8");
  const { data, content } = matter(fileContents);

  const processedContent = await remark().use(html).process(content);
  const contentHtml = processedContent.toString();

  return {
    slug,
    title: data.title || slug,
    description: data.description,
    order: data.order,
    contentHtml,
  };
}

export function getAllDocs(): DocMeta[] {
  return getMarkdownFiles(docsDirectory);
}

export function getAllExamples(): DocMeta[] {
  return getMarkdownFiles(examplesDirectory);
}

export async function getDocBySlug(slug: string): Promise<DocContent | null> {
  return getMarkdownContent(docsDirectory, slug);
}

export async function getExampleBySlug(slug: string): Promise<DocContent | null> {
  return getMarkdownContent(examplesDirectory, slug);
}

export function getAllDocSlugs(): string[] {
  if (!fs.existsSync(docsDirectory)) {
    return [];
  }
  return fs.readdirSync(docsDirectory)
    .filter((fileName) => fileName.endsWith(".md"))
    .map((fileName) => fileName.replace(/\.md$/, ""));
}

export function getAllExampleSlugs(): string[] {
  if (!fs.existsSync(examplesDirectory)) {
    return [];
  }
  return fs.readdirSync(examplesDirectory)
    .filter((fileName) => fileName.endsWith(".md"))
    .map((fileName) => fileName.replace(/\.md$/, ""));
}

Playground with Live Code Editing

Then, using codesandbox's @codesandbox/sandpack-react package, I created interactive code examples that users can run directly in their browsers. This is a great way to let users experiment with the floe package without needing to set up a local environment, sandpack-react makes importing the @fullstackcraftllc/flow package and running code snippets super easy:

import { Sandpack } from "@codesandbox/sandpack-react";

<div className="rounded-lg overflow-hidden border border-gray-200 shadow-sm">
    <Sandpack
    template="vanilla-ts"
    theme="light"
    files={{
        "/index.ts": example.code,
    }}
    customSetup={{
        dependencies: {
        "@fullstackcraftllc/floe": "latest",
        },
    }}
    options={{
        showConsole: true,
        showConsoleButton: true,
        editorHeight: 500,
        showLineNumbers: true,
        showInlineErrors: true,
        wrapContent: true,
        autorun: true,
        autoReload: true,
    }}
    />
</div>

That's About It!

From there, we have some other separate "recipe" pages with longer code snippets, but overall, this is the gist of how I set up the documentation and example site for the upcoming floe package. I'm pretty excited about this package and think it could be really useful for developers working with option flow data.

Until next time!

-Chris

More posts:

footer-frame