Initial commit: dlstack file-driven link index
A hand-rolled linktree alternative — pure static HTML/CSS/JS, no build step. Drop on any shared host via SFTP and edit data/links.json to update. Features - File-driven content via data/links.json (links, projects, YouTube, client tiles, portfolio pieces) - Two interchangeable templates: editorial (Fraunces + paper + vermilion) and swiss (Archivo grotesque, all-caps poster) - Auto/light/dark theme toggle with no-flash boot script - Auto-fetched favicons via Google S2 (with image-URL override) - Lazy YouTube facades (no third-party JS until clicked) - Adaptive client-logo grid - Scroll-triggered reveal animations - ~40 KB total payload, ~12 KB gzipped The repo ships links.example.json as a demo; data/links.json is gitignored so personal content stays out of the public repo.
This commit is contained in:
377
README.md
Normal file
377
README.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 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(108–124px, 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.
|
||||
|
||||
#### `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, 2–4px 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.
|
||||
Reference in New Issue
Block a user