# dlstack
A hand-built, file-driven personal link index. Drop it on any shared host
via SFTP — no build step, no framework, no database. To update the page,
edit `data/links.json` and re-upload.
Live at .
## Setup
The repo ships with `data/links.example.json` (Ada Lovelace placeholder
data). The app reads `data/links.json` first, and falls back to the
example if there's no real file — so a fresh clone renders immediately.
To customize:
```bash
cp data/links.example.json data/links.json
# edit data/links.json — your real content
```
`data/links.json` is gitignored, so your personal content stays out of
the public repo. Upload `data/links.json` to your host alongside the
other files when you deploy.
## File layout
```
dlstack/
├── index.html shell + font preloads + no-flash boot script
├── favicon.svg the dL mark, ink/paper adaptive
├── data/
│ ├── links.example.json committed example — used as fallback
│ └── links.json your real content (gitignored)
└── assets/
├── css/
│ ├── styles.css editorial template (default)
│ └── swiss.css swiss template (overrides, scoped)
└── js/
└── app.js loader + renderers
```
Total payload: ~40 KB across all assets (uncompressed). Gzipped: ~12 KB.
## Deploying
Upload the whole directory via SFTP/FTP to the web root of any shared host.
That's it — no PHP, no Node, no DB.
To update content: edit `data/links.json` locally, upload, done.
If you're behind Cloudflare during active development, enable
**Caching → Configuration → Development Mode** in the CF dashboard to
bypass the edge cache for 3 hours. Otherwise purge edge cache after each
asset change.
## `data/links.json` schema
```jsonc
{
"profile": { … }, // who you are
"theme": { … }, // colors + which template
"sections": [ … ], // groups of items
"social": [ … ], // pill buttons under the hero
"footer": { … } // bottom strip
}
```
### `profile`
| field | type | description |
|-----------|--------|---------------------------------------------------|
| `name` | string | Hero name. Last word is set in italic accent. |
| `handle` | string | Optional. Shown in the top marker (e.g. `@you`). |
| `tagline` | string | Italic subtitle. Words separated by `·` get the accent color. |
| `bio` | string | One-paragraph intro under the tagline. |
| `location`| string | Currently unused after the live clock was removed; safe to leave or omit. |
### `theme`
| field | type | values | description |
|------------|--------|---------------------------------|-------------|
| `accent` | string | hex color, e.g. `"#E8482C"` | Single sharp accent color used throughout. |
| `template` | string | `"editorial"` (default), `"swiss"`, or `"cosmos"` | Visual treatment. See [Templates](#templates). |
| `mode` | string | unused; kept for future use | Theme toggle (auto/light/dark) is controlled by the corner pill, not this field. |
### `sections`
An ordered array of groups. Each section renders with a numbered masthead
(`№ 01 / Sites`) and a grid of items.
```json
{
"id": "sites", // required, unique slug
"label": "Sites", // required, the section title
"kicker": "Where I live online", // optional italic tagline
"layout": "clients", // optional — see Layouts below
"headless": true, // optional — render body only, no head
"items": [ … ] // required, array of items
}
```
Set `headless: true` to render a section's body without the
`№ NN / Label / kicker` header. Tighter top margin too, so the
contents visually nest under the previous section. Useful for sliding
a testimonials carousel underneath a Clients wall, for example —
put the testimonials section directly after the clients section in
the `sections` array with `headless: true` and they'll read as one.
#### Layouts
| `layout` | When to use | Grid behavior |
|------------------|----------------------------------------|----------------------------------------|
| *omitted* | Default bento mix | 12-column asymmetric grid (link cards span 4/12, projects/youtube span 6/12, featured spans 8/12, portfolio spans 12/12) |
| `"clients"` | Logo wall of clients/partners | Auto-flowing square tiles, `minmax(108–124px, 1fr)` |
| `"testimonials"` | Quotes from clients / collaborators | Crossfade carousel — one quote at a time, auto-advancing every 7s, with prev/next nav, dot indicators, swipe + ←/→ keys, pause on hover/focus. Reduced-motion users see all quotes stacked. |
`layout: "clients"` is also auto-detected if the first item in the section
has `type: "client"`. Same goes for `layout: "testimonials"` when the first
item is `type: "testimonial"`.
### Items
Every item lives in a section's `items` array and has a `type` field. The
six available types are `link`, `card`, `youtube`, `client`, `portfolio`,
and `testimonial` — documented below.
#### Optional `date` (any item type)
Every item type supports an optional `date` string. Use it to surface
"published", "shipped", "released", or "worked on" timestamps without
changing the item's `type`.
Accepted formats (all ISO-style, all interpreted as plain strings — no
time zones, no parsing surprises):
| Input | Rendered as | Use for |
|----------------|------------------------|------------------------------------|
| `"2026"` | `2026` | Year-only — "this is from 2026" |
| `"2026-05"` | `May 2026` | Year + month — soft / recurring |
| `"2026-05-14"` | `May 14, 2026` | Full date — talks, launches, posts |
Where it appears per type:
- **`link`** — small monospaced caption under the host (separated by a `·`).
- **`card` (project)** — at the end of the tag row.
- **`portfolio`** — under the caption description.
- **`youtube`** — inline after the title overlay.
- **`client`** — rendered as a small caption under the client title.
The raw value is also written to the `