# 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) 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) | | | **Lucide** | Generic UI icons — mail, rss, link, phone, calendar, etc. (1500+, MIT-licensed) | | | **Phosphor** | 9000+ icons with multiple weights (regular / bold / thin / duotone) | | | **Heroicons** | Tailwind's set, clean and minimal | | 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: '', linkedin: '', mail: '', rss: '', link: '', // Newly added — Bluesky from Simple Icons: bluesky: '' }; ``` 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 `` 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.