Files
dlStack/README.md
Joel Brock 543d3bd8e1 Refine Swiss + Editorial themes; add optional date field
Swiss: drop all-caps on titles, soften heavy ink borders to muted rules
or remove them where background contrast carries the form, balance hero
name weights (smaller lighter first name vs heavier italic last name),
fix unreadable white description text on portfolio hover.

Editorial: enlarge the section "No" mark and set it in italic Fraunces so
it reads as typography instead of a tick, swap the first name into DM
Serif Display for a different J, sharpen the section number caption.

Client tile: contain custom logos at 86% so wide marks like
"The Cooperative Way" stop getting cropped to "operative".

New: optional `date` field on every item type (link/card/portfolio/
youtube/client). Accepts YYYY, YYYY-MM, or YYYY-MM-DD. Rendered
human-friendly with the raw ISO preserved in <time datetime>.
Documented in README and demonstrated in links.example.json.
2026-05-15 16:33:53 -07:00

409 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <https://www.joelbrock.org>.
## 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) or `"swiss"` | 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
"items": [ ] // required, array of items
}
```
#### 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(108124px, 1fr)` |
`layout: "clients"` is also auto-detected if the first item in the section
has `type: "client"`.
### Items
Every item lives in a section's `items` array and has a `type` field. The
five available types are 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 `<time datetime="…">` attribute so
the original ISO string is preserved for machines and screen readers,
even though humans see the friendly form.
```json
{ "type": "card", "title": "CoVote launched", "date": "2025-11-04" }
```
#### `type: "link"` — standard link card
```json
{
"type": "link",
"title": "Columinate",
"url": "https://columinate.com",
"description": "Cooperative Technology Solutions",
"image": "https://example.com/logo.png", // optional — overrides auto-favicon
"featured": false // optional
}
```
| field | required? | notes |
|---------------|-----------|-------|
| `title` | yes | Card headline. |
| `url` | yes | Where the card links to. Opens in new tab. |
| `description` | no | Smaller line under the title. |
| `image` | no | Custom logo URL. Fills the favicon slot edge-to-edge (`object-fit: cover`). If omitted, a favicon is auto-fetched via Google's S2 service (`google.com/s2/favicons?sz=128`). |
| `icon` | no | Alias for `image`. |
| `featured` | no | Reserved; not currently styled differently for link cards. |
#### `type: "card"` — project card
Bigger, prose-friendly card with tags.
```json
{
"type": "card",
"title": "CoVote",
"url": "https://covote.org",
"description": "Anonymous election platform for co-ops.",
"tags": ["democracy", "co-ops"],
"featured": true // optional — makes it the hero card
}
```
| field | required? | notes |
|---------------|-----------|-------|
| `title` | yes | Serif headline (Fraunces in editorial; Archivo 900 caps in Swiss). |
| `url` | no | If present, the card becomes a link. |
| `description` | no | Paragraph under the title. |
| `tags` | no | Array of strings — small monospaced chips. |
| `featured` | no | When `true`, card spans 8/12 columns AND fills with the accent color (paper text on vermilion). Use sparingly — one per section. |
#### `type: "youtube"` — embedded video
Renders a lazy facade (thumbnail + play button). The real YouTube iframe
only loads when clicked, so the page stays fast even with many videos.
```json
{
"type": "youtube",
"id": "42EXoE7pNAc",
"title": "The 7 Cooperative Principles Song"
}
```
| field | required? | notes |
|---------|-----------|-------|
| `id` | yes | YouTube video ID (the `v=` parameter from the URL). |
| `title` | yes | Shown over the thumbnail and as the iframe's accessible title. |
#### `type: "client"` — logo tile
Used in client/partner walls. Square logo tile with a small caption
below. Best in a section with `layout: "clients"`.
```json
{
"type": "client",
"title": "Columinate",
"url": "https://columinate.coop",
"image": "https://columinate.coop/i/logo-square.png" // optional
}
```
| field | required? | notes |
|---------|-----------|-------|
| `title` | yes | Caption below the tile. Long names wrap inside the column width. |
| `url` | no | If present, the tile becomes a link. |
| `image` | no | Custom logo. If omitted, an auto-favicon is used (rendered at 60% of the tile, contained). |
| `icon` | no | Alias for `image`. |
#### `type: "portfolio"` — wide design/showcase
Always spans the full row. Use for featured design pieces, case-study
images, or any visual you want to feature large.
```json
{
"type": "portfolio",
"title": "Brand identity — Acme Co-op",
"url": "https://example.com/case-study",
"image": "https://example.com/portfolio/acme-hero.jpg",
"description": "Logo system, packaging, signage",
"ratio": "5:2" // optional, default "5:2"
}
```
| field | required? | notes |
|---------------|-----------|-------|
| `image` | yes | The image to display. Fills the card with `object-fit: cover`. |
| `title` | no | Caption headline below the image. |
| `description` | no | Caption subline. |
| `url` | no | If present, the card becomes a link. |
| `ratio` | no | Override aspect ratio. Format `"W:H"` (e.g. `"16:9"`, `"3:1"`, `"4:3"`). Default `"5:2"` (wide, not tall). |
### `social`
Pill buttons that appear under the hero bio.
```json
[
{ "label": "GitHub", "url": "https://github.com/you", "icon": "github" },
{ "label": "LinkedIn", "url": "https://linkedin.com/in/you", "icon": "linkedin" },
{ "label": "Email", "url": "mailto:you@example.com", "icon": "mail" },
{ "label": "RSS", "url": "/feed.xml", "icon": "rss" }
]
```
| field | required? | notes |
|---------|-----------|-------|
| `label` | yes | Text shown next to the icon. |
| `url` | yes | Link target. |
| `icon` | no | One of the built-in icon names (see below). Unknown values fall back to a generic chain-link icon. |
**Built-in icons:** `github`, `linkedin`, `mail`, `rss`, `link`.
The icons are not from a library — they're hand-coded inline SVGs in the
`SOCIAL_ICONS` object inside `assets/js/app.js`. Inline because:
- No extra HTTP requests
- Tint cleanly with `currentColor` so they inherit the link's text color
(works in both editorial + Swiss templates, light + dark modes)
- Total weight for all 5 is ~2 KB
#### Where to find more icons
| Source | Best for | URL |
|---|---|---|
| **Simple Icons** | Brand logos — GitHub, Mastodon, Bluesky, YouTube, Instagram, X/Twitter, Substack, Discord, Patreon, Buy Me a Coffee, etc. (3000+ brands) | <https://simpleicons.org> |
| **Lucide** | Generic UI icons — mail, rss, link, phone, calendar, etc. (1500+, MIT-licensed) | <https://lucide.dev/icons> |
| **Phosphor** | 9000+ icons with multiple weights (regular / bold / thin / duotone) | <https://phosphoricons.com> |
| **Heroicons** | Tailwind's set, clean and minimal | <https://heroicons.com> |
For social/brand logos → **Simple Icons**. For generic UI marks (mail,
rss, link, etc.) → **Lucide** or **Phosphor**.
#### How to add a new icon
1. Find the icon on one of the sites above.
2. Click it and choose **"Copy SVG"** (or download the SVG).
3. Open `assets/js/app.js` and find the `SOCIAL_ICONS` object.
4. Paste the SVG as a new entry, keyed by a short name. Strip any
hardcoded `fill="..."` or `stroke="..."` colors and replace them with
`currentColor` so it inherits the link color.
```js
const SOCIAL_ICONS = {
github: '<svg viewBox="0 0 24 24" fill="currentColor">…</svg>',
linkedin: '<svg viewBox="0 0 24 24" fill="currentColor">…</svg>',
mail: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">…</svg>',
rss: '<svg viewBox="0 0 24 24" fill="currentColor">…</svg>',
link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">…</svg>',
// Newly added — Bluesky from Simple Icons:
bluesky: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="…"/></svg>'
};
```
5. Reference the new icon by its key in `data/links.json`:
```json
{ "label": "Bluesky", "url": "https://bsky.app/profile/you", "icon": "bluesky" }
```
6. Upload `app.js` and `links.json` (and purge Cloudflare cache or use
Development Mode).
The fallback for any unknown `icon` value is a generic chain-link icon,
so it'll still render even if you forget step 4.
### `footer`
```json
{
"copy": "Hand-built. No trackers.",
"year": "auto"
}
```
| field | required? | notes |
|--------|-----------|-------|
| `copy` | no | Left side of the footer. |
| `year` | no | Currently unused — the right side auto-displays the current year and your name. |
## Templates
`theme.template` selects the visual treatment. Both stylesheets are
always loaded, but the Swiss rules are scoped to
`:root[data-template="swiss"]` so only one is "active" at a time.
### `"editorial"` (default)
Magazine/poster aesthetic.
- **Type:** Fraunces (variable serif, italic for accents) + Geist (sans body) + Geist Mono (UI/eyebrows)
- **Mood:** warm paper background, vermilion accent, subtle grain texture, asymmetric bento grid, magazine-style section numbers, italic last-name in the hero, floating typographic asterism
- **Cards:** rounded corners (14px), thin rules, lift + corner-arrow on hover
### `"swiss"`
Müller-Brockmann tribute. Akzidenz/Univers spirit with [Archivo](https://fonts.google.com/specimen/Archivo) as the free variable substitute.
- **Type:** Archivo only, weights 400/500/600/700/900, all-caps headlines
- **Mood:** near-pure white paper, near-pure black ink, true Swiss red (`#DC2127` by default, overridable via `theme.accent`)
- **Geometry:** zero border-radius everywhere, 24px black rules, solid red disc replaces the typographic asterism
- **Cards:** invert on hover (white → solid black with paper text). Featured cards are solid red, invert to black on hover.
- **Animations:** no spin, no italic — only sharp pop-in for the geometric mark
To switch:
```json
"theme": { "accent": "#E8482C", "template": "swiss" }
```
Choice is also cached in `localStorage` (`dlstack-template`) so reloads
don't flash the wrong template before JSON parses.
## Theme (light / dark / auto)
A small pill in the top-right cycles **Auto → Light → Dark → Auto** on
click. "Auto" follows `prefers-color-scheme`. Choice persists in
`localStorage` (`dlstack-theme`). An inline boot script in `<head>`
applies the saved choice before CSS paint, so there's no flash on reload.
The accent color (`theme.accent` in JSON) is applied as a CSS custom
property at runtime and respects both light and dark modes.
## Reserved sub-paths
- `data/links.json` — content
- `assets/css/styles.css` — editorial template + base
- `assets/css/swiss.css` — swiss template overrides
- `assets/js/app.js` — renderer
- `favicon.svg` — the dL mark
Don't rename these without updating the references in `index.html`.
## Local preview
From the project root:
```bash
python3 -m http.server 8765
# then open http://127.0.0.1:8765
```
Any static file server works; CORS isn't an issue since everything is
same-origin.