Chobble Client is a content repository that merges with the Chobble Template at build time to produce a static website. It uses Bun as the package manager and runtime.
This project separates content from template:
chobble-client): Site content, custom styles, build scriptsAt build time, GitHub Actions merges both repos via sparse-checkout, then runs Eleventy.
bun install # Install dependencies (MUST use bun, not npm)
bun run build # Build the site
bun run serve # Development server with hot reload
bun run test # Run tests
bun run lint # Check code with Biome
bun run lint:fix # Auto-fix lint issues
bun run cpd # Copy-paste detection on scripts/
chobble-client/
├── scripts/ # Build utilities and tooling
├── _data/ # Site configuration (site.json, meta.json)
├── pages/ # Content pages (markdown)
├── css/ # Custom stylesheets
├── images/ # Site images
├── .pages.yml # CMS configuration
├── biome.json # Linting config (extends js-toolkit base)
├── bunfig.toml # Bun test configuration
└── .jscpd.json # Copy-paste detection config
This codebase uses a functional programming approach with curried, composable functions. This is ideal for a static site generator, which is fundamentally a series of transforms with no mutable state:
Content Files → Parse → Transform → Filter → Sort → Render → Static HTML
Each step is a pure function. Data flows through pipelines without mutation.
Use the #fp alias for functional utilities:
import { pipe, filter, map, unique } from "#fp";
import { memoize } from "#fp/memoize";
import { sortBy } from "#fp/sorting";
#fp)| Function | Purpose | Example |
|---|---|---|
pipe(...fns) |
Compose functions left-to-right | pipe(filter(x), map(y))(arr) |
| Function | Purpose | Example |
|---|---|---|
filter(pred) |
Curried array filter | filter(x => x > 0)(arr) |
map(fn) |
Curried array map | map(x => x * 2)(arr) |
flatMap(fn) |
Curried array flatMap | flatMap(x => [x, x])(arr) |
reduce(fn, init) |
Curried array reduce | reduce((a, x) => a + x, 0)(arr) |
sort(cmp) |
Non-mutating sort | sort((a, b) => a - b)(arr) |
sortBy(key) |
Sort by property/getter | sortBy('name')(users) |
| Function | Purpose | Example |
|---|---|---|
unique(arr) |
Remove duplicates | unique([1, 1, 2]) → [1, 2] |
uniqueBy(fn) |
Dedupe by key | uniqueBy(x => x.id)(arr) |
compact(arr) |
Remove falsy values | compact([1, null, 2]) → [1, 2] |
filterMap(pred, fn) |
Filter + map in one pass | filterMap(x => x > 0, x => x * 2)(arr) |
| Function | Purpose | Example |
|---|---|---|
memberOf(vals) |
Membership predicate | filter(memberOf(['a', 'b']))(arr) |
notMemberOf(vals) |
Exclusion predicate | filter(notMemberOf(['x']))(arr) |
exclude(vals) |
Filter out values | exclude(['a'])(arr) |
pick(keys) |
Extract object keys | pick(['a', 'b'])(obj) |
| Function | Purpose | Example |
|---|---|---|
memoize(fn, opts?) |
Cache results | memoize(fn, { cacheKey }) |
indexBy(getKey) |
Build cached lookup | indexBy(x => x.id)(arr) |
groupByWithCache(fn) |
Build cached grouping | groupByWithCache(x => x.tags)(arr) |
| Function | Purpose | Example |
|---|---|---|
pluralize(s, p?) |
Format count | pluralize('item')(3) → "3 items" |
accumulate(fn) |
Safe array building in reduce | See below |
import { pipe, filter, map, sortBy, unique } from "#fp";
// Process blog posts: filter drafts, extract tags, sort by date
const processedPosts = pipe(
filter(post => !post.draft),
sortBy('date'),
map(post => ({ ...post, tags: post.tags || [] }))
)(posts);
// Get all unique tags
const allTags = pipe(
flatMap(post => post.tags),
unique
)(processedPosts);
accumulate()Avoid the noAccumulatingSpread lint error:
// BAD - O(n^2) performance
const ids = items.reduce((acc, item) =>
item.id ? [...acc, item.id] : acc, []);
// GOOD - O(n) performance
import { accumulate } from "#fp";
const ids = accumulate((acc, item) => {
if (item.id) acc.push(item.id);
return acc;
})(items);
The project enforces strict code quality via Biome.
| Rule | Requirement |
|---|---|
useArrowFunction |
Use arrow functions |
useTemplate |
Use template literals |
useConst |
Use const (or let when reassignment needed) |
noVar |
Never use var |
noDoubleEquals |
Use ===, not == |
noForEach |
Use for...of or curried map/filter |
noAccumulatingSpread |
Use accumulate() helper |
noUnusedImports |
Remove unused imports |
noUnusedVariables |
Remove unused variables |
noExcessiveCognitiveComplexity |
Max complexity: 7 (30 in tests) |
noConsole |
No console.log except in scripts/ |
bun run lint:fix to auto-formatforEach - Use for...of loops or curried map/filteraccumulate() helper for O(1) operationsvar - Always use const (or let when reassignment needed)== - Always use ===pipe, curried functions, immutabilitybun run lint:fix to auto-fix issues