Compare commits
154 commits
eleventy-t
...
main
Author | SHA1 | Date | |
---|---|---|---|
17e0709fd9 | |||
e4174cb639 | |||
e5f09d803e | |||
d91660826b | |||
a88aa22955 | |||
d0cc01d833 | |||
6063931d39 | |||
71442b3468 | |||
7976800fb1 | |||
676aa5d08b | |||
45c1c27ea0 | |||
02cb3bfc36 | |||
55692154da | |||
814b55d595 | |||
a08cd1115b | |||
e04f92d3ac | |||
65bfbf8331 | |||
a2e33f68b6 | |||
b32d029675 | |||
9c6902197e | |||
208119a8fb | |||
416e0a18ea | |||
668bc2b3b1 | |||
01587dd5b8 | |||
67a3efe4dc | |||
477c66cd82 | |||
71860f9123 | |||
bef90382f3 | |||
56801bb11c | |||
6cc0765ad1 | |||
355606b92a | |||
812baaa994 | |||
7f470c84c6 | |||
c9da2c569c | |||
ce7de30e5c | |||
ff3c0fb23a | |||
b6c48c8e0e | |||
30846b675c | |||
3723103fe8 | |||
|
a76c0b1d69 | ||
|
6f0f70dd96 | ||
|
d2a9ec7936 | ||
|
ddbad1d1a2 | ||
|
7f4c3168f1 | ||
|
c8a35169f0 | ||
|
0c29ce63d7 | ||
|
7f292fbb76 | ||
|
d2ac199504 | ||
|
d247e6d38d | ||
|
e5e68b57ba | ||
|
fa0be3f522 | ||
|
170f3c6b53 | ||
|
fdbcf9a8ab | ||
|
fbfbe92aa1 | ||
|
1607d31beb | ||
|
769063f3cd | ||
|
76c0228e92 | ||
|
21cb51f612 | ||
|
d87c0cbbb8 | ||
|
7ca99f843e | ||
|
0289caa1fe | ||
|
31c99021c8 | ||
|
3080efd63c | ||
|
3fa15866cf | ||
|
ec17ee84e6 | ||
|
1dba20798a | ||
|
377f3771d6 | ||
|
6a6c5e5e14 | ||
|
afab1ab781 | ||
|
2899aa90de | ||
|
e11fe0c683 | ||
|
6897a3050c | ||
|
9f26f6dc6c | ||
|
470c5f9796 | ||
|
ed81904193 | ||
|
509449c9e5 | ||
|
b195c84f12 | ||
|
bff5e2aae6 | ||
|
984ecb559a | ||
|
25bc0c13b5 | ||
|
4a444614bc | ||
|
6bacdd4a1c | ||
|
a376c7bc22 | ||
|
122a14e50e | ||
|
2324388324 | ||
|
9f67090673 | ||
|
3779131ada | ||
|
9d5e083e81 | ||
|
f333d8d6e4 | ||
|
4c6c0d23b2 | ||
|
5cd8d15d5c | ||
|
098843f5d8 | ||
|
03e23c1ea4 | ||
|
47e46af2ca | ||
|
b090e99073 | ||
|
0bc8122880 | ||
|
f4c50b6cad | ||
|
c70ee60df9 | ||
|
0ae8cc8992 | ||
|
8e32350919 | ||
|
514562f8e7 | ||
|
5209eab34d | ||
|
27b1162e22 | ||
|
ea2de80597 | ||
|
a797a67997 | ||
|
976be98e02 | ||
|
67b0ca97b2 | ||
|
c94722952c | ||
|
9dbd68c048 | ||
|
796619d22b | ||
|
929d9cf8a4 | ||
|
6fedc57b96 | ||
|
d9c57486cc | ||
|
21a00ac9cd | ||
|
30fb6427c3 | ||
|
b46476fdf1 | ||
|
4f766fca0f | ||
|
20e3d71b2a | ||
|
4a18fadb67 | ||
|
5b03f34a79 | ||
|
b7be3c56bc | ||
|
ba8dea747e | ||
|
17d0a9f9c0 | ||
|
ceb7cdb3d9 | ||
|
1fb1591a03 | ||
|
6164c18f0d | ||
|
0970c95531 | ||
|
a1c27f973d | ||
|
013816cee3 | ||
|
43345f946e | ||
|
51e9b03ff2 | ||
|
87c774be6b | ||
|
e27d720d2f | ||
|
e5b84cd5f6 | ||
|
af10e21a87 | ||
|
527ede261b | ||
|
fef051e191 | ||
|
7d1b1a06d7 | ||
|
113d583524 | ||
|
53f1a88fd6 | ||
|
ee5c6ddaa0 | ||
|
1afcf222d7 | ||
|
453331f3dd | ||
|
16047b9d57 | ||
|
85cd1be513 | ||
|
81fc36e7d9 | ||
|
8a2642df7f | ||
|
b75e2659b4 | ||
|
cfda1dcdfe | ||
|
786bec76e9 | ||
|
d9c28d07f0 | ||
|
3c7649a733 | ||
|
1ebb0e7226 | ||
|
9f7387c54b |
48 changed files with 8127 additions and 1899 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules
|
|||
public
|
||||
.DS_Store
|
||||
.cache
|
||||
.vscode
|
||||
|
|
44
README.md
44
README.md
|
@ -26,22 +26,28 @@ npm build
|
|||
|
||||
This template uses the following plugins to extend the capabilities and features of 11ty:
|
||||
|
||||
| Package | Description |
|
||||
|------------------------------------------|----------------------------------------------------------|
|
||||
| `@11ty/eleventy` (`IdAttributePlugin`) | Adds an `id` to headings for anchor links |
|
||||
| `@11ty/eleventy-img` | Automated image processing and conversion |
|
||||
| `@11ty/eleventy-navigation` | Generate navigation from collections |
|
||||
| `@11ty/eleventy-plugin-rss` | Generates RSS or Atom XML feeds for news readers |
|
||||
| `@11ty/eleventy-plugin-syntaxhighlight` | Syntax highlighting for code blocks |
|
||||
| `@alexcarpenter/eleventy-plugin-caniuse` | Can I Use feature embeds |
|
||||
| `@grimlink/eleventy-plugin-lucide-icons` | Lucide icon shortcodes |
|
||||
| `@myxotod/eleventy-plugin-readingtime` | Adds an estimated reading time to posts |
|
||||
| `@quasibit/eleventy-plugin-sitemap` | Generates a `sitemap.xml` from collections |
|
||||
| `eleventy-plugin-embed-everything` | Embeds YouTube, Soundcloud, Spotify, Instagram, etc. |
|
||||
| `eleventy-plugin-metagen` | Generates ` ` tags for social media embeds |
|
||||
| `eleventy-plugin-og-image` | Generates OpenGraph images for social media sharing |
|
||||
| `eleventy-plugin-robotstxt` | Generates a `robots.txt` for the site |
|
||||
| `markdown-it-abbr` | Extends Markdown with syntax for `` tags |
|
||||
| `markdown-it-collapsible` | Extends Markdown with syntax for ` ` and ` ` tags |
|
||||
| `markdown-it-footnote` | Extends Markdown with syntax for footnotes |
|
||||
| `markdown-it-obsidian-callouts` | Extends Markdown with syntax for Obsidian style callouts |
|
||||
| Package | Description |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `@11ty/eleventy-img` | Automated image processing and conversion |
|
||||
| `@11ty/eleventy-navigation` | Generate navigation from collections |
|
||||
| `@11ty/eleventy-plugin-rss` | Generates RSS or Atom XML feeds for news readers |
|
||||
| `@11ty/eleventy-plugin-syntaxhighlight` | Syntax highlighting for code blocks |
|
||||
| `@alexcarpenter/eleventy-plugin-caniuse` | Can I Use feature embeds |
|
||||
| `@grimlink/eleventy-plugin-lucide-icons` | Lucide icon shortcodes |
|
||||
| `@myxotod/eleventy-plugin-readingtime` | Adds an estimated reading time to posts |
|
||||
| `@quasibit/eleventy-plugin-sitemap` | Generates a `sitemap.xml` from eleventy pages |
|
||||
| `@sardine/eleventy-plugin-tinyhtml` | Minify HTML output |
|
||||
| `eleventy-plugin-embed-everything` | Embeds YouTube, Soundcloud, Spotify, Instagram, etc. |
|
||||
| `eleventy-plugin-icons` | Plugin to use various icon sets in templates |
|
||||
| `eleventy-plugin-metagen` | Generates `<meta>` tags for social media embeds |
|
||||
| `eleventy-plugin-og-image` | Generates OpenGraph images for social media sharing |
|
||||
| `eleventy-plugin-robotstxt` | Generates a `robots.txt` for the site |
|
||||
| `eleventy-plugin-tailwindcss-4` | Integrates Tailwind 4 to the 11ty build process |
|
||||
| `markdown-it-abbr` | Extends Markdown with syntax for `<abbr>` tags |
|
||||
| `markdown-it-anchor` | Adds anchor tag IDs to headings |
|
||||
| `markdown-it-collapsible` | Extends Markdown with syntax for `<details>` and `<summary>` |
|
||||
| `markdown-it-deflist` | Extends Markdown with syntax for `<dl>`, `<dt>` and `<dd>` |
|
||||
| `markdown-it-footnote` | Extends Markdown with syntax for footnotes |
|
||||
| `markdown-it-image-figures` | Extends Markdown with syntax for `<figure>` and `<figcaption>` |
|
||||
| `markdown-it-obsidian-callouts` | Extends Markdown with syntax for footnotes Obsidian style callouts |
|
||||
| `simple-icons` | Simple Icons icon pack for use with `eleventy-plugin-icons` |
|
||||
|
|
|
@ -1,22 +1,46 @@
|
|||
import fs from 'node:fs';
|
||||
import { env } from 'node:process';
|
||||
import { eleventyImageTransformPlugin } from '@11ty/eleventy-img';
|
||||
import { feedPlugin } from '@11ty/eleventy-plugin-rss';
|
||||
import Image from '@11ty/eleventy-img';
|
||||
import eleventyPluginCiu from '@alexcarpenter/eleventy-plugin-caniuse';
|
||||
import eleventyPluginEmbedEverything from 'eleventy-plugin-embed-everything';
|
||||
import eleventyPluginIcons from 'eleventy-plugin-icons';
|
||||
import eleventyPluginLucideIcons from '@grimlink/eleventy-plugin-lucide-icons';
|
||||
import eleventyPluginMetagen from 'eleventy-plugin-metagen';
|
||||
import eleventyPluginNavigation from '@11ty/eleventy-navigation';
|
||||
import eleventyPluginOgImage from 'eleventy-plugin-og-image';
|
||||
import eleventyPluginReadingTime from '@myxotod/eleventy-plugin-readingtime';
|
||||
import eleventyPluginRobotsTxt from 'eleventy-plugin-robotstxt';
|
||||
import eleventyPluginRss from '@11ty/eleventy-plugin-rss';
|
||||
import eleventyPluginSyntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight';
|
||||
import eleventyPluginTOC from '@thedigitalman/eleventy-plugin-toc-a11y';
|
||||
import eleventyPluginTailwindcss from 'eleventy-plugin-tailwindcss-4';
|
||||
import eleventyPluginTinyHtml from '@sardine/eleventy-plugin-tinyhtml';
|
||||
import markdownIt from 'markdown-it';
|
||||
import markdownItAbbr from 'markdown-it-abbr';
|
||||
import markdownItAnchor from 'markdown-it-anchor'
|
||||
import markdownItAnchor from 'markdown-it-anchor';
|
||||
import markdownItCallouts from 'markdown-it-obsidian-callouts';
|
||||
import markdownItCollapsible from 'markdown-it-collapsible';
|
||||
import markdownItDeflist from 'markdown-it-deflist';
|
||||
import markdownItFootnote from 'markdown-it-footnote';
|
||||
import markdownItImageFigures from 'markdown-it-image-figures';
|
||||
|
||||
const urlFormat = ({ src, width, format }) => {
|
||||
const baseUrl = `https://img.sebin-nyshkim.net/i`;
|
||||
const imgUuid = src.match(/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
|
||||
const imgFormat = format === 'jpeg' ? 'jpg' : format;
|
||||
|
||||
if (src.startsWith(baseUrl)) return `${baseUrl}/${imgUuid[1]}.${imgFormat}?width=${width}`;
|
||||
|
||||
return src;
|
||||
};
|
||||
|
||||
const MARKDOWNIT_OPTIONS = { html: true, linkify: false, typographer: true };
|
||||
const ELEVENTY_IMAGE_DEFAULTS = {
|
||||
formats: ['webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/',
|
||||
urlFormat
|
||||
};
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy('./src/css/prism.css');
|
||||
|
@ -25,24 +49,48 @@ export default async function (eleventyConfig) {
|
|||
eleventyConfig.addPassthroughCopy('./src/fonts/');
|
||||
eleventyConfig.addWatchTarget('./src/fonts/');
|
||||
|
||||
// eleventyConfig.addPassthroughCopy('./src/img/');
|
||||
// eleventyConfig.addWatchTarget('./src/img/');
|
||||
eleventyConfig.addPassthroughCopy('./src/js/');
|
||||
eleventyConfig.addWatchTarget('./src/js/');
|
||||
|
||||
eleventyConfig.addCollection('posts', (collection) => {
|
||||
return collection.getFilteredByGlob('./src/posts/*.md');
|
||||
eleventyConfig.addCollection('posts', (collection) =>
|
||||
process.env.ELEVENTY_PRODUCTION
|
||||
? collection.getFilteredByGlob('./src/posts/*.md')
|
||||
: collection.getFilteredByGlob('./src/{posts,drafts}/*.md')
|
||||
);
|
||||
|
||||
eleventyConfig.addGlobalData('site_name', "Sebin's Blog");
|
||||
eleventyConfig.addGlobalData('type', 'article');
|
||||
eleventyConfig.addGlobalData('image', { width: 1200, height: 630, src: '', alt: '' });
|
||||
eleventyConfig.addGlobalData('author', {
|
||||
name: 'Sebin Nyshkim',
|
||||
href: 'https://blog.sebin-nyshkim.net',
|
||||
image: 'https://img.sebin-nyshkim.net/i/b6629b72-ab77-4a6c-bf97-b1a615cc2454'
|
||||
});
|
||||
eleventyConfig.addGlobalData('twitter', { cardType: 'summary_large_image' });
|
||||
eleventyConfig.addGlobalData('mastodon', { fediverseCreator: '@SebinNyshkim@meow.social' });
|
||||
|
||||
eleventyConfig.setFrontMatterParsingOptions({
|
||||
excerpt: (file) => {
|
||||
if (!file.data.tags) return; // immediately return if not a blog post with tags
|
||||
const separator = '<!-- more -->';
|
||||
const excerpt = file.content.substring(0, file.content.indexOf(separator)).trim();
|
||||
file.excerpt = new markdownIt(MARKDOWNIT_OPTIONS).render(excerpt).trim();
|
||||
}
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(eleventyPluginCiu);
|
||||
eleventyConfig.addPlugin(eleventyPluginEmbedEverything);
|
||||
eleventyConfig.addPlugin(eleventyPluginLucideIcons);
|
||||
eleventyConfig.addPlugin(eleventyPluginIcons, {
|
||||
sources: [{ name: 'simple', path: 'node_modules/simple-icons/icons' }]
|
||||
});
|
||||
eleventyConfig.addPlugin(eleventyPluginLucideIcons, { 'aria-hidden': 'true' });
|
||||
eleventyConfig.addPlugin(eleventyPluginMetagen);
|
||||
eleventyConfig.addPlugin(eleventyPluginNavigation);
|
||||
eleventyConfig.addPlugin(eleventyPluginReadingTime);
|
||||
eleventyConfig.addPlugin(eleventyPluginTOC);
|
||||
eleventyConfig.addPlugin(eleventyPluginRobotsTxt, {
|
||||
sitemapURL: 'https://blog.sebin-nyshkim.net/sitemap.xml',
|
||||
shouldBlockAIRobots: true,
|
||||
rules: new Map([['*', [{ allow: '/posts' }]]]),
|
||||
rules: new Map([['*', [{ allow: '/' }, { disallow: '/404.html' }, { disallow: '/og-images' }]]])
|
||||
});
|
||||
eleventyConfig.addPlugin(eleventyPluginSyntaxHighlight);
|
||||
eleventyConfig.addPlugin(eleventyPluginOgImage, {
|
||||
|
@ -53,6 +101,8 @@ export default async function (eleventyConfig) {
|
|||
<meta name="twitter:image" content="${host + src}">`;
|
||||
},
|
||||
satoriOptions: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Tilt Warp',
|
||||
|
@ -61,49 +111,65 @@ export default async function (eleventyConfig) {
|
|||
style: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
outputFileExtension: 'webp',
|
||||
sharpOptions: {
|
||||
quality: 100
|
||||
}
|
||||
});
|
||||
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
|
||||
...ELEVENTY_IMAGE_DEFAULTS,
|
||||
extensions: 'html',
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
sizes: ['1280', '720', '480', 'auto'],
|
||||
widths: [640, 800, 1280, 1920, 2560, 3840, 'auto'],
|
||||
defaultAttributes: {
|
||||
loading: 'lazy',
|
||||
decoding: 'async'
|
||||
decoding: 'async',
|
||||
sizes: '(min-width: 1280px) 960px, (min-width: 1024px) 768px, (min-width: 640px) 640px, 480px'
|
||||
}
|
||||
});
|
||||
eleventyConfig.addPlugin(feedPlugin, {
|
||||
type: 'atom', // or "rss", "json"
|
||||
outputPath: '/feed.xml',
|
||||
collection: {
|
||||
name: 'posts', // iterate over `collections.posts`
|
||||
limit: 0 // 0 means no limit
|
||||
},
|
||||
metadata: {
|
||||
language: 'en',
|
||||
title: "Sebin's Blog",
|
||||
subtitle: 'Writing about stuff I have vague interests in and commenting on stuff I read.',
|
||||
base: 'https://blog.sebin-nyshkim.net/',
|
||||
author: {
|
||||
name: 'Sebin Nyshkim',
|
||||
email: '' // Optional
|
||||
}
|
||||
}
|
||||
eleventyConfig.addPlugin(eleventyPluginRss);
|
||||
eleventyConfig.addPlugin(eleventyPluginTailwindcss, {
|
||||
input: 'css/style.css',
|
||||
output: 'css/style.css',
|
||||
minify: true
|
||||
});
|
||||
eleventyConfig.addPlugin(eleventyPluginTinyHtml, { removeOptionalTags: false });
|
||||
|
||||
eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(markdownItAbbr));
|
||||
eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(markdownItCollapsible));
|
||||
eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(markdownItCallouts));
|
||||
eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(markdownItFootnote));
|
||||
eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(markdownItAnchor));
|
||||
eleventyConfig.setLibrary('md', markdownIt(MARKDOWNIT_OPTIONS));
|
||||
|
||||
eleventyConfig.amendLibrary('md', (mdLib) =>
|
||||
mdLib
|
||||
.use(markdownItAbbr)
|
||||
.use(markdownItAnchor)
|
||||
.use(markdownItCallouts)
|
||||
.use(markdownItCollapsible)
|
||||
.use(markdownItDeflist)
|
||||
.use(markdownItFootnote)
|
||||
.use(markdownItImageFigures, { figcaption: true })
|
||||
);
|
||||
|
||||
eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`);
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
...ELEVENTY_IMAGE_DEFAULTS,
|
||||
widths: [1920, 2560, 3840]
|
||||
});
|
||||
|
||||
const getSets = ({ url, sourceType }, i) => `url(${url}) type('${sourceType}') ${i + 1}x`;
|
||||
|
||||
return Object.values(imgset)
|
||||
.map((format) => format.map(getSets))
|
||||
.flat();
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('toDateObj', (dateString) => new Date(dateString));
|
||||
eleventyConfig.addFilter('isoDate', (dateObj) => dateObj.toISOString());
|
||||
eleventyConfig.addFilter('longDate', (dateObj) => dateObj.toString());
|
||||
eleventyConfig.addFilter('readableDate', (dateObj) =>
|
||||
dateObj.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
);
|
||||
eleventyConfig.addFilter('toHTML', (str) => new markdownIt(MARKDOWNIT_OPTIONS).render(str ? str : ''));
|
||||
eleventyConfig.addFilter('toPlain', (str) => (str ? str.replace(/<[^>]+>/g, '') : null));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
|
@ -111,6 +177,7 @@ export const config = {
|
|||
input: 'src',
|
||||
output: 'public',
|
||||
layouts: 'layouts',
|
||||
includes: 'includes'
|
||||
includes: 'includes',
|
||||
data: 'data'
|
||||
}
|
||||
};
|
||||
|
|
5156
package-lock.json
generated
5156
package-lock.json
generated
File diff suppressed because it is too large
Load diff
37
package.json
37
package.json
|
@ -4,8 +4,11 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "eleventy --serve & npx tailwindcss -i ./src/css/style.css -o ./public/css/style.css --watch",
|
||||
"build": "ELEVENTY_PRODUCTION=true eleventy && NODE_ENV=production npx tailwindcss -i ./src/css/style.css -o ./public/css/style.css --minify"
|
||||
"start": "eleventy --serve --incremental",
|
||||
"build": "ELEVENTY_PRODUCTION=true eleventy --incremental",
|
||||
"build:clean": "rm -rf ./public",
|
||||
"build:lightningcss": "lightningcss --minify --bundle --targets '> 0.2% and not dead, firefox >= 115' ./public/css/style.css -o ./public/css/style.css",
|
||||
"upload": "rsync -avc --progress --delete -e ssh ./public/ dragonhoard:/mnt/user/appdata/sebin-blog/www/"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
|
@ -16,28 +19,34 @@
|
|||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^3.0.0",
|
||||
"@11ty/eleventy-img": "^5.0.0",
|
||||
"@11ty/eleventy": "^3.1.2",
|
||||
"@11ty/eleventy-img": "^6.0.4",
|
||||
"@11ty/eleventy-navigation": "^0.3.5",
|
||||
"@11ty/eleventy-plugin-rss": "^2.0.2",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||
"@11ty/eleventy-plugin-rss": "^2.0.4",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.1",
|
||||
"@alexcarpenter/eleventy-plugin-caniuse": "^0.1.0",
|
||||
"@grimlink/eleventy-plugin-lucide-icons": "^2.0.7",
|
||||
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.7",
|
||||
"@myxotod/eleventy-plugin-readingtime": "^2.0.0",
|
||||
"@quasibit/eleventy-plugin-sitemap": "^2.2.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@thedigitalman/eleventy-plugin-toc-a11y": "^2.1.0",
|
||||
"eleventy-google-fonts": "^0.1.0",
|
||||
"eleventy-plugin-embed-everything": "^1.19.0",
|
||||
"@sardine/eleventy-plugin-tinyhtml": "^0.2.0",
|
||||
"@tailwindcss/cli": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"eleventy-plugin-embed-everything": "^1.21.0",
|
||||
"eleventy-plugin-icons": "^4.5.3",
|
||||
"eleventy-plugin-metagen": "^1.8.3",
|
||||
"eleventy-plugin-og-image": "^4.0.0",
|
||||
"eleventy-plugin-og-image": "^4.1.0",
|
||||
"eleventy-plugin-robotstxt": "^1.0.0",
|
||||
"eleventy-plugin-tailwindcss-4": "^2.0.1",
|
||||
"lightningcss-cli": "^1.30.1",
|
||||
"markdown-it-abbr": "^2.0.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-collapsible": "^2.0.2",
|
||||
"markdown-it-deflist": "^3.0.0",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdown-it-obsidian-callouts": "^0.3.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"markdown-it-image-figures": "^2.1.1",
|
||||
"markdown-it-obsidian-callouts": "^0.3.2",
|
||||
"simple-icons": "^14.15.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss-safe-area": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
|
13
src/404.md
Normal file
13
src/404.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: 404 Not found
|
||||
layout: page.njk
|
||||
permalink: /404.html
|
||||
---
|
||||
|
||||
# Here be no dragons
|
||||
|
||||

|
||||
|
||||
The page you were looking for does not exist!
|
||||
|
||||
It may have been moved or deleted. Use the navigation on the top of the page to select where you wanna go.
|
27
src/about.md
27
src/about.md
|
@ -8,9 +8,9 @@ eleventyNavigation:
|
|||
|
||||
# {{ title }}
|
||||
|
||||

|
||||

|
||||
|
||||
| | |
|
||||
| Key | Value |
|
||||
| --------- | ----------------- |
|
||||
| Name | Sebin |
|
||||
| Age | 36 |
|
||||
|
@ -19,15 +19,25 @@ eleventyNavigation:
|
|||
|
||||
I'm a dude from the south of Germany pretending to be a dragon online.
|
||||
|
||||
I'm a web developer at heart and love to make pretty and sleek websites (as you might have noticed). I started profesionally coding right around the time HTML5 took off and kept at it ever since. You can take a look at some of my personal projects over on my [main home page].
|
||||
I'm a web developer at heart and love to make pretty and sleek websites (as you might have noticed). I started coding professionally right around the time HTML5 took off and kept at it ever since. You can take a look at some of my personal projects over on my [main home page].
|
||||
|
||||
My code repos[^1]:
|
||||
My code repos[^gitrepos]:
|
||||
|
||||
- [GitHub]
|
||||
- [GitLab]
|
||||
- [Forgejo][selfgit]
|
||||
|
||||
Besides that I also love FOSS and use Arch Linux (btw) as my daily driver! Whenever I can I try to get involved and contribute translations to various tools and apps. I've been running Linux for the better part of the last 10 years, going from Linux Mint, to Manjaro and eventually pure Arch. It gave me a whole new appreciation for how computers work and being able to fully control my general computing experience (the privacy benefits are cool, too).
|
||||
|
||||
My tools of the trade:
|
||||
|
||||
| Software | Notes |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| Arch Linux | Highly customizable rolling release Linux distribution |
|
||||
| GNOME | Sleek, modern desktop environment with solid Wayland support |
|
||||
| Firefox | It's not Chromium |
|
||||
| Visual Studio Code | My code editor of choice |
|
||||
|
||||
When I'm not doing development work, I'm getting my hands dirty in self-hosting. Services I'm running include:
|
||||
|
||||
- [Navidrome] (music streaming)
|
||||
|
@ -37,6 +47,8 @@ When I'm not doing development work, I'm getting my hands dirty in self-hosting.
|
|||
- [Wallabag] (read-it-later pile of shame)
|
||||
- [Nextcloud] (cloud storage & collaboration)
|
||||
- [Immich] (Google Photos without the Google parts)
|
||||
- [AdGuard Home] (DNS Sinkhole for ads and trackers)
|
||||
- [Forgejo] (light-weight Git forge)
|
||||
|
||||
As for the pretending to be a dragon online thing: Yes, I'm one of those furries. Been one for the past 20 years. Also hella gay 🏳️🌈
|
||||
|
||||
|
@ -46,8 +58,9 @@ Video games also shaped a lot of my music tastes. I'm listening to tons of video
|
|||
|
||||
[main home page]: https://sebin-nyshkim.net
|
||||
|
||||
[GitHub]: https://github.com/SebinNyshkim
|
||||
[GitHub]: https://github.com/SebinNyshkim
|
||||
[GitLab]: https://gitlab.com/SebinNyshkim
|
||||
[selfgit]: https://git.sebin-nyshkim.net/SebinNyshkim
|
||||
|
||||
[Navidrome]: https://www.navidrome.org/
|
||||
[Jellyfin]: https://jellyfin.org/
|
||||
|
@ -56,6 +69,8 @@ Video games also shaped a lot of my music tastes. I'm listening to tons of video
|
|||
[Wallabag]: https://wallabag.org/
|
||||
[Nextcloud]: https://nextcloud.com/
|
||||
[Immich]: https://immich.app/
|
||||
[AdGuard Home]: https://adguard.com/adguard-home/overview.html
|
||||
[Forgejo]: https://forgejo.org/
|
||||
|
||||
[Secret of Mana]: https://www.igdb.com/games/secret-of-mana
|
||||
[Secret of Evermore]: https://www.igdb.com/games/secret-of-evermore
|
||||
|
@ -77,4 +92,4 @@ Video games also shaped a lot of my music tastes. I'm listening to tons of video
|
|||
*[NES]: Nintendo Entertainment System
|
||||
*[SNES]: Super NES
|
||||
|
||||
[^1]: In case you're wondering: most of my repos are personal projects, just for me, and don't need to be developed out in the openIn case you're wonderin
|
||||
[^gitrepos]: In case you're wondering: most of my repos are personal projects, just for me, and don't need to be developed out in the open.
|
||||
|
|
|
@ -14,14 +14,12 @@ I can be found all over the Internet!
|
|||
| ------------ | --------------------------- |
|
||||
| Mastodon | [@SebinNyshkim@meow.social] |
|
||||
| Bluesky | [@sebin-nyshkim.net] |
|
||||
| Twitter | [@SebinNyshkim] |
|
||||
| Fur Affinity | [sonofdragons] |
|
||||
|
||||
If you prefer a more direct line to me, there's [Telegram], [Discord] and [Signal].
|
||||
|
||||
[@SebinNyshkim@meow.social]: https://meow.social/@SebinNyshkim
|
||||
[@sebin-nyshkim.net]: https://bsky.app/profile/sebin-nyshkim.net
|
||||
[@SebinNyshkim]: https://twitter.com/SebinNyshkim
|
||||
[sonofdragons]: https://www.furaffinity.net/user/sonofdragons
|
||||
[Telegram]: https://t.me/SebinNyshkim
|
||||
[Discord]: https://discordapp.com/users/227753049530040321
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.read-more-button {
|
||||
@apply inline-flex items-center gap-2 rounded-xl bg-sky-600 px-5 py-2 text-lg font-bold text-white no-underline transition-colors duration-300 hover:bg-sky-700;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
.callout {
|
||||
@apply rounded-lg border-s-8 border-gray-700 bg-gray-200 p-4 text-gray-700 dark:border-gray-200 dark:bg-gray-900 dark:text-gray-200;
|
||||
@apply rounded-lg border-s-8 border-gray-700 bg-gray-200 p-4 text-gray-700 dark:border-gray-200 dark:bg-gray-900 dark:text-gray-200 pe-9;
|
||||
|
||||
margin: 1.25em 0;
|
||||
|
||||
|
@ -17,7 +17,8 @@
|
|||
}
|
||||
|
||||
.callout-title {
|
||||
@apply flex items-center font-bold;
|
||||
@apply flex items-center;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
.callout-title-icon {
|
||||
|
@ -28,7 +29,9 @@
|
|||
@apply ms-9;
|
||||
}
|
||||
|
||||
.callout[data-callout='abstract'] {
|
||||
.callout[data-callout='abstract'],
|
||||
.callout[data-callout='summary'],
|
||||
.callout[data-callout='tldr'] {
|
||||
@apply border-fuchsia-700 bg-fuchsia-200 text-fuchsia-700 dark:border-fuchsia-200 dark:bg-fuchsia-900 dark:text-fuchsia-200;
|
||||
}
|
||||
|
||||
|
@ -36,7 +39,8 @@
|
|||
@apply border-rose-700 bg-rose-200 text-rose-700 dark:border-rose-200 dark:bg-rose-900 dark:text-rose-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='danger'] {
|
||||
.callout[data-callout='danger'],
|
||||
.callout[data-callout='error'] {
|
||||
@apply border-amber-700 bg-amber-200 text-amber-700 dark:border-amber-200 dark:bg-amber-900 dark:text-amber-200;
|
||||
}
|
||||
|
||||
|
@ -44,7 +48,9 @@
|
|||
@apply border-stone-700 bg-stone-200 text-stone-700 dark:border-stone-200 dark:bg-stone-900 dark:text-stone-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='failure'] {
|
||||
.callout[data-callout='failure'],
|
||||
.callout[data-callout='fail'],
|
||||
.callout[data-callout='missing'] {
|
||||
@apply border-red-700 bg-red-200 text-red-700 dark:border-red-200 dark:bg-red-900 dark:text-red-200;
|
||||
}
|
||||
|
||||
|
@ -56,19 +62,26 @@
|
|||
@apply border-cyan-700 bg-cyan-200 text-cyan-700 dark:border-cyan-200 dark:bg-cyan-900 dark:text-cyan-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='question'] {
|
||||
.callout[data-callout='question'],
|
||||
.callout[data-callout='faq'],
|
||||
.callout[data-callout='help'] {
|
||||
@apply border-blue-700 bg-blue-200 text-blue-700 dark:border-blue-200 dark:bg-blue-900 dark:text-blue-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='quote'] {
|
||||
.callout[data-callout='quote'],
|
||||
.callout[data-callout='cite'] {
|
||||
@apply border-zinc-700 bg-zinc-200 text-zinc-700 dark:border-zinc-200 dark:bg-zinc-900 dark:text-zinc-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='success'] {
|
||||
.callout[data-callout='success'],
|
||||
.callout[data-callout='check'],
|
||||
.callout[data-callout='done'] {
|
||||
@apply border-emerald-700 bg-emerald-200 text-emerald-700 dark:border-emerald-200 dark:bg-emerald-900 dark:text-emerald-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='tip'] {
|
||||
.callout[data-callout='tip'],
|
||||
.callout[data-callout='hint'],
|
||||
.callout[data-callout='important'] {
|
||||
@apply border-indigo-700 bg-indigo-200 text-indigo-700 dark:border-indigo-200 dark:bg-indigo-900 dark:text-indigo-200;
|
||||
}
|
||||
|
||||
|
@ -76,6 +89,8 @@
|
|||
@apply border-lime-700 bg-lime-200 text-lime-700 dark:border-lime-200 dark:bg-lime-900 dark:text-lime-200;
|
||||
}
|
||||
|
||||
.callout[data-callout='warning'] {
|
||||
.callout[data-callout='warning'],
|
||||
.callout[data-callout='attention'],
|
||||
.callout[data-callout='caution'] {
|
||||
@apply border-amber-700 bg-amber-200 text-amber-700 dark:border-amber-200 dark:bg-amber-900 dark:text-amber-200;
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
details {
|
||||
@apply rounded-lg border border-solid border-sky-500 text-sky-500;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
@apply rounded-none rounded-t-lg;
|
||||
}
|
||||
|
||||
summary {
|
||||
@apply rounded-lg bg-sky-200 p-4;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.eleventy-navigation .nav-list {
|
||||
@apply mx-3 flex gap-2 md:mx-6 md:gap-3;
|
||||
}
|
||||
|
||||
.eleventy-navigation .nav-link {
|
||||
@apply rounded-xl px-4 py-2 text-base sm:text-lg md:text-xl capitalize text-white transition-all duration-300 hover:bg-sky-900 hover:shadow-lg md:m-0 md:max-h-12 hover:dark:bg-sky-800;
|
||||
}
|
||||
|
||||
.eleventy-navigation a.active {
|
||||
@apply bg-sky-900 shadow-lg dark:bg-sky-800;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
.pagination {
|
||||
@apply flex justify-center gap-2;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
@apply flex size-10 items-center justify-center rounded-full;
|
||||
}
|
||||
|
||||
.pagination [aria-current] {
|
||||
@apply text-slate-300 bg-sky-600 dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
@apply bg-slate-100 hover:bg-sky-600 hover:text-slate-300 dark:bg-slate-800 hover:dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
@apply cursor-not-allowed bg-slate-400 text-slate-300 dark:bg-slate-950 dark:text-slate-700;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
.postmeta {
|
||||
@apply flex flex-wrap items-center gap-4;
|
||||
}
|
||||
|
||||
.postmeta > * {
|
||||
@apply inline-flex items-center gap-1;
|
||||
}
|
||||
|
||||
.postmeta a {
|
||||
@apply hover:text-sky-400;
|
||||
}
|
||||
|
||||
.postmeta svg {
|
||||
@apply stroke-sky-600 dark:stroke-sky-600;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
.tags {
|
||||
@apply flex gap-1;
|
||||
}
|
||||
|
||||
.tags .tag {
|
||||
@apply inline-flex items-center gap-1 rounded-md border border-solid border-sky-500 bg-sky-200 px-1.5 py-0.5 text-sky-500 dark:border-sky-500 dark:bg-sky-950 dark:text-sky-500;
|
||||
}
|
|
@ -1,284 +1,202 @@
|
|||
pre[class*='language-'] {
|
||||
--padding-y: var(--am-prism-padding-y, 1rem);
|
||||
--padding-x: var(--am-prism-padding-x, 1rem);
|
||||
padding: var(--padding-y) var(--padding-x);
|
||||
overflow: auto;
|
||||
font-size: var(--am-prism-font-size, 0.85em);
|
||||
border-radius: var(--am-prism-border-radius, 0.4em);
|
||||
}
|
||||
pre > code[class*='language-'] {
|
||||
--am-prism-font-family: 'M PLUS 1 Code';
|
||||
padding: initial;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
font-family: var(--am-prism-font-family, ui-monospace), monospace;
|
||||
line-height: var(--am-prism-line-height, 1.5);
|
||||
background-color: initial;
|
||||
}
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
pre[class*='language-'] .line-numbers-rows {
|
||||
box-sizing: content-box;
|
||||
margin: calc(var(--padding-y) * -1) 0;
|
||||
padding: var(--padding-y) 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.line-numbers.line-numbers .line-numbers-rows {
|
||||
border-right-width: var(--am-prism-border-width, 1px);
|
||||
border-right-color: var(--am-prism-border-color);
|
||||
}
|
||||
.line-numbers .line-numbers-rows > span:before {
|
||||
color: var(--am-prism-line-numbers-color);
|
||||
}
|
||||
div.code-toolbar > .toolbar {
|
||||
top: 0.3rem !important;
|
||||
right: 0.3rem !important;
|
||||
}
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button.copy-to-clipboard-button {
|
||||
display: inline-flex;
|
||||
padding: 0 0.75em;
|
||||
font-size: var(--am-prism-font-size, 0.8em);
|
||||
font-family: var(--am-prism-font-family, ui-monospace), monospace;
|
||||
font-weight: 600 !important;
|
||||
line-height: 2.25em;
|
||||
color: var(--am-prism-copy-color);
|
||||
background-color: var(--am-prism-copy-bg);
|
||||
border-radius: calc(var(--am-prism-border-radius, 0.4em) - 0.1em);
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button.copy-to-clipboard-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button.copy-to-clipboard-button:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #24292f;
|
||||
}
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-']::selection {
|
||||
text-shadow: none;
|
||||
background: #9fc6e9;
|
||||
}
|
||||
pre[class*='language-'] {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
color: #24292f;
|
||||
background: #eff1f3;
|
||||
}
|
||||
pre[data-line] {
|
||||
position: relative;
|
||||
}
|
||||
pre[class*='language-'] > code[class*='language-'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: inherit 0;
|
||||
margin-top: 1em;
|
||||
background: #fff8c5;
|
||||
box-shadow: inset 5px 0 0 #eed888;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
line-height: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.token.cdata,
|
||||
.token.comment,
|
||||
.token.doctype,
|
||||
.token.prolog {
|
||||
color: #6e7781;
|
||||
}
|
||||
.token.punctuation {
|
||||
color: #24292f;
|
||||
}
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.deleted,
|
||||
.token.number,
|
||||
.token.property,
|
||||
.token.symbol,
|
||||
.token.tag {
|
||||
color: #0550ae;
|
||||
}
|
||||
.token.attr-name,
|
||||
.token.builtin,
|
||||
.token.char,
|
||||
.token.inserted,
|
||||
.token.selector,
|
||||
.token.string {
|
||||
color: #0a3069;
|
||||
}
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.entity,
|
||||
.token.operator,
|
||||
.token.url {
|
||||
color: #0550ae;
|
||||
}
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #cf222e;
|
||||
}
|
||||
.token.function {
|
||||
color: #8250df;
|
||||
}
|
||||
.token.important,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #0a3069;
|
||||
}
|
||||
.token.bold,
|
||||
.token.important {
|
||||
font-weight: 700;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
:root {
|
||||
--am-prism-line-numbers-color: #6e778177;
|
||||
--am-prism-border-color: #6e778122;
|
||||
--am-prism-copy-color: #24292faa;
|
||||
--am-prism-copy-bg: #6e778122;
|
||||
--text-color: #4c4f69;
|
||||
--background-color: #e6e9ef;
|
||||
|
||||
--token-color-keyword: #8839ef;
|
||||
--token-color-builtin: #d20f39;
|
||||
--token-color-class-name: #df8e1d;
|
||||
--token-color-function: #1e66f5;
|
||||
--token-color-number: #fe640b;
|
||||
--token-color-string: #40a02b;
|
||||
--token-color-symbol: var(--token-color-class-name);
|
||||
--token-color-regex: #ea76cb;
|
||||
--token-color-url: var(--token-color-string);
|
||||
--token-color-operator: #04a5e5;
|
||||
--token-color-variable: #4c4f69;
|
||||
--token-color-constant: var(--token-color-number);
|
||||
--token-color-property: var(--token-color-function);
|
||||
--token-color-punctuation: #7c7f93;
|
||||
--token-color-important: var(--token-color-keyword);
|
||||
--token-color-comment: var(--token-color-punctuation);
|
||||
--token-color-tag: var(--token-color-function);
|
||||
--token-color-attr-name: var(--token-color-class-name);
|
||||
--token-color-attr-value: var(--token-color-string);
|
||||
--token-color-namespace: var(--token-color-class-name);
|
||||
--token-color-doctype: var(--token-color-keyword);
|
||||
--token-color-cdata: #179299;
|
||||
--token-color-entity: var(--token-color-builtin);
|
||||
--token-color-atrule: var(--token-color-keyword);
|
||||
--token-color-selector: var(--token-color-function);
|
||||
--token-color-deleted: var(--token-color-builtin);
|
||||
--token-color-inserted: var(--token-color-string);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-']::selection {
|
||||
text-shadow: none;
|
||||
background: #234879;
|
||||
}
|
||||
pre[class*='language-'] {
|
||||
background: #161b22;
|
||||
}
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
color: #c9d1d9;
|
||||
background: #343942;
|
||||
}
|
||||
pre[data-line] {
|
||||
position: relative;
|
||||
}
|
||||
pre[class*='language-'] > code[class*='language-'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: inherit 0;
|
||||
margin-top: 1em;
|
||||
background: #2f2a1e;
|
||||
box-shadow: inset 5px 0 0 #674c16;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
line-height: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.token.cdata,
|
||||
.token.comment,
|
||||
.token.doctype,
|
||||
.token.prolog {
|
||||
color: #8b949e;
|
||||
}
|
||||
.token.punctuation,
|
||||
.token.variable {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.deleted,
|
||||
.token.number,
|
||||
.token.property,
|
||||
.token.symbol,
|
||||
.token.tag {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.token.attr-name,
|
||||
.token.builtin,
|
||||
.token.char,
|
||||
.token.inserted,
|
||||
.token.selector,
|
||||
.token.string {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.entity,
|
||||
.token.operator,
|
||||
.token.url {
|
||||
color: #a5d6ff;
|
||||
background: #161b22;
|
||||
}
|
||||
.token.atrule,
|
||||
.token.keyword {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.token.attr-value,
|
||||
.token.function {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
.token.class-name,
|
||||
.token.important,
|
||||
.token.regex {
|
||||
color: #a8daff;
|
||||
}
|
||||
.token.bold,
|
||||
.token.important {
|
||||
font-weight: 700;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
:root {
|
||||
--am-prism-line-numbers-color: #8b949e55;
|
||||
--am-prism-border-color: #8b949e22;
|
||||
--am-prism-copy-color: #c9d1d9;
|
||||
--am-prism-copy-bg: #8b949e22;
|
||||
--text-color: #c6d0f5;
|
||||
--background-color: #292c3c;
|
||||
|
||||
--token-color-keyword: #ca9ee6;
|
||||
--token-color-builtin: #e78284;
|
||||
--token-color-class-name: #e5c890;
|
||||
--token-color-function: #8caaee;
|
||||
--token-color-number: #ef9f76;
|
||||
--token-color-string: #a6d189;
|
||||
--token-color-symbol: var(--token-color-class-name);
|
||||
--token-color-regex: #f4b8e4;
|
||||
--token-color-url: var(--token-color-string);
|
||||
--token-color-operator: #99d1db;
|
||||
--token-color-variable: #c6d0f5;
|
||||
--token-color-constant: var(--token-color-number);
|
||||
--token-color-property: var(--token-color-function);
|
||||
--token-color-punctuation: #949cbb;
|
||||
--token-color-important: var(--token-color-keyword);
|
||||
--token-color-comment: var(--token-color-punctuation);
|
||||
--token-color-tag: var(--token-color-function);
|
||||
--token-color-attr-name: var(--token-color-class-name);
|
||||
--token-color-attr-value: var(--token-color-string);
|
||||
--token-color-namespace: var(--token-color-class-name);
|
||||
--token-color-doctype: var(--token-color-keyword);
|
||||
--token-color-cdata: #81c8be;
|
||||
--token-color-entity: var(--token-color-builtin);
|
||||
--token-color-atrule: var(--token-color-keyword);
|
||||
--token-color-selector: var(--token-color-function);
|
||||
--token-color-deleted: var(--token-color-builtin);
|
||||
--token-color-inserted: var(--token-color-string);
|
||||
}
|
||||
}
|
||||
|
||||
:where(pre, code)[class*='language-'] {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
:where(:not(pre) > code, pre)[class*='language-'] {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
/* https://prismjs.com/tokens.html */
|
||||
|
||||
.token {
|
||||
&.keyword {
|
||||
color: var(--token-color-keyword);
|
||||
}
|
||||
|
||||
&.builtin {
|
||||
color: var(--token-color-builtin);
|
||||
}
|
||||
|
||||
&.class-name {
|
||||
color: var(--token-color-class-name);
|
||||
}
|
||||
|
||||
&.function {
|
||||
color: var(--token-color-function);
|
||||
}
|
||||
|
||||
&.boolean,
|
||||
&.number {
|
||||
color: var(--token-color-number);
|
||||
}
|
||||
|
||||
&.string,
|
||||
&.char {
|
||||
color: var(--token-color-string);
|
||||
}
|
||||
|
||||
&.symbol {
|
||||
color: var(--token-color-symbol);
|
||||
}
|
||||
|
||||
&.regex {
|
||||
color: var(--token-color-regex);
|
||||
}
|
||||
|
||||
&.url {
|
||||
color: var(--token-color-url);
|
||||
}
|
||||
|
||||
&.operator {
|
||||
color: var(--token-color-operator);
|
||||
}
|
||||
|
||||
&.variable {
|
||||
color: var(--token-color-variable);
|
||||
}
|
||||
|
||||
&.constant {
|
||||
color: var(--token-color-constant);
|
||||
}
|
||||
|
||||
&.property {
|
||||
color: var(--token-color-property);
|
||||
}
|
||||
|
||||
&.punctuation {
|
||||
color: var(--token-color-punctuation);
|
||||
}
|
||||
|
||||
&.important {
|
||||
color: var(--token-color-important);
|
||||
}
|
||||
|
||||
&.comment {
|
||||
color: var(--token-color-comment);
|
||||
}
|
||||
|
||||
&.tag {
|
||||
color: var(--token-color-tag);
|
||||
}
|
||||
|
||||
&.attr-name {
|
||||
color: var(--token-color-attr-name);
|
||||
}
|
||||
|
||||
&.attr-value {
|
||||
color: var(--token-color-attr-value);
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
color: var(--token-color-namespace);
|
||||
}
|
||||
|
||||
&.prolog,
|
||||
&.doctype {
|
||||
color: var(--token-color-doctype);
|
||||
}
|
||||
|
||||
&.cdata {
|
||||
color: var(--token-color-cdata);
|
||||
}
|
||||
|
||||
&.entity {
|
||||
color: var(--token-color-entity);
|
||||
}
|
||||
|
||||
&.atrule {
|
||||
color: var(--token-color-atrule);
|
||||
}
|
||||
|
||||
&.selector {
|
||||
color: var(--token-color-selector);
|
||||
}
|
||||
|
||||
/* Diff */
|
||||
|
||||
&.deleted {
|
||||
color: var(--token-color-deleted);
|
||||
}
|
||||
|
||||
&.inserted {
|
||||
color: var(--token-color-inserted);
|
||||
}
|
||||
|
||||
/* Other */
|
||||
|
||||
&.important,
|
||||
&.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
&.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
@import url(modules/buttons.css);
|
||||
@import url(modules/callouts.css);
|
||||
@import url(modules/details.css);
|
||||
@import url(modules/navigation.css);
|
||||
@import url(modules/pagination.css);
|
||||
@import url(modules/postmeta.css);
|
||||
@import url(modules/tags.css);
|
||||
@import 'tailwindcss';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import './modules/callouts.css' layer(base);
|
||||
|
||||
@plugin '@tailwindcss/typography';
|
||||
@plugin 'tailwindcss-safe-area';
|
||||
|
||||
@theme {
|
||||
--grid-template-rows-article-header: 1fr auto;
|
||||
|
||||
--min-height-128: 32rem;
|
||||
--min-height-160: 40rem;
|
||||
--min-height-192: 48rem;
|
||||
|
||||
--width-128: calc(var(--spacing) * 128);
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Encode Sans', sans-serif;
|
||||
--font-copy: 'Encode Sans', sans-serif;
|
||||
--font-heading: 'Tilt Warp', sans-serif;
|
||||
--font-monospace: 'M PLUS 1 Code', monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-copy);
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
|
@ -23,27 +34,72 @@ h3,
|
|||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: 'Tilt Warp', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variation-settings: 'XROT' 0, 'YROT' 0;
|
||||
font-family: var(--font-heading);
|
||||
@apply text-balance;
|
||||
}
|
||||
|
||||
a:not(.nav-link, .read-more-button) {
|
||||
blockquote {
|
||||
@apply relative mt-10 rounded-xl border-2 border-sky-500 ps-6 pe-6 font-normal text-slate-700 not-italic *:before:content-none *:after:content-none lg:ps-8 lg:pe-8 2xl:ps-10 2xl:pe-10 dark:text-slate-300;
|
||||
|
||||
&::before {
|
||||
@apply absolute -top-8 left-4 block h-12 w-20 bg-slate-300 text-center text-8xl leading-none font-bold text-slate-300 not-italic content-['“'] dark:bg-slate-900 dark:text-slate-900;
|
||||
-webkit-text-stroke: var(--color-sky-600) 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
a:not(nav a, .button, :has(h1)) {
|
||||
@apply font-normal text-inherit underline decoration-sky-600 decoration-2 underline-offset-4 transition-colors duration-300 hover:text-sky-600;
|
||||
}
|
||||
|
||||
.blogpost {
|
||||
@apply prose prose-slate md:prose-lg lg:prose-xl dark:prose-invert prose-headings:font-normal prose-strong:text-inherit prose-li:marker:!text-inherit prose-th:font-bold prose-img:rounded-3xl;
|
||||
details {
|
||||
@apply overflow-clip rounded-xl shadow-xl border border-solid border-sky-600 bg-sky-200 text-sky-600 dark:bg-sky-950 dark:text-sky-300;
|
||||
|
||||
margin: 1.25em 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin: 1.3333333em 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
> :not(summary) {
|
||||
@apply px-4 last:pb-4 sm:px-6 sm:last:pb-6;
|
||||
}
|
||||
|
||||
&[open] summary {
|
||||
@apply border-b border-solid border-sky-600;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-toc {
|
||||
@apply sticky top-0 right-0 prose prose-sm;
|
||||
summary {
|
||||
@apply cursor-pointer p-4;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
.footnote-item:target p {
|
||||
@apply bg-slate-200 dark:bg-sky-950;
|
||||
dl {
|
||||
& dt {
|
||||
@apply font-normal! text-sky-600! dark:text-sky-300!;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
& code {
|
||||
@apply text-sky-800! dark:text-sky-500!;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-item {
|
||||
padding-inline: 0.4em;
|
||||
|
||||
&:target {
|
||||
@apply bg-slate-200 dark:bg-sky-950;
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
|
@ -54,6 +110,10 @@ abbr {
|
|||
}
|
||||
}
|
||||
|
||||
s {
|
||||
@apply line-through decoration-red-800 decoration-2;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
abbr {
|
||||
@apply underline decoration-sky-600 decoration-dotted decoration-2 underline-offset-4;
|
||||
|
|
9
src/data/eleventyComputed.js
Normal file
9
src/data/eleventyComputed.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { env } from 'node:process';
|
||||
|
||||
export default {
|
||||
site: {
|
||||
url: env.ELEVENTY_PRODUCTION ? 'https://blog.sebin-nyshkim.net' : 'http://localhost:8080'
|
||||
},
|
||||
page_title: (data) => (data.title ? `${data.title} - ${data.site_name}` : data.site_name),
|
||||
og_title: (data) => (data.title ? data.title : data.site_name)
|
||||
};
|
5
src/drafts/drafts.json
Normal file
5
src/drafts/drafts.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"layout": "blogpost.njk",
|
||||
"permalink": "/drafts/{{ title | slugify }}/",
|
||||
"date": "git Created"
|
||||
}
|
34
src/faq.md
Normal file
34
src/faq.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
title: FAQ
|
||||
layout: page.njk
|
||||
eleventyNavigation:
|
||||
key: FAQ
|
||||
order: 5
|
||||
---
|
||||
|
||||
# {{ title }}
|
||||
|
||||

|
||||
|
||||
I'm sure you have questions, this is where I'll try to answer them.
|
||||
|
||||
## Where's the comment section?
|
||||
|
||||
I've deliberately decided not to have comments on this blog for two reasons:
|
||||
|
||||
1. I'm a resident of Germany and having a comment section would legally require me to check every comment for incriminating material. As the comments become part of what is published on this site, I also become responsible for the contents in these comments in the eyes of the law. Furthermore, the European Court of Justice considers incriminating comments made on websites an offense, even after they have been deleted.
|
||||
2. Spam.
|
||||
|
||||
Since this is supposed to be a hobby of mine I'd rather not deal with that.
|
||||
|
||||
If you wish to comment on my blog posts, you are welcome to do so in the replies of the social media posts in which I link them.
|
||||
|
||||
## Why do you put "AI" in quotes?
|
||||
|
||||
Because the marketing departments of companies like OpenAI made a mockery of the term, so I refuse to call it that in earnest.
|
||||
|
||||
What these companies call "AI" is actually Machine Learning (ML) because the algorithms have to be trained and lack sufficient reasoning behind the output they produce. It *might* be right sometimes but it's still nothing more than an algorithm that strings words together based on statistical probabilities. It does not and cannot understand what the meaning of its output is, because it does not have any concept of what we consider truth.
|
||||
|
||||
What is peddled as "AI" these days is nothing more than a stochastic parrot regurgitating the vile shit that comes out of cesspools such as Reddit ruining the internet as a communication platform by flooding it with a torrent of slop. It exacerbates the issue we've dealt with before with content farms and Big Tech firms such as Meta, Google, Microsoft, Amazon and Apple are complicit in perpetuating this in an attempt to sell us on a future that will never come.
|
||||
|
||||
I refuse to pay respect to an over-hyped grift that destroys our environment, makes use of the same bullshit playbook crypto bros used to pump & dump a ponzy-scheme, and is generally pretty useless and gets things wrong a third-grader could get right no issue.
|
38
src/feed.njk
Normal file
38
src/feed.njk
Normal file
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
permalink: feed.xml
|
||||
eleventyExcludeFromCollections: true
|
||||
metadata:
|
||||
title: Sebin's Blog
|
||||
description: Writing about stuff I have vague interests in and commenting on stuff I read.
|
||||
language: en
|
||||
base: 'https://blog.sebin-nyshkim.net/'
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ metadata.language or page.lang }}">
|
||||
<title>{{ metadata.title }}</title>
|
||||
<subtitle>{{ metadata.description }}</subtitle>
|
||||
<link href="{{ permalink | htmlBaseUrl(metadata.base) }}" rel="self"/>
|
||||
<link href="{{ metadata.base | addPathPrefixToFullUrl }}"/>
|
||||
<updated>{{ collections.posts | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
|
||||
<id>{{ metadata.base | addPathPrefixToFullUrl }}</id>
|
||||
{%- for post in collections.posts | reverse %}
|
||||
{%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
|
||||
<entry>
|
||||
<author>
|
||||
<name>{{ post.data.author.name }}</name>
|
||||
<uri>{{ post.data.author.href }}</uri>
|
||||
</author>
|
||||
<title>{{ post.data.title }}</title>
|
||||
<link href="{{ absolutePostUrl }}"/>
|
||||
<updated>{{ post.date | dateToRfc3339 }}</updated>
|
||||
<id>{{ absolutePostUrl }}</id>
|
||||
{%- for tag in post.data.tags %}
|
||||
<category term="{{ tag }}" />
|
||||
{%- endfor %}
|
||||
<content type="html">
|
||||
<img src="{{ post.data.image.src }}.webp?width=1200" alt="{{ post.data.image.alt }}">
|
||||
{{ post.content | renderTransforms(post.data.page, metadata.base) }}
|
||||
</content>
|
||||
</entry>
|
||||
{%- endfor %}
|
||||
</feed>
|
Binary file not shown.
Before Width: | Height: | Size: 172 KiB |
Binary file not shown.
Before Width: | Height: | Size: 157 KiB |
Binary file not shown.
Before Width: | Height: | Size: 752 KiB |
20
src/includes/postmeta.macro.njk
Normal file
20
src/includes/postmeta.macro.njk
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% macro postmeta(params) %}
|
||||
<ul
|
||||
class="flex flex-wrap items-center {% if params.center %} justify-center {% else %} justify-start {% endif %} gap-4 text-sm *:inline-flex *:items-center *:gap-1 *:*:stroke-sky-600 md:text-base xl:text-lg 2xl:text-xl"
|
||||
role="group"
|
||||
aria-label="Published on {{ params.date | readableDate }}, written by {{ params.author.name }}, approximate reading time {{ params.content | readingtime }}"
|
||||
>
|
||||
<li aria-label="Published on">
|
||||
{% lucide "calendar", { "class": "size-4 md:size-5 xl:size-6" } %}
|
||||
<time datetime="{{ params.date | isoDate }}">{{ params.date | readableDate }}</time>
|
||||
</li>
|
||||
<li aria-label="Written by">
|
||||
{% lucide "user", { "class": "size-4 md:size-5 xl:size-6" } %}
|
||||
<a href="{{ params.author.href }}">{{ params.author.name }}</a>
|
||||
</li>
|
||||
<li aria-label="Approximate reading time">
|
||||
{% lucide "glasses", { "class": "size-4 md:size-5 xl:size-6" } %}
|
||||
{{ params.content | readingtime }}
|
||||
</li>
|
||||
</ul>
|
||||
{% endmacro %}
|
11
src/index.md
11
src/index.md
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
title: Home
|
||||
layout: page.njk
|
||||
eleventyNavigation:
|
||||
key: home
|
||||
|
@ -7,12 +6,10 @@ eleventyNavigation:
|
|||
|
||||
# Rawr there! 👋🏻
|
||||
|
||||

|
||||

|
||||
|
||||
This is where I pour out my thoughts whenever I have them :3
|
||||
Welcome to my little corner of the internet! This is where I pour out my thoughts whenever I have them :3
|
||||
|
||||
Don't expect too frequent posts, though, as I tend to lack inspiration for long form, in-depth content. That's only when obsession strikes x)
|
||||
Here you'll find posts about tech, sometimes tutorials, and long-form commentary on contemporary happenings.
|
||||
|
||||
I'll probably rather link a few articles that have inspired me and made me think, and maybe add a few supplementary comments.
|
||||
|
||||
So if that's kind of your thing, I'd be happy to have you around! Subscribe to the RSS feed in your favorite feed reader app to get my ramblings delivered hot off the press!
|
||||
So if that's kind of your thing, I'd be happy to have you around!
|
||||
|
|
27
src/js/ackee.js
Normal file
27
src/js/ackee.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const ackeeBanner = document.querySelector('#ackee-banner');
|
||||
const yesBtn = ackeeBanner.querySelector('#yes');
|
||||
const noBtn = ackeeBanner.querySelector('#no');
|
||||
const confirmKey = 'ackeeDetailed';
|
||||
|
||||
const ackeeServer = 'https://ackee.sebin-nyshkim.net';
|
||||
const ackeeDomainId = 'fc6deee5-c700-4c8a-87cd-421b673a33aa';
|
||||
const ackeeOpts = { detailed: true };
|
||||
|
||||
const record = (server, domainId, options) => {
|
||||
const instance = ackeeTracker.create(server, options);
|
||||
instance.record(domainId);
|
||||
};
|
||||
|
||||
yesBtn.addEventListener('click', () => {
|
||||
localStorage.setItem(confirmKey, true);
|
||||
record(ackeeServer, ackeeDomainId, ackeeOpts);
|
||||
});
|
||||
noBtn.addEventListener('click', () => localStorage.setItem(confirmKey, false));
|
||||
|
||||
if (localStorage.getItem(confirmKey) === null) {
|
||||
ackeeBanner.show();
|
||||
}
|
||||
|
||||
if (localStorage.getItem(confirmKey) === 'true') {
|
||||
record(ackeeServer, ackeeDomainId, ackeeOpts);
|
||||
}
|
|
@ -2,41 +2,56 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
{% metagen
|
||||
title=title + ' - Sebin\'s Blog',
|
||||
desc=description,
|
||||
url=url + page.url,
|
||||
twitter_card_type=twitter.cardType,
|
||||
twitter_handle=twitter.account,
|
||||
name=author.name,
|
||||
generator='eleventy'
|
||||
title = page_title,
|
||||
desc = page.excerpt | toPlain,
|
||||
url = 'https://blog.sebin-nyshkim.net' + page.url,
|
||||
type = type,
|
||||
site_name = site_name,
|
||||
og_title = og_title,
|
||||
og_image_width = image.width,
|
||||
og_image_height = image.height,
|
||||
og_image_alt = image.alt,
|
||||
og_image_type = 'image/webp',
|
||||
twitter_title = og_title,
|
||||
twitter_card_type = twitter.cardType,
|
||||
name = author.name,
|
||||
generator = eleventy.generator,
|
||||
preconnect = ['https://img.sebin-nyshkim.net'],
|
||||
dns_prefetch = ['https://img.sebin-nyshkim.net'],
|
||||
css = ['/fonts/tilt-warp/tilt-warp.css:rel="preload":as="style"',
|
||||
'/fonts/tilt-warp/tilt-warp.css',
|
||||
'/fonts/encode-sans/encode-sans.css:rel="preload":as="style"',
|
||||
'/fonts/encode-sans/encode-sans.css',
|
||||
'/fonts/m-plus-1-code/m-plus-1-code.css:rel="preload":as="style"',
|
||||
'/fonts/m-plus-1-code/m-plus-1-code.css',
|
||||
'/css/style.css:rel="preload":as="style"',
|
||||
'/css/style.css',
|
||||
'/css/prism.css:rel="preload":as="style"',
|
||||
'/css/prism.css']
|
||||
%}
|
||||
{% ogImage "og-image.og.njk", { title: title } %}
|
||||
{% if mastodon.fediverseCreator %}
|
||||
{% ogImage "og-image.og.njk", { title: title or site_name, author: author, image: image } %}
|
||||
{% if mastodon.fediverseCreator and mastodon.fediverseCreator != '' %}
|
||||
<meta name="fediverse:creator" content="{{ mastodon.fediverseCreator }}" />
|
||||
{% endif %}
|
||||
<link rel="alternate" href="/feed.xml" type="application/atom+xml">
|
||||
<link rel="stylesheet" href="{{ '/fonts/tilt-warp/tilt-warp.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ '/fonts/encode-sans/encode-sans.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ '/fonts/m-plus-1-code/m-plus-1-code.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ '/css/style.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ '/css/prism.css' | url }}">
|
||||
<link rel="alternate" href="/feed.xml" title="Sebin's Blog" type="application/atom+xml">
|
||||
<link rel="shortcut icon" href="https://img.sebin-nyshkim.net/i/b6629b72-ab77-4a6c-bf97-b1a615cc2454.png" type="image/png">
|
||||
<link rel="me" href="https://meow.social/@SebinNyshkim">
|
||||
</head>
|
||||
<body class="h-dvh bg-slate-300 text-slate-700 dark:bg-slate-900 dark:text-slate-300">
|
||||
<header>
|
||||
<div class="mx-auto max-w-screen-xl sm:px-safe-offset-4 md:px-safe-offset-6">
|
||||
<nav class="eleventy-navigation flex min-h-16 items-center justify-center bg-sky-600 shadow-xl sm:m-4 sm:mx-auto sm:justify-between sm:rounded-xl dark:bg-sky-950">
|
||||
<div class="hidden sm:flex sm:items-center">
|
||||
<img src="/img/sebin.png" alt="it me" class="m-4 max-w-14 rounded-full border-4 shadow-2xl">
|
||||
<h1 class="text-3xl text-white">Sebin's Blog</h1>
|
||||
</div>
|
||||
<body class="bg-slate-300 text-slate-700 dark:bg-slate-900 dark:text-slate-300">
|
||||
<header class="absolute z-10 left-0 right-0 top-0 sm:px-safe-offset-4 md:px-safe-offset-6">
|
||||
<div class="flex min-h-16 max-w-(--breakpoint-xl) items-center justify-center bg-sky-600 shadow-xl sm:m-4 sm:mx-auto sm:justify-between sm:rounded-xl dark:bg-sky-950">
|
||||
<div class="hidden sm:flex sm:items-center">
|
||||
<img src="https://img.sebin-nyshkim.net/i/b6629b72-ab77-4a6c-bf97-b1a615cc2454" alt="" class="m-4 max-w-12 rounded-full border-4 border-white shadow-2xl lg:m-5 lg:max-w-14">
|
||||
<h1 class="text-2xl text-white lg:text-3xl">{{ site_name }}</h1>
|
||||
</div>
|
||||
<nav class="eleventy-navigation" aria-label="Main">
|
||||
{{
|
||||
collections.all |
|
||||
eleventyNavigation |
|
||||
eleventyNavigationToHtml({
|
||||
listClass: "nav-list",
|
||||
anchorClass: "nav-link",
|
||||
activeAnchorClass: "active",
|
||||
listClass: "mx-3 flex gap-2 md:mx-6 md:gap-3",
|
||||
anchorClass: "rounded-xl px-4 py-2 capitalize text-white transition-all duration-300 hover:bg-sky-900 hover:shadow-lg sm:text-lg md:m-0 md:max-h-12 lg:text-xl dark:hover:bg-sky-800",
|
||||
activeAnchorClass: "bg-sky-900 shadow-lg dark:bg-sky-800",
|
||||
activeKey: eleventyNavigation.key or page.url.split('/')[1]
|
||||
}) |
|
||||
safe
|
||||
|
@ -45,25 +60,82 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mb-20 space-y-10 pt-10 *:px-safe-offset-4 sm:mb-32 sm:pt-16 md:mb-40 md:pt-20 md:*:px-safe-offset-6">
|
||||
<main class="mb-12 space-y-12 md:mb-14 md:space-y-14 lg:mb-16 lg:space-y-16">
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
|
||||
<footer class="pb-16">
|
||||
<div class="mx-auto max-w-screen-xl divide-y divide-slate-400 px-safe-offset-4 md:px-safe-offset-6 dark:divide-slate-600">
|
||||
<footer class="pb-12 md:pb-14 lg:pb-16">
|
||||
<div class="mx-auto max-w-(--breakpoint-xl) divide-y divide-slate-400 px-safe-offset-4 md:px-safe-offset-6 dark:divide-slate-600">
|
||||
<div class="flex flex-wrap gap-6 md:flex-nowrap md:gap-0"></div>
|
||||
<div class="mt-16 flex flex-wrap justify-between gap-4 pt-10 sm:flex-nowrap">
|
||||
<div class="basis-full">
|
||||
<div class="mt-12 flex flex-wrap justify-between gap-4 sm:flex-nowrap md:mt-14 lg:mt-16">
|
||||
<div class="basis-full space-y-4">
|
||||
<p>© {% year %} Sebin Nyshkim</p>
|
||||
<p>Content licensed under
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="basis-full sm:text-right">
|
||||
<p>Made with <a href="https://11ty.dev">11ty</a></p>
|
||||
<div class="basis-full space-y-4 sm:text-right">
|
||||
<p>Made with ❤️ and <a href="https://11ty.dev">11ty</a></p>
|
||||
<ul class="flex justify-start gap-4 sm:justify-end" aria-label="Connect">
|
||||
<li>
|
||||
<a
|
||||
class="fill-slate-700 hover:fill-[#0285FF] dark:fill-slate-300"
|
||||
href="https://bsky.app/profile/sebin-nyshkim.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Bluesky"
|
||||
>
|
||||
{% icon "simple:bluesky", { width: 24, height: 24 } %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="fill-slate-700 hover:fill-[#6364FF] dark:fill-slate-300"
|
||||
href="https://meow.social/@SebinNyshkim"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Mastodon"
|
||||
>
|
||||
{% icon "simple:mastodon", { width: 24, height: 24 } %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="fill-slate-700 hover:fill-[#FFA500] dark:fill-slate-300"
|
||||
href="https://blog.sebin-nyshkim.net/feed.xml"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="RSS Feed"
|
||||
>
|
||||
{% icon "simple:rss", { width: 24, height: 24 } %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<dialog id="ackee-banner" class="fixed start-4 end-4 top-auto ms-auto bottom-4 max-w-128 rounded-xl bg-sky-600 text-white shadow-xl dark:bg-sky-950">
|
||||
<form method="dialog" class="flex flex-col">
|
||||
<div class="space-y-4 p-4 text-center">
|
||||
<p class="text-xl font-bold">Analytics</p>
|
||||
<p>
|
||||
May I collect some anonymized data about the device you use to view this site? I won't know who you are. See:
|
||||
<a href="/privacy" class="!decoration-white hover:!text-white dark:!decoration-sky-600 dark:hover:!text-sky-600">Privacy Policy</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row *:first:rounded-es-xl *:last:rounded-ee-xl *:first:border-e">
|
||||
<button id="yes" class="flex-1/2 grow bg-sky-500 px-4 py-2 hover:bg-sky-800 dark:bg-sky-800 dark:hover:bg-sky-500">Yeah sure</button>
|
||||
<button id="no" class="flex-1/2 grow bg-sky-500 px-4 py-2 hover:bg-sky-800 dark:bg-sky-800 dark:hover:bg-sky-500">Nope</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="https://ackee.sebin-nyshkim.net/tracker.js"></script>
|
||||
<script src="/js/ackee.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,48 +2,48 @@
|
|||
layout: base.njk
|
||||
---
|
||||
|
||||
<div class="mx-auto grid max-w-screen-xl grid-flow-col md:grid-cols-content gap-8">
|
||||
{% if toc %}
|
||||
<aside>
|
||||
{{ content | toc | safe }}
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
<article class="blogpost justify-items-center">
|
||||
<h1 class="text-4xl">{{ title }}</h1>
|
||||
<aside class="not-prose space-y-4 text-base">
|
||||
<ul class="postmeta">
|
||||
<li>
|
||||
{% lucide "calendar", {"size": "20"} %}
|
||||
<time datetime="{{ page.date | isoDate }}" title="{{ page.date | longDate }}">
|
||||
{{ page.date | readableDate }}
|
||||
</time>
|
||||
</li>
|
||||
<li>
|
||||
{% lucide "user", {"size": "20"} %}
|
||||
<a href="{{ author.href }}">{{ author.name }}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% lucide "glasses", {"size": "20"} %}
|
||||
{{ content | readingtime }}
|
||||
</li>
|
||||
</ul>
|
||||
<article>
|
||||
<header
|
||||
{% if image and image.src != '' %}
|
||||
style="background-image: image-set({% bgimgset image.src %})"
|
||||
{% endif %}
|
||||
{% if image and image.credit != '' %}
|
||||
data-credit="{{ image.credit }}"
|
||||
{% endif %}
|
||||
class="relative mb-8 grid min-h-96 grid-flow-row grid-rows-article-header place-items-center gap-6 bg-cover bg-center pt-20 px-safe-offset-8 pb-8 *:z-10 before:absolute before:inset-0 before:bg-slate-300/65 after:absolute after:right-0 after:bottom-0 after:z-10 after:rounded-tl-md after:bg-black after:px-2 after:py-1 after:text-xs after:text-white after:content-[attr(data-credit)] sm:min-h-128 sm:pt-28 md:pt-32 md:after:text-sm lg:mb-12 lg:min-h-160 lg:px-safe-offset-12 lg:pb-12 2xl:min-h-192 dark:before:bg-slate-900/65"
|
||||
>
|
||||
<div class="mx-auto prose prose-slate md:prose-lg lg:prose-xl 2xl:prose-2xl dark:prose-invert prose-headings:font-normal prose-h1:m-0">
|
||||
<h1 class="text-balance text-center">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="not-prose space-y-6 self-end text-sm md:text-base xl:text-lg 2xl:text-xl">
|
||||
{%- from 'postmeta.macro.njk' import postmeta %}
|
||||
{{ postmeta({ date: page.date, author: author, content: content, center: true }) }}
|
||||
{% if tags.length > 0 %}
|
||||
<ul class="tags">
|
||||
<ul class="flex flex-wrap justify-center gap-1" aria-label="Tagged in">
|
||||
{% for tag in tags %}
|
||||
<li class="tag">
|
||||
{% lucide "tag", {"size": "20"} %}
|
||||
<li class="inline-flex items-center gap-1 rounded-md border border-solid border-sky-500 bg-sky-200 px-1.5 py-0.5 text-sky-500 dark:bg-sky-950">
|
||||
{% lucide "tag", { "class": "size-4 md:size-5 xl:size-6" } %}
|
||||
{{ tag }}
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
{% if image and image.src != '' %}
|
||||
<img src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mx-auto prose px-safe-offset-4 prose-slate md:prose-lg md:px-safe-offset-6 lg:prose-xl 2xl:prose-2xl dark:prose-invert prose-headings:font-normal prose-p:text-justify prose-figcaption:text-center prose-figcaption:text-balance prose-strong:font-bold prose-strong:text-inherit prose-li:marker:text-inherit! prose-th:font-bold prose-img:mx-auto prose-img:rounded-3xl prose-hr:my-12 prose-hr:border-slate-400 md:prose-hr:my-14 lg:prose-hr:my-16 dark:prose-hr:border-slate-600">
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="text-center! text-balance">If you enjoy my writing and would like to show your appreciation, I set up a Ko-fi where you can donate a couple bucks to say "thanks!"</p>
|
||||
|
||||
<a href='https://ko-fi.com/I2I333GJJ' target='_blank'>
|
||||
<img
|
||||
src='https://storage.ko-fi.com/cdn/kofi5.png?v=6'
|
||||
alt='Buy Me a Coffee at ko-fi.com'
|
||||
class="h-12 w-auto rounded-none!"
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
layout: base.njk
|
||||
---
|
||||
|
||||
<section class="blogpost">
|
||||
<section class="prose prose-slate mx-auto pt-28 px-safe-offset-4 md:prose-lg lg:prose-xl 2xl:prose-2xl dark:prose-invert prose-headings:font-normal prose-strong:text-inherit prose-li:marker:text-inherit! prose-th:font-bold prose-img:rounded-3xl sm:pt-52 md:px-safe-offset-6">
|
||||
{{ content | safe }}
|
||||
</section>
|
||||
|
|
|
@ -2,34 +2,34 @@
|
|||
layout: base.njk
|
||||
---
|
||||
|
||||
<section class="blogpost">
|
||||
<section class="prose prose-slate mx-auto pt-28 px-safe-offset-4 md:prose-lg lg:prose-xl 2xl:prose-2xl dark:prose-invert prose-headings:font-normal prose-strong:text-inherit prose-li:marker:text-inherit! prose-th:font-bold prose-img:rounded-3xl sm:pt-52 md:px-safe-offset-6">
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<p>Everything I have written, from newest to oldest.</p>
|
||||
<p>Everything I have written, from newest to oldest. Also available as <a href="/feed.xml">RSS feed</a>. Get my ramblings delivered directly to your favorite news reader app!</p>
|
||||
</section>
|
||||
|
||||
{{ content | safe }}
|
||||
|
||||
{% if pagination.pages.length > 1 %}
|
||||
<nav>
|
||||
<ol class="pagination">
|
||||
<nav aria-label="Pagination">
|
||||
<ol class="flex justify-center gap-2">
|
||||
<li>
|
||||
{% if page.url != pagination.href.first %}
|
||||
<a href="{{ pagination.href.first }}">{% lucide "chevron-first" %}</a>
|
||||
<a class="flex size-10 items-center justify-center rounded-full bg-slate-100 hover:bg-sky-600 hover:text-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700" href="{{ pagination.href.first }}">{% lucide "chevron-first" %}</a>
|
||||
{% else %}
|
||||
<span>{% lucide "chevron-first" %}</span>
|
||||
<span class="flex size-10 cursor-not-allowed items-center justify-center rounded-full bg-slate-400 text-slate-300 dark:bg-slate-950 dark:text-slate-700" aria-disabled="true">{% lucide "chevron-first" %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if pagination.href.previous %}
|
||||
<a href="{{ pagination.href.previous }}">{% lucide "chevron-left" %}</a>
|
||||
<a class="flex size-10 items-center justify-center rounded-full bg-slate-100 hover:bg-sky-600 hover:text-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700" href="{{ pagination.href.previous }}">{% lucide "chevron-left" %}</a>
|
||||
{% else %}
|
||||
<span>{% lucide "chevron-left" %}</span>
|
||||
<span class="flex size-10 cursor-not-allowed items-center justify-center rounded-full bg-slate-400 text-slate-300 dark:bg-slate-950 dark:text-slate-700" aria-disabled="true">{% lucide "chevron-left" %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{%- for pageEntry in pagination.pages %}
|
||||
<li>
|
||||
<a
|
||||
<a class="flex size-10 items-center justify-center rounded-full bg-slate-100 hover:bg-sky-600 hover:text-slate-300 aria-[current]:bg-sky-600 aria-[current]:text-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 dark:aria-[current]:bg-slate-700"
|
||||
href="{{ pagination.hrefs[ loop.index0 ] }}"
|
||||
{% if page.url == pagination.hrefs[ loop.index0 ] %}
|
||||
aria-current="page"
|
||||
|
@ -41,16 +41,16 @@ layout: base.njk
|
|||
{%- endfor %}
|
||||
<li>
|
||||
{% if pagination.href.next %}
|
||||
<a href="{{ pagination.href.next }}">{% lucide "chevron-right" %}</a>
|
||||
<a class="flex size-10 items-center justify-center rounded-full bg-slate-100 hover:bg-sky-600 hover:text-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700" href="{{ pagination.href.next }}">{% lucide "chevron-right" %}</a>
|
||||
{% else %}
|
||||
<span>{% lucide "chevron-right" %}</span>
|
||||
<span class="flex size-10 cursor-not-allowed items-center justify-center rounded-full bg-slate-400 text-slate-300 dark:bg-slate-950 dark:text-slate-700" aria-disabled="true">{% lucide "chevron-right" %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if page.url != pagination.href.last %}
|
||||
<a href="{{ pagination.href.last }}">{% lucide "chevron-last" %}</a>
|
||||
<a class="flex size-10 items-center justify-center rounded-full bg-slate-100 hover:bg-sky-600 hover:text-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700" href="{{ pagination.href.last }}">{% lucide "chevron-last" %}</a>
|
||||
{% else %}
|
||||
<span>{% lucide "chevron-last" %}</span>
|
||||
<span class="flex size-10 cursor-not-allowed items-center justify-center rounded-full bg-slate-400 text-slate-300 dark:bg-slate-950 dark:text-slate-700" aria-disabled="true">{% lucide "chevron-last" %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
|
|
|
@ -1,27 +1,98 @@
|
|||
<div
|
||||
style="
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding: 64px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 64px;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
">
|
||||
<div style="display: flex; flex: 0 0 256px">
|
||||
<img
|
||||
src="https://ref.sebin-nyshkim.net/sebin/assets/sebin-smug-icon-C3NF7g4H.png"
|
||||
width="256"
|
||||
height="256"
|
||||
alt="Sebin"
|
||||
style="border-radius: 100%; border: 8px solid currentColor" />
|
||||
</div>
|
||||
<style>
|
||||
#top {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
<div style="flex: 1 1 0; display: flex; flex-flow: column nowrap">
|
||||
<h1 style="font-size: 72px">{{ title | safe }}</h1>
|
||||
<h2 style="margin-top: 64px; font-size: 30px; line-height: 36px">blog.sebin-nyshkim.net</h2>
|
||||
#main {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#heading {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex: 1 0 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
padding: 64px 112px;
|
||||
font-size: 72px;
|
||||
text-wrap: balance;
|
||||
background-color: rgb(15 23 42 / 0.65);
|
||||
}
|
||||
|
||||
#footer {
|
||||
flex: 1 0 96px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
background-color: rgb(8 47 73);
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer #avatar {
|
||||
flex: 0 0 64px;
|
||||
display: flex;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: 100% 100%;
|
||||
border: 4px solid currentColor;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#footer #meta {
|
||||
flex: 1 0 0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="top">
|
||||
<div id="main">
|
||||
<div id="heading">
|
||||
{% if image and image.src != '' %}
|
||||
<img id="background" src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
{% endif %}
|
||||
<h1>{{ title | safe }}</h1>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<div id="avatar" style="{% if author and author.image != '' %} background-image: url({{ author.image }}){% endif %}"></div>
|
||||
<div id="meta">
|
||||
<p id="author">by {{ author.name }}</p>
|
||||
<p id="blog">blog.sebin-nyshkim.net</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,35 +12,28 @@ pagination:
|
|||
---
|
||||
|
||||
{% for post in blogposts %}
|
||||
<article class="blogpost">
|
||||
<h1>{{ post.data.title }}</h1>
|
||||
<aside class="not-prose space-y-4 text-base">
|
||||
<ul class="postmeta">
|
||||
<li>
|
||||
{% lucide "calendar", {"size": "20"} %}
|
||||
<time datetime="{{ post.date | isoDate }}" title="{{ post.date | longDate }}">
|
||||
{{ post.date | readableDate }}
|
||||
</time>
|
||||
</li>
|
||||
<li>
|
||||
{% lucide "user", {"size": "20"} %}
|
||||
<a href="{{ post.data.author.href }}">{{ post.data.author.name }}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% lucide "glasses", {"size": "20"} %}
|
||||
{{ post.content | readingtime }}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<article class="mx-auto prose px-safe-offset-4 prose-slate md:prose-lg md:px-safe-offset-6 lg:prose-xl 2xl:prose-2xl dark:prose-invert prose-headings:my-4 prose-headings:font-normal prose-headings:duration-300 prose-headings:hover:text-inherit lg:prose-headings:my-6 prose-a:no-underline prose-a:hover:text-sky-600 prose-figure:my-4 lg:prose-figure:my-6 prose-strong:text-inherit prose-li:marker:text-inherit! prose-th:font-bold prose-img:rounded-3xl">
|
||||
<a href="{{ post.url }}">
|
||||
<h1>{{ post.data.title }}</h1>
|
||||
</a>
|
||||
<div class="not-prose">
|
||||
{%- from 'postmeta.macro.njk' import postmeta %}
|
||||
{{ postmeta({ date: post.date, author: post.data.author, content: post.content, center: false }) }}
|
||||
</div>
|
||||
|
||||
{% if post.data.image and post.data.image.src != '' %}
|
||||
<img src="{{ post.data.image.src }}" alt="{{ post.data.image.alt }}">
|
||||
<a href="{{ post.url }}">
|
||||
<figure class="relative">
|
||||
<img src="{{ post.data.image.src }}" alt="{{ post.data.image.alt }}">
|
||||
{% if post.data.image and post.data.image.credit != '' %}
|
||||
<figcaption class="absolute bottom-0 right-0 rounded-tl-md rounded-br-3xl bg-black px-2 py-1 pe-4 text-xs text-white md:text-sm">
|
||||
{{ post.data.image.credit }}
|
||||
</figcaption>
|
||||
{% endif %}
|
||||
</figure>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ post.data.description }}</p>
|
||||
|
||||
<a href="{{ post.url }}" class="not-prose read-more-button">
|
||||
Read on {% lucide "chevron-right", {"size":"20"} %}
|
||||
</a>
|
||||
{{ post.data.page.excerpt | safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
|
685
src/posts/2024-10-19_building-a-blog-with-eleventy.md
Normal file
685
src/posts/2024-10-19_building-a-blog-with-eleventy.md
Normal file
|
@ -0,0 +1,685 @@
|
|||
---
|
||||
title: Building a Blog with Eleventy (blind, any%)
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/3163c7b0-4657-40bb-b08a-462a06d1fdd5
|
||||
alt: Close-up of SVG code on a computer screen
|
||||
credit: Photo by Paico Oficial on Unsplash
|
||||
tags: ["coding", "eleventy"]
|
||||
---
|
||||
|
||||
I recently felt like getting back into blogging. But setting up and maintaining WordPress felt like more than I was looking for. I was looking for something much simpler. Preferably file-based and with Markdown support. That was my introduction to Eleventy.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
> [!info] Update 2025-02-17
|
||||
> Updated Tailwind instructions for Tailwind v4
|
||||
|
||||
Starting an Eleventy project is fairly straight-forward if you've ever worked with any Node.js package:
|
||||
|
||||
1. Create a project folder
|
||||
1. Initialize a `package.json`: `npm init -y`
|
||||
1. Install the package from npm: `npm install @11ty/eleventy`
|
||||
1. Create a content file in [one of the supported template languages](https://www.11ty.dev/docs/languages/)
|
||||
1. Run `npx @11ty/eleventy` to let Eleventy work its magic
|
||||
|
||||
An `eleventy.config.js` file is entirely optional, but will become necessary for more complex setups down the line. But as a starter, it doesn't get any easier than this. No config, no build steps to set up, it just works(tm)!
|
||||
|
||||
So with that in mind, let's go through the steps of learning how to go from the basics to a blog.
|
||||
|
||||
## Setting up `package.json`
|
||||
|
||||
It's probably a good idea to add at least two entries to the `scripts` section of `package.json`. One is for starting the local development environment, to view the site live in a web browser and have it reload automatically after hitting save in your code editor. The other is for building a production ready build to put on a webserver.
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"start": "eleventy --serve",
|
||||
"build": "eleventy"
|
||||
}
|
||||
```
|
||||
|
||||
If you're using a code editor that has support for NPM built in (e.g. Visual Studio Code), you can easily start any entry with a simple click and get helpful debug output of what Eleventy is doing.
|
||||
|
||||
## Content
|
||||
|
||||
I love Markdown as a document format! ❤️
|
||||
|
||||
Its syntax makes it easy to produce well-formatted documents, and it remains readable even when viewed in plain text. It has widespread support across the web and in development communities, so it was a no-brainer for me to want to write my blog posts in Markdown. Eleventy supports it as a content templating language. It's like a match made in heaven!
|
||||
|
||||
Eleventy uses file names as basis for routing. So a file called `index.md` at the project root will become the `index.html` in the web root of the site. If you name the file something else, say `about.md`, Eleventy will output it as `about/index.html`. By default, most web servers are looking for an `index.html` file when a web browser just requests a path without an explicit file name. That's how I can have readable URLs and not having to worry about routing at the application level too much. Awesome!
|
||||
|
||||
What I'm trying to achieve doesn't need a database, complex routing rules, or any big CMS in the backend. I just wanna put words on the internet and for that simple HTML is the best way to do that.
|
||||
|
||||
After all the building blocks around that are put in place, I can just focus on writing the words. There's many apps on desktop and mobile that have Markdown formatting built in that allow me to work on posts at home and on the go. One such example would be Nextcloud's Notes app that uses Markdown for formatting notes. I can use something like [Iotas](https://flathub.org/apps/org.gnome.World.Iotas) or [Apostrophe](https://apps.gnome.org/Apostrophe/) on my GNOME Linux desktop to compose drafts or jot down something real quick in the [Nextcloud Notes app for Android](https://play.google.com/store/apps/details?id=it.niedermann.owncloud.notes), push the completed post written in Markdown to the Git repo, re-build the site and deploy it.
|
||||
|
||||
It doesn't get any simpler than that!
|
||||
|
||||
## Layouts
|
||||
|
||||
Markdown, as a simple markup language, is not intended to create fully compliant HTML documents however. This is where Eleventy layouts come in. Layouts can be written in [any template language Eleventy supports](https://www.11ty.dev/docs/languages/). Nunjucks (`*.njk`) seems to be a pretty popular choice in the Eleventy community, so I went with that. There's many languages available, but not as well documented as either Nunjucks or Liquid (the latter of which I've never heard of and its documentation seems equally sparse…)
|
||||
|
||||
You can think of layouts as re-usable content wrappers. Which is great, since it spares me having to wrap all my Markdown files in the same boilerplate every time, which would be kind of a drag and detract from the whole putting my words on the internet with ease and such.
|
||||
|
||||
Eleventy by default looks for layouts in a sub-directory called `_includes`. The naming for layout files is arbitrary, so I just went with a `base.njk` layout that holds all the default boilerplate HTML structure that will be common to all pages on the site. Since I'm using Nunjucks as my templating language for layouts, it offers me some variables inside the HTML and this is what makes this a whole lot more dynamic.
|
||||
|
||||
```twig
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% raw %}{{ title }}{% endraw %}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% raw %}{{ content | safe }}{% endraw %}
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The `safe` in this case is what Eleventy calls a "filter". It's basically just a JavaScript function that gets the value of the thing in front of the pipe as its first argument and does stuff to it. In this case it prevents escaping of `content` because it is already valid HTML from transforming the Markdown contents, so double-escaping that would create garbage.
|
||||
|
||||
## Linking content and layouts
|
||||
|
||||
Creating a layout by itself does nothing yet, though. I have to somehow let Eleventy know that content and layout are somehow related. This is achieved by a block of YAML at the beginning of each file, which Eleventy calls *front matter*, where that information lives.
|
||||
|
||||
In this case, any blogpost I write has to have at least a reference to a layout in the beginning:
|
||||
|
||||
```md
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
# Look ma! A blog post!
|
||||
|
||||
Words words words words…
|
||||
```
|
||||
|
||||
The path is always relative to the `layout` path variable configured in Eleventy, which by default points to `_includes`.
|
||||
|
||||
This way {% raw %}`{{ content }}`{% endraw %} will receive the rendered output of that Markdown file.
|
||||
|
||||
There's still one problem, though: What if not all pages on your site look exactly the same? What if you want some to look different from others?
|
||||
|
||||
## Layout-ception
|
||||
|
||||
Eleventy doesn't really care what the {% raw %}`{{ content }}`{% endraw %} in a layout really is. It can even be another layout! This is what Eleventy calls "layout chaining": The rendered output of one layout becomes the content of another layout. This is what I'm doing with single blog posts (the one you're reading right now, in fact)! This allows for highly re-usable layouts and actually encourages modularity in your code structure.
|
||||
|
||||
Suppose the layout to be chained is called `blogpost.njk` and will receive all the, well, blog posts. It could look something like this:
|
||||
|
||||
```twig
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<article>
|
||||
{% raw %}{{ content | safe }}{% endraw %}
|
||||
</article>
|
||||
```
|
||||
|
||||
See what I did there? This is the whole thing! Now I just have to point every blog post to a different layout and I'm golden.
|
||||
|
||||
## Data files
|
||||
|
||||
"But Sebin," I hear you say, "writing all that stuff in the beginning of each post is tedious! What if you forget to do it?"
|
||||
|
||||
The authors of Eleventy probably thought the exact same, apparently. Introducing another Eleventy concept: **Data files!**
|
||||
|
||||
Data files are just `*.json` files which can be used to set some or all of the common *front matter* data for every file in a directory, e.g. if all my blog posts live in a sub-directory `posts` I can put a data file in there named the same as the directory (i.e. `posts/posts.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": "blogpost.njk"
|
||||
}
|
||||
```
|
||||
|
||||
This applies the `blogpost.njk` layout to every file. But we can take this even further and do a lot more to free up some space in the actual blog post Markdown files.
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": "blogpost.njk",
|
||||
"date": "Created",
|
||||
"permalink": "/posts/{% raw %}{{ title | slugify }}{% endraw %}/",
|
||||
"author": {
|
||||
"name": "Sebin Nyshkim",
|
||||
"href": "https://blog.sebin-nyshkim.net"
|
||||
},
|
||||
"twitter": {
|
||||
"account": "@SebinNyshkim"
|
||||
},
|
||||
"mastodon": {
|
||||
"fediverseCreator": "@SebinNyshkim@meow.social"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `date` tells Eleventy how to resolve the date associated with a blog post, in this instance the date of when the file was created[^eleventydates]. The `permalink` variable allows me control where a post will be saved to, with the `slugify` filter taking the value of the `title` variable and making it URL friendly. And as you can see, you can add anything you want.
|
||||
|
||||
[^eleventydates]: Specifically, the file system creation date. This can get a little tricky if you care about the accuracy of the post date, because if you were to create an empty file in the `posts` directory, Eleventy will take the exact date the file was written to disk, not the date the post was *actually published* to the site. You could get around that with `git Created` to instruct Eleventy to take the date the file was pushed to the Git repo instead, but this has [caveats](https://www.11ty.dev/docs/dates/).
|
||||
|
||||
## Data cascade
|
||||
|
||||
Of particular note in this context is what Eleventy calls the *data cascade*, meaning data set earlier in the cascade gets overridden by later data. In the earlier example this would mean that if a blog post were to re-assign the `layout` variable, it would look different from any other post, even if the `*.json` data file said something else. That is because the post always comes last in the cascade.
|
||||
|
||||
This opens up an interesting use-case, however. A friend asked me, if I'd allow guest articles on my blog and I told him I'd look into how to do that. I think this is a workable solution!
|
||||
|
||||
If I set myself as default author in the data file, all my friend would have to do is override the author information in the blog post Markdown file:
|
||||
|
||||
```md
|
||||
---
|
||||
title: How to have someone else write in your blog
|
||||
date: Created
|
||||
author:
|
||||
name: Not Sebin
|
||||
href: https://no-derg.zone
|
||||
twitter:
|
||||
account: "@FakeSebin42069"
|
||||
mastodon:
|
||||
fediverseCreator: "@FakeSebin@no-derg.zone"
|
||||
---
|
||||
|
||||
# There be no dragons here
|
||||
|
||||
This post brought to you by the *Anti-Sebin Gang*
|
||||
```
|
||||
|
||||
When using a more complex data structure like this, it's important to note that setting the top level object to something like `false` does not unset it. If I don't keep to the same data structure and set the individual properties to something *falsey* the data from earlier in the cascade will persist. Something worth keeping in mind!
|
||||
|
||||
## Styling
|
||||
|
||||
Until now the site has no styles, which doesn't look particularly pretty. Adding styles isn't very complicated either, though. It's just one line to add to the `base.njk` layout in the page's `<head>` section:
|
||||
|
||||
```twig
|
||||
<link rel="stylesheet" href="{% raw %}{{ '/css/style.css' | url }}{% endraw %}">
|
||||
```
|
||||
|
||||
The `url` filter on the `href` attribute makes sure the resulting path will be dynamic enough that if the site were to move in a sub-directory nothing breaks.
|
||||
|
||||
Eleventy does not process CSS files, however, so we need to tell it to do something with them.
|
||||
|
||||
## Configuring Eleventy
|
||||
|
||||
Up until this point a config was entirely optional, but since we're bound to do something more mildly complex with Eleventy down the line, we might as well just add it.
|
||||
|
||||
Eleventy's config file is named `eleventy.config.js` and the format should be immediately familiar to anybody having worked with Webpack, Vite, NextJS and the like[^eleventyconfig]:
|
||||
|
||||
[^eleventyconfig]: Eleventy supports both CommonJS and ESM syntax. I'm using ESM syntax because it's widely adopted in the Node.js development world and is supported by Eleventy since v3 (which just so happens to have released shortly before I started this blog, fancy that)
|
||||
|
||||
```js
|
||||
export default async function(eleventyConfig) {
|
||||
// Access to the full `eleventyConfig` Configuration API
|
||||
};
|
||||
```
|
||||
|
||||
Unlike Webpack for example, however, the config format is pretty straight-forward. You get the entire Eleventy Configuration API interface to play with; no entire config format to figure out.
|
||||
|
||||
With it, you can add watch targets, copy files to the output directory, add plugins, shortcodes, filters, collections and extend included libraries. This makes Eleventy extremely capable for a static site generator!
|
||||
|
||||
### Custom paths
|
||||
|
||||
The config is also where the default paths of source and output directories can be adjusted. I like my work environment clean and tidy, so I'm giving the things I work with clear distinct names and places where they go:
|
||||
|
||||
```js
|
||||
export default async function(eleventyConfig) {};
|
||||
|
||||
export const config = {
|
||||
dir: {
|
||||
input: 'src',
|
||||
output: 'public',
|
||||
layouts: 'layouts',
|
||||
includes: 'includes'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The Eleventy docs [recommend](https://www.11ty.dev/docs/config-shapes/) setting the paths outside of the callback function for order-of-operation reasons, which sounds like something I'd want to adhere to.
|
||||
|
||||
### Adding watch targets and pass-through copies
|
||||
|
||||
To have Eleventy watch for changes in files and copy them over during serve and build we have the `addWatchTarget()` and `addPassthroughCopy()` helper functions. Pointing these at directories copies the entire content of that directory:
|
||||
|
||||
```js
|
||||
export default async function(eleventyConfig) {
|
||||
eleventyConfig.addWatchTarget('./src/css/');
|
||||
eleventyConfig.addWatchTarget('./src/fonts/');
|
||||
eleventyConfig.addWatchTarget('./src/img/');
|
||||
|
||||
eleventyConfig.addPassthroughCopy('./src/css/');
|
||||
eleventyConfig.addPassthroughCopy('./src/fonts/');
|
||||
eleventyConfig.addPassthroughCopy('./src/img/');
|
||||
};
|
||||
```
|
||||
|
||||
Since Eleventy also uses `browsersync` it makes developing locally really comfortable, as the browser reloads the page automatically when I hit save in my code editor.
|
||||
|
||||
### Adding shortcodes
|
||||
|
||||
Shortcodes are a really neat feature allowing me to have small reusable snippets I can call from anywhere in my templates:
|
||||
|
||||
```js
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`);
|
||||
};
|
||||
```
|
||||
|
||||
Now I can just put {% raw %}`{% year %}`{% endraw %} anywhere and it will give me the current year for things like the copyright notice at the bottom of the page, it will update itself when the year changes.
|
||||
|
||||
### Adding filters
|
||||
|
||||
You've seen filters being used with the {% raw %}`{{ content | safe }}`{% endraw %} variable in content files. The config is also where I can declare a couple of filters myself for text transformations I need in multiple places, e.g. the date on the top of posts. I can declare it with the `addFilter()` helper method:
|
||||
|
||||
```js
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addFilter('toDateObj', (dateString) => new Date(dateString));
|
||||
eleventyConfig.addFilter('isoDate', (dateObj) => dateObj.toISOString());
|
||||
eleventyConfig.addFilter('longDate', (dateObj) => dateObj.toString());
|
||||
eleventyConfig.addFilter('readableDate', (dateObj) =>
|
||||
dateObj.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
With this, I can now do things like {% raw %}`{{ date | readableDate }}`{% endraw %} to format a JavaScript date object instance as a nicely readable string. These are also chainable, so doing something like {% raw %}`{{ '2024-10-13' | toDateObj | readableDate }}`{% endraw %} gives me *Oct 13, 2024* from a date string the `Date()` constructor accepts[^jsdateobject].
|
||||
|
||||
[^jsdateobject]: The core JavaScript `Date` object is perfectly capable of formatting date and time strings. A library such as `luxon`, like it's mentioned in the Eleventy docs, is better suited when needing to do more complex things with dates like calculating durations and intervals, e.g. I wanted the post date to say something like "3 days ago" or something.
|
||||
|
||||
### Adding collections
|
||||
|
||||
I like to have some tags on my posts to categorize them into topics. After all, that's what tags are for. One issue I encountered with the `tags` variable in *front matter*, however, is that it causes the tag "posts" to show up in the list of tags on every post, which feels kind of redundant. Of course it belongs to posts, that's the majority of the content on this site! Problem is, if I remove it from *front matter* data I lose the one mechanism that gives me all posts in a *collection* I can easily loop over for generating the post listing page. Bummer! 😖
|
||||
|
||||
That's when I discovered I can also define collections via the Configuration API in `eleventy.config.js`, which allows me to circumvent this issue. It's like I cat eat my cake and have it, too!
|
||||
|
||||
```js
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addCollection('posts', (collection) => collection.getFilteredByGlob('./src/posts/*.md'));
|
||||
};
|
||||
```
|
||||
|
||||
Adding collections via the config also has other benefits like being able to pre-filter and sort them. That way they're already the way they need to be at the time of consumption in templates and layouts[^jsarraymutate].
|
||||
|
||||
[^jsarraymutate]: In fact, the [docs](https://www.11ty.dev/docs/collections/#do-not-use-array-reverse()) specifically tell you to avoid calling array methods on collections that mutate arrays *in place* which can have undesirable side-effects and cause you a lot of headaches. So, yeah, don't do that!
|
||||
|
||||
## Turning it up to 11ty
|
||||
|
||||
At this stage the blog really started to come together. However, I still had a few requirements that were not covered by the basic package.
|
||||
|
||||
For one I really like having callouts in case I write up tutorials that could really benefit from some notes, warnings and such. Having footnotes also would be really nice, so side-thoughts don't clutter the main text. I thought it was covered by basic Markdown, but turns out it isn't or it's part of some flavor of Markdown that Eleventy doesn't do. Syntax highlighting in code blocks are also really good for code snippet readability!
|
||||
|
||||
Also, since this is a blog after all, it would really benefit from having an RSS feed people can subscribe to. And even though I'll try not to go too crazy on images, whenever I do decide to add some to a post, it should be light on the bandwidth, so an image processing pipeline would be pretty dope!
|
||||
|
||||
I found all of that possible with the help of plugins for Eleventy itself and the extensible parsers it uses!
|
||||
|
||||
### Extending Eleventy
|
||||
|
||||
Although Eleventy is still relatively new (v1 was released in January 2022), there's already quite a few plugins from [its creators](https://www.11ty.dev/docs/plugins/official/) and [the community](https://www.11ty.dev/docs/plugins/community/).
|
||||
|
||||
In addition to that, it also uses parsers which are themselves very extensible. For example, rendering Markdown is done via `markdown-it` and there exist [a plethora of plugins](https://www.npmjs.com/search?q=keywords:markdown-it-plugin) to extend the Markdown syntax available to me.
|
||||
|
||||
The handy-dandy `addPlugin()` helper method adds plugins to Eleventy:
|
||||
|
||||
```js
|
||||
import { eleventyImageTransformPlugin } from '@11ty/eleventy-img';
|
||||
import { feedPlugin } from '@11ty/eleventy-plugin-rss';
|
||||
import eleventyPluginLucideIcons from '@grimlink/eleventy-plugin-lucide-icons';
|
||||
import eleventyPluginNavigation from '@11ty/eleventy-navigation';
|
||||
import eleventyPluginSyntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(eleventyPluginLucideIcons);
|
||||
eleventyConfig.addPlugin(eleventyPluginNavigation);
|
||||
eleventyConfig.addPlugin(eleventyPluginSyntaxHighlight);
|
||||
|
||||
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
|
||||
// Plugin options go here
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(feedPlugin, {
|
||||
// Plugin options go here
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Most plugins for Eleventy still use CommonJS in their examples, but they're still easily added by treating them like default exports using regular ESM `import` statements. Only some plugins have named exports. Some also allow for configuration, like the image transform plugin, which accepts some parameters for image sizes, formats and default attributes on `<img>` tags. The RSS feed plugin has some required options that must be set for it to work, like the type of feed, output file name, number of entries, which collection to include, etc. The plugin's documentation will likely tell if it's configurable and which options are required and which are optional.
|
||||
|
||||
### Adding navigation
|
||||
|
||||
Eleventy offers a 1st party navigation plugin. It operates on collections to generate the navigation in templates via filters. The most simple way to add navigation to a site is:
|
||||
|
||||
```twig
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>{% raw %}{{ collections.all | eleventyNavigation | eleventyNavigationToHtml | safe }}{% endraw %}</nav>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
This outputs an `<ul>` with all the pages that have the `eleventyNavigation` *front matter* set up:
|
||||
|
||||
```md
|
||||
---
|
||||
eleventyNavigation:
|
||||
key: home
|
||||
---
|
||||
|
||||
# Rawr there! 👋🏻
|
||||
|
||||
Welcome to the dragon's hoard
|
||||
```
|
||||
|
||||
```md
|
||||
---
|
||||
eleventyNavigation:
|
||||
key: about
|
||||
---
|
||||
|
||||
# About me
|
||||
|
||||
I'm a durgin! I go rawr! I also code 👾
|
||||
```
|
||||
|
||||
That gives us 2 entries in the navigation. Every navigation element must have at least a `key` by which it is identified in the navigation structure. Optional parameters include `parent`, which is used to refer to a `key` to get nested navigation items. If the entries need to be reordered, the `order` parameter does exactly that.
|
||||
|
||||
Now, having just an `<ul>` full of links doesn't exactly make for the prettiest navigation. For that reason, the `eleventyNavigationToHtml` filter accepts an option object that lets you customize the elements and classes it will put in the markup of the final page. That gives you enough control over the visual design of the navigation, like hover states and highlighting the currently active page.
|
||||
|
||||
Something I wish the docs were clearer about, though, is the fact that this is not entirely solvable in *front matter* as shown in the [examples](https://www.11ty.dev/docs/plugins/navigation/#advanced-all-rendering-options-for-eleventynavigationtohtml). Added to my `base.njk` layout it just kept throwing errors about `eleventyNavigation` not being defined (I guess it means the value for the `activeKey`?) which is confusing as hell when it's right there. So either the example given in the docs is just flat-out wrong or the filter doesn't get passed the instance of `eleventyNavigation`. That kept me busy for longer than I would've liked. Eventually I put it in directly in the options of the filter. Doesn't look as tidy as I would've liked, but it works.
|
||||
|
||||
```twig
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
{% raw %}{{
|
||||
collections.all |
|
||||
eleventyNavigation |
|
||||
eleventyNavigationToHtml({
|
||||
listClass: "nav-list",
|
||||
anchorClass: "nav-link",
|
||||
activeAnchorClass: "active",
|
||||
activeKey: eleventyNavigation.key
|
||||
}) | safe
|
||||
}}{% endraw %}
|
||||
</nav>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
As long as the blog is still in its infancy, the post overview page is still quite manageable. Depending on how frequent I'll be posting, however, this may change. Even though I'll only show the first paragraph of each post in the listing, I don't want readers to have to scroll for ages to find something.
|
||||
|
||||
This is where the pagination plugin comes to the rescue! If I want the blog post page to only show a limited number of posts before it's necessary to "flip the page", I just add it to that page like so:
|
||||
|
||||
```twig
|
||||
---
|
||||
layout: postlist.njk
|
||||
title: Posts
|
||||
eleventyNavigation:
|
||||
key: posts
|
||||
order: 3
|
||||
pagination:
|
||||
data: collections.posts
|
||||
size: 10
|
||||
alias: blogposts
|
||||
reverse: true
|
||||
---
|
||||
|
||||
{% raw %}{% for post in blogposts %}
|
||||
<article>
|
||||
<h1>{{ post.data.title }}</h1>
|
||||
|
||||
<p>{{ post.data.description }}</p>
|
||||
|
||||
<a href="{{ post.url }}" class="button">Read on</a>
|
||||
</article>
|
||||
{% endfor %}{% endraw %}
|
||||
```
|
||||
|
||||
As you can see, pagination is done with the aptly named `pagination` object in *front matter*. For it to work properly it needs data to operate on, in this case the `posts` collection, and the maximum number of items per page. Optional parameters I've used here are `alias` for reference in the actual template code and `reverse` to sort posts from newest to oldest.
|
||||
|
||||
Adding it to the page where the posts will eventually get listed:
|
||||
|
||||
```twig
|
||||
<nav>
|
||||
<ol class="pagination">
|
||||
<li><a href="{% raw %}{{ pagination.href.first }}{% endraw %}">First Page</a></li>
|
||||
<li><a href="{% raw %}{{ pagination.href.previous }}{% endraw %}">Previous Page</a></li>
|
||||
{% raw %}{%- for pageEntry in pagination.pages %}
|
||||
<li>
|
||||
<a href="{{ pagination.hrefs[ loop.index0 ] }}"
|
||||
{% if page.url == pagination.hrefs[ loop.index0 ] %}
|
||||
aria-current="page"
|
||||
{% endif %}
|
||||
>
|
||||
<!-- The aria-current attribute gives us an additional selector for CSS -->
|
||||
<!-- It's also a quick accessibility win! -->
|
||||
{{ loop.index }}
|
||||
</a>
|
||||
</li>
|
||||
{%- endfor %}{% endraw %}
|
||||
<li><a href="{% raw %}{{ pagination.href.next }}{% endraw %}">Next Page</a></li>
|
||||
<li><a href="{% raw %}{{ pagination.href.last }}{% endraw %}">Last Page</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
```
|
||||
|
||||
It might be unnecessary to show pagination when there's less than 10 posts (only 1 page). So let's hide it when it would be otherwise unnecessary:
|
||||
|
||||
```twig
|
||||
{% raw %}{% if pagination.pages.length > 1 %}
|
||||
<nav>
|
||||
<ol class="pagination">
|
||||
<li><a href="{{ pagination.href.first }}">First Page</a></li>
|
||||
<li><a href="{{ pagination.href.previous }}">Previous Page</a></li>
|
||||
{%- for pageEntry in pagination.pages %}
|
||||
<li>
|
||||
<a href="{{ pagination.hrefs[ loop.index0 ] }}"
|
||||
{% if page.url == pagination.hrefs[ loop.index0 ] %}
|
||||
aria-current="page"
|
||||
{% endif %}
|
||||
>
|
||||
<!-- The aria-current attribute gives us an additional selector for CSS -->
|
||||
<!-- It's also a quick accessibility win! -->
|
||||
{{ loop.index }}
|
||||
</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
<li><a href="{{ pagination.href.next }}">Next Page</a></li>
|
||||
<li><a href="{{ pagination.href.last }}">Last Page</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}{% endraw %}
|
||||
```
|
||||
|
||||
We can take this even further: If the visitor is viewing the first page in the pagination it wouldn't make much sense for the first and previous links to be clickable. The same goes for the next and last links. So let's give them elements that aren't clickable by default and add the `aria-disabled` attributes for both communicating to assistive technologies that the element is disabled and to have an additional way to target them in CSS.
|
||||
|
||||
```twig
|
||||
{% raw %}{% if pagination.pages.length > 1 %}
|
||||
<nav aria-label="Post pages">
|
||||
<ol class="pagination">
|
||||
<li>
|
||||
{% if page.url != pagination.href.first %}
|
||||
<a href="{{ pagination.href.first }}">First Page</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true">First Page</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if pagination.href.previous %}
|
||||
<a href="{{ pagination.href.previous }}">Previous Page</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true">Previous Page</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<!-- Loop over pagination.pages array and add the pages to the list -->
|
||||
{%- for pageEntry in pagination.pages %}
|
||||
<li>
|
||||
<a href="{{ pagination.hrefs[ loop.index0 ] }}"
|
||||
{% if page.url == pagination.hrefs[ loop.index0 ] %}
|
||||
aria-current="page"
|
||||
{% endif %}
|
||||
>
|
||||
{{ loop.index }}
|
||||
</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
<li>
|
||||
{% if pagination.href.next %}
|
||||
<a href="{{ pagination.href.next }}">Next Page</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true">Next Page</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if page.url != pagination.href.last %}
|
||||
<a href="{{ pagination.href.last }}">Last Page</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true">Last Page</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}{% endraw %}
|
||||
```
|
||||
|
||||
This might seem a bit excessive, but the additional if-statements allow for some clear visual cues for when a visitor has reached the beginning/end of the list and prevents clicking on navigation elements that shouldn't be clickable.
|
||||
|
||||
### Extending bundled libraries
|
||||
|
||||
As mentioned, Eleventy comes with a pretty extensible Markdown parser called `markdown-it`. Extending the included libraries is done via the `amendLibrary()` helper function:
|
||||
|
||||
```js
|
||||
import markdownIt from 'markdown-it';
|
||||
import markdownItAbbr from 'markdown-it-abbr';
|
||||
import markdownItAnchor from 'markdown-it-anchor';
|
||||
import markdownItCallouts from 'markdown-it-obsidian-callouts';
|
||||
import markdownItCollapsible from 'markdown-it-collapsible';
|
||||
import markdownItFootnote from 'markdown-it-footnote';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.setLibrary('md', markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
}));
|
||||
|
||||
eleventyConfig.amendLibrary('md', (mdLib) =>
|
||||
mdLib
|
||||
.use(markdownItAbbr)
|
||||
.use(markdownItAnchor)
|
||||
.use(markdownItCollapsible)
|
||||
.use(markdownItCallouts)
|
||||
.use(markdownItFootnote)
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
The `mdLib` parameter makes the library interface of the chosen file type (`'md'`) available. The `use()` method is the same method that's showcased in the majority of [`markdown-it` plugins on NPM](https://www.npmjs.com/search?q=keywords%3Amarkdown%2Dit%2Dplugin).
|
||||
|
||||
### Adding Tailwind
|
||||
|
||||
Tailwind is a popular CSS framework that gets you quick results very easily without ever writing a single line of CSS yourself. I've been using it since this year and after some initial learning curve I've gotten some good use out of it[^sebinontailwind].
|
||||
|
||||
[^sebinontailwind]: I've gone [on record](https://meow.social/@SebinNyshkim/111771456618970816) in the past that I'm not particularly fond of the "CSS class barf" that Tailwind makes you write in your markup. I still think it looks awful, but the design clearly targets re-usable components written in frameworks like React and Vue, and the speed at which it allows you to get good looking results is undeniable.
|
||||
|
||||
Adding Tailwind to an Eleventy project is pretty straight-forward:
|
||||
|
||||
```bash
|
||||
npm install tailwindcss @tailwindcss/cli
|
||||
```
|
||||
|
||||
Starting with v4, integrating Tailwind is as easy as creating a CSS file and adding a single line to it:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
```
|
||||
|
||||
In order for Tailwind and Eleventy to run next to each other the entries in the `script` section of `package.json` need to be adjusted a bit:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"start": "eleventy --serve & npx @tailwindcss/cli -i ./src/css/style.css -o ./public/css/style.css --watch",
|
||||
"build": "ELEVENTY_PRODUCTION=true eleventy && NODE_ENV=production npx tailwindcss -i ./src/css/style.css -o ./public/css/style.css --minify"
|
||||
}
|
||||
```
|
||||
|
||||
And that's about it! Tailwind is now ready to use in Eleventy templates and layouts.
|
||||
|
||||
There's just one problem: I can't add Tailwind classes to my Markdown files! 😨
|
||||
|
||||
### Tailwind Typography
|
||||
|
||||
Not to worry, though! Tailwind offers a 1st party typography plugin that is specifically made for this use case.
|
||||
|
||||
It's a quick install with `npm`:
|
||||
|
||||
```bash
|
||||
npm install @tailwindcss/typography
|
||||
```
|
||||
|
||||
And just one additional line of CSS:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@plugin '@tailwindcss/typography';
|
||||
```
|
||||
|
||||
Now there's a whole set of new utility classes available. Tailwind Typography comes with sensible default styles for prose content. Just adding the `prose` CSS class to the blog post layout is enough:
|
||||
|
||||
```twig
|
||||
<article class="prose">
|
||||
{% raw %}{{ content | safe }}{% endraw %}
|
||||
</article>
|
||||
```
|
||||
|
||||
Yes, that's really all there is to it! Tailwind Typography makes heavy use of the CSS [`:where()` pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:where) for styling descendants of the element with the `prose` class. If something in the styling of descendants is not to my liking, Tailwind Typography comes with its own set of modifiers to adjust the formatting of the most common content tags (`<h1>` - `<h6>`, `<p>`, `<a>`, `<strong>`, `<em>`, `<ul>`, `<ol>`, `<li>`, `<img>`, `<table>`, `<tr>`, `<th>`, `<td>`, `<blockquote>`, and so on). And if something should absolutely not be formatted like prose the `not-prose` class prevents that.
|
||||
|
||||
See the Tailwind Typography [GitHub](https://github.com/tailwindlabs/tailwindcss-typography#basic-usage) page for a primer on how to get the most out of it.
|
||||
|
||||
## Documentation woes
|
||||
|
||||
As fun as it was learning Eleventy and being really impressed with what it can help you accomplish, the learning curve at times was not. Lots of that comes down to its documentation, which could use lots more love.
|
||||
|
||||
Don't get me wrong, it gets the job done and large parts of my blog are lifted straight from there. But when learning something new, especially something like a framework, I expect the docs to be better than "just barely made it". It's the make or break factor whether it can garner wider adoption.
|
||||
|
||||
Getting the blog to a functionally and visually pleasing state took me about 3 days, most of which was spent on trying to figure out problems the docs couldn't provide a clear answer for. It was hard for me to decide if I simply used it wrong, or I ran into bugs or some limitation that I wasn't made aware of. Sure, there's the [pitfall section](https://www.11ty.dev/docs/pitfalls/), but it didn't cover the issues I kept running into.
|
||||
|
||||
What made this even more frustrating was that searching online brought up the Eleventy docs repeatedly as the top result—for multiple major versions, too! So when I found myself in this sort of feedback loop, it just added to my frustration, that the docs didn't provide what I needed to understand the issue I was having.
|
||||
|
||||
Sure, there are community contributions linked all over the docs, but my point is if there's docs, I expect them to be comprehensive and everything else to be complementary. The way the docs also repeatedly link to tutorial videos and webcasts instead of a clear textual explanation that's indexable by search engines is something I find very annoying. I don't want to watch video tutorials for v2 or v1 from 2 or 4 years ago that have aged poorly. Neither do I want to read the outdated tutorials of other people the official docs link to which haven't seen updates in years. Just 👏🏻 update 👏🏻 the 👏🏻 official 👏🏻 docs!
|
||||
|
||||
Good examples that come to mind in this regard are the [React](https://react.dev/learn) and [Vue](https://vuejs.org/guide/introduction.html) docs, which are heavily tutorialized and leave the in-depth stuff for later or for when I really need to understand the inner workings better. This is where I feel the Eleventy docs can be very hit or miss.
|
||||
|
||||
There's lots of abstract examples on how to use the navigation and pagination components, but both are missing clear examples of how the data structure looks like. Not just on a surface level, I would've liked examples of how it looks like when it's populated with actual data like it would be when building a site with Eleventy—not just the usual `testdata: [foo, bar, baz]`. And as mentioned earlier, some examples given don't work as shown, as is the case with the Eleventy navigation plugin, where the docs make it look like it should work, but it actually doesn't—either because the examples are outdated or incomplete.
|
||||
|
||||
And while the broad support for every templating language under the sun is nice and all, it's really excessive, to the point I had a hard time deciding which one I should even start with. The differences all seem very minor and if I'm being completely honest, most look like some variation of EJS and Handlebars. I didn't feel like I'm really making an informed decision, more like a gut feeling of what the majority of the community around Eleventy chose to go with and hop on the bandwagon. At least that is sure to get me lots of example code to look at on GitHub when I'm getting desperate, I guess. Still, I would prefer if Eleventy would either reduce the number of templating languages (which would certainly also reduce the amount of work involved to support them all) or give some clear recommendations on which are best supported by the developers of Eleventy (not just based on popularity).
|
||||
|
||||
When it comes to docs of your own framework, you can't apply the Node.js developer mindset of just pulling in tons of other people's work and call it a day. At some point you're begging to have someone pull a leftpad on you.
|
||||
|
||||
## Closing thoughts
|
||||
|
||||
Even though that last part about the docs might come off a little ranty, I still like what I've built with Eleventy and I intend to keep it around.
|
||||
|
||||
It's what I want for a blog based around Markdown and the simplicity of just having to serve static files means it's easy to host from home and actually own my little corner of the web.
|
||||
|
||||
Who knows? Maybe I'll use it for other projects or migrate some other existing projects over. Time will tell.
|
||||
|
||||
Well, I've been rambling for long enough now. This post has been in the oven for well over a week and has received a complete rewrite in-between 😂 (I'm a hopeless perfectionist)
|
||||
|
||||
And so, I declare this new foray into me blogging officially 🎉 **OPEN** 🥳
|
|
@ -0,0 +1,723 @@
|
|||
---
|
||||
title: Responsive, Self-hosted Images for Your Eleventy Blog
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/f33879d8-3f98-4c8e-a1b3-08edf6f174ac
|
||||
alt: Close-up of SVG code on a computer screen
|
||||
credit: Photo by Florian Olivo on Unsplash
|
||||
tags: ["self-hosting", "docker", "eleventy"]
|
||||
---
|
||||
|
||||
While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
GitHub imposes [limits](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github) on the size of files you can commit, and when your repo reaches a certain size, GitHub will ask you nicely to reduce its size (or else…)
|
||||
|
||||
There's Git LFS, but even then, having to check out several hundreds of megabytes of assets (or more) when you're cloning your repo is equally unwieldy and the costs for storage and bandwidth can quickly add up as the repo grows.
|
||||
|
||||
Fortunately, Eleventy provides the `@11ty/eleventy-img` plugin, which is very capable when it comes to transformations and responsive images. It works not only with local files, but also with remote resources and includes them in the output during a build.
|
||||
|
||||
You could certainly host your images on an external site like Imgur and have `@11ty/eleventy-img` pull them in for processing to avoid having to put them in your Git repo. However, external hosting services may have their own restrictions on what you can upload to them (file size, resolution, formats). And with every internet company trying to jump on the "AI" bandwagon, no matter how small, no matter how unnecessary, I'd be very wary of trusting someone else with my blog's images.
|
||||
|
||||
So self-hosting is pretty much the only other option. I found a [repo on GitHub](https://github.com/awesome-selfhosted/awesome-selfhosted#photo-and-video-galleries) that lists a number of self-hosting services, among them a couple for image hosting. One of them, which I would like to cover in this post, is called PicoShare. It can be used to host not only images, but also videos, zip archives and much more. It just hosts the files, it doesn't process them, so it's perfect for running on that Raspberry Pi you've got lying around in a drawer somewhere.
|
||||
|
||||
## Setting up PicoShare
|
||||
|
||||
> [!info] Note
|
||||
> I'm assuming you already know how to use Docker.
|
||||
|
||||
[PicoShare](https://github.com/mtlynch/picoshare) is single-user only, but for the purpose of having a super simple file hosting solution with an equally simple web UI, this is entirely sufficient. If you need to give other people access to it, you can do so with its "Guest Links" feature and they can upload something to it.
|
||||
|
||||
Here's my `docker-compose.yml` I use to run PicoShare:
|
||||
|
||||
```yaml
|
||||
version: "3.2"
|
||||
|
||||
services:
|
||||
picoshare:
|
||||
image: mtlynch/picoshare
|
||||
container_name: picoshare
|
||||
environment:
|
||||
PORT: 4001
|
||||
PS_BEHIND_PROXY: true
|
||||
PS_SHARED_SECRET: SuperSecretPassNobodyWillEverKnow
|
||||
ports:
|
||||
- 4001:4001
|
||||
volumes:
|
||||
- picoshare:/data
|
||||
|
||||
volumes:
|
||||
picoshare:
|
||||
```
|
||||
|
||||
The PicoShare `README.md` doesn't use volumes in its compose file example, but it was the only way for me to work around weird permission issues with bind mounts on Podman. I'm also running it behind a reverse proxy, so I have the `PS_BEHIND_PROXY` environment variable set. This is so I can have a memorable host name as well as TLS termination, which spares me having to get a Let's Encrypt certificate for every individual service I host.
|
||||
|
||||
So after a quick `docker compose up -d` PicoShare should be up and running in no time and visiting `http://localhost:4001` should give your its greeting page.
|
||||
|
||||

|
||||
|
||||
From there you can log in with the passphrase you set in the `docker-compose.yml` and start uploading some files. By default, PicoShare sets an expiration period of 30 days on every upload. If you plan on using it for blog post images, that's probably not what you want, so you may want to disable that in the settings.
|
||||
|
||||
Once you upload a file, PicoShare shows you a full link and a short link. You can use either to link to an image for linking an image on a website.
|
||||
|
||||
## Responsive Images in Eleventy
|
||||
|
||||
In order to have responsive images in the blog at all, you have to customize the config and configure the image transform plugin. Install the `@11ty/eleventy-img` plugin via NPM and add it with the `addPlugin()` helper function with some options:
|
||||
|
||||
```js
|
||||
import { eleventyImageTransformPlugin } from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
|
||||
extensions: "html",
|
||||
formats: ["webp", "jpeg"],
|
||||
defaultAttributes: {
|
||||
loading: "lazy",
|
||||
decoding: "async",
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The Eleventy image transform plugin is highly configurable. Below are the most common options, followed by more advanced options.
|
||||
|
||||
+++ Basic options
|
||||
`extensions` – Type: `string` – Detault: `"html"`
|
||||
: Comma separated list of which extensions to scan for `<img>` tags to transform into responsive `<picture>` tags.
|
||||
|
||||
`widths` – Type: `Array<number | "auto" | null>` – Default: `[null]`
|
||||
: A list of resolutions in which the images should be created. Their aspect ratio will be kept automatically. Including `auto` or `null` in the list will also include a converted version of the image in its original resolution.
|
||||
|
||||
**Important:** When including more than one resolution the `sizes` attribute becomes obligatory. See `defaultAttributes` below.
|
||||
|
||||
`formats` – Type: `Array<string | "auto" | null>` – Default: `['webp', 'jpeg']`
|
||||
: A list of formats images should be converted into. Accepted formats are:
|
||||
- `auto`/`null` (same as input)
|
||||
- `svg`/`svg+xml` (only applies when the input is SVG)
|
||||
- `jpeg`/`jpg`
|
||||
- `png`
|
||||
- `webp`
|
||||
- `avif`
|
||||
|
||||
`concurrency` – Type: `number`– Default: `10`
|
||||
: How many concurrent threads should be used for image transformation.
|
||||
|
||||
`urlPath` – Type: `string` – Default: `"/img/"`
|
||||
: A path that gets prefixed to the `src` attribute of `<img>` tags.
|
||||
|
||||
`outputDir` – Type: `string` – Default: `"img/"`
|
||||
: The output directory for transformed images, relative to the Eleventy output directory.
|
||||
|
||||
`svgShortCircuit` – Type: `boolean | "size"` – Default: `false`
|
||||
: Whether or not to skip raster formats for SVG. Only applies when `svg` is included in the `formats` array.
|
||||
|
||||
- `false` (default): SVG images will be converted into raster images.
|
||||
- `true`: SVG images will not be converted into raster images, even if `svg` is part of the `formats` array of image types to be converted into. Useful when you can't say for sure if the input will be raster or vector based and you want to always keep SVGs as vector based images.
|
||||
- `size`: SVG images will only be converted into raster images, if the resulting output file is smaller than the original SVG file.
|
||||
|
||||
`svgAllowUpscale` – Type: `boolean` – Default: `true`
|
||||
: Whether or not to upscale SVG images before they are converted into raster images.
|
||||
|
||||
`svgCompressionSize` – Type: `string` – Default: `""` (empty string)
|
||||
: Set to `br` to report an SVG's size as it would be after being compressed with [Brotli](https://www.brotli.org/).
|
||||
|
||||
`defaultAttributes` – Type: `object` – Default: `undefined`
|
||||
: HTML attributes to add to every processed image. Can be any valid HTML attribute for [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes) or [`<source>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source#attributes).
|
||||
+++
|
||||
|
||||
For the most common use-cases, the default options get the job done pretty well. If you want to fine-tune the inner workings of the Eleventy image transform plugin even further, refer to the advanced options below:
|
||||
|
||||
+++ Advanced options
|
||||
`sharpOptions` – Type: `sharp.SharpOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-constructor)
|
||||
: An options object to pass to the Sharp constructor.
|
||||
|
||||
`sharpWebpOptions` – Type: `sharp.WebpOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#webp)
|
||||
: Options for how WebP images should be processed by Sharp.
|
||||
|
||||
`sharpPngOptions` – Type: `sharp.PngOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#png)
|
||||
: Options for how PNG images should be processed by Sharp.
|
||||
|
||||
`sharpJpegOptions` – Type: `sharp.JpegOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#jpeg)
|
||||
: Options for how JPEG images should be processed by Sharp.
|
||||
|
||||
`sharpAvifOptions` – Type: `sharp.AvifOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#avif)
|
||||
: Options for how AVIF images should be processed by Sharp.
|
||||
|
||||
`cacheOptions` – Type: `CacheOptions` – Default: See below
|
||||
: Controls how external sources should be cached.
|
||||
|
||||
`type` – Type: `string` – Default: `'buffer'`
|
||||
: The expected content type of the response when fetching the remote resource. Accepts `buffer` (binary data), `json` or `text`.
|
||||
|
||||
`directory` – Type: `string` – Default: `".cache"`
|
||||
: Directory in which to store cached resources. Relative to the project directory.
|
||||
|
||||
`fetchOptions` – Type: `RequestInit` – Default: See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)
|
||||
: Options for configuring the behavior of the JavaScript Fetch API used to fetch resources.
|
||||
|
||||
`concurrency` – Type: `number` – Default: `10`
|
||||
: How many concurrent fetch operations are allowed.
|
||||
|
||||
`duration` – Type: `string` – Default: `'1d'`
|
||||
: Time that needs to pass before fetching an already fetched resource again.
|
||||
|
||||
`removeUrlQueryParams` – Type: `boolean` – Default: `undefined`
|
||||
: Whether or not to remove the query parameters from URLs cached by Eleventy Image.
|
||||
|
||||
`dryRun` – Type: `boolean` – Default: `undefined`
|
||||
: Only simulate doing something.
|
||||
|
||||
`verbose` – Type: `boolean` – Default: `undefined`
|
||||
: Control verbosity of the cache.
|
||||
|
||||
`hashLength` – Type: `number` – Default: `30`
|
||||
: Truncates hash to this length.
|
||||
|
||||
`filenameFormat` – Type: `function(options): string | null | undefined`
|
||||
: A function to customize the file names of transformed images in the final output. Returns either `string`, `null` or `undefined`
|
||||
|
||||
`options` – Type: `object`
|
||||
: `id` – Type: `string`
|
||||
: Hash of the original image
|
||||
|
||||
`src` – Type: `string`
|
||||
: Original image path
|
||||
|
||||
`width` – Type: `number`
|
||||
: Current width in px
|
||||
|
||||
`format` – Type: `string`
|
||||
: Current file format
|
||||
|
||||
`options` – Type: `ImageOptions`
|
||||
: An Eleventy Image options object (yes, the one you're reading right now)
|
||||
|
||||
`urlFormat` – Type: `function(format, options): string`
|
||||
: A function to control how the URLs will look like when processed by Eleventy Image. When setting this function, its parameters are obligatory. Useful when you already have your own image processing service and saving the images with the site isn't necessary or desired. Returns a string (the final URL).
|
||||
|
||||
`format` – Type: `object`
|
||||
: `hash` – Type: `string`
|
||||
: hash of the remote image. Not included for `statsOnly` images.
|
||||
|
||||
`src` – Type: `string`
|
||||
: source file name of the remote image.
|
||||
|
||||
`width` – Type: `number`
|
||||
: output width the remote image should have.
|
||||
|
||||
`format` – Type: `string`
|
||||
: image format the remote image should have.
|
||||
|
||||
`options` – Type: `ImageOptions`
|
||||
: An Eleventy Image options object (yes, the one you're reading right now)
|
||||
|
||||
`statsOnly` – Type: `boolean` – Default: `undefined`
|
||||
: Whether or not to process files. If `true` only returned stats of images, doesn't read or write any files. Useful in combination with `urlFormat()`.
|
||||
|
||||
`useCache` – Type: `boolean` – Default: `true`
|
||||
: Whether or not to use in-memory cache.
|
||||
|
||||
`dryRun` – Type: `boolean` – Default: `undefined`
|
||||
: Whether or not to write anything to disk. A buffer instance will still be returned.
|
||||
|
||||
`hashLength` – Type: `number` – Default: `10`
|
||||
: The maximum length of file name hashes.
|
||||
|
||||
`useCacheValidityInHash` – Type: `true` – Default: `true`
|
||||
: Advanced
|
||||
|
||||
`fixOrientation` – Type: `boolean` – Default: `false`
|
||||
: Whether or not to rotate images to ensure correct orientation.
|
||||
|
||||
`minimumThreshold` – Type: `number` – Default: `1.25`
|
||||
: Ensures original size is included if smaller than largest specified width by threshold amount.
|
||||
+++
|
||||
|
||||
After you've set up the plugin to your needs, whenever you're using an `<img>` in your templates (or the equivalent syntax in Markdown: ``), the plugin will transform the referenced image according to your options and put a corresponding `<picture>` tag in the resulting output.
|
||||
|
||||
> [!tip] Tip
|
||||
> If you want a side by side comparison to decide how to fine-tune the image compressors, check out [Squoosh](https://squoosh.app/).
|
||||
|
||||
Some recommendations for a config with some options set:
|
||||
|
||||
```js
|
||||
import { eleventyImageTransformPlugin } from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
|
||||
extensions: 'html',
|
||||
formats: ['avif', 'webp', 'auto'], // commonly supported image formats
|
||||
widths: [640, 1280, 1920, 3840, 'auto'], // image sizes, from mobile up to 4K
|
||||
sharpJpegOptions: {
|
||||
mozjpeg: true, // more efficient compression for JPEGs
|
||||
optimiseScans: true, // progressive loading
|
||||
quality: 95 // higher quality
|
||||
},
|
||||
sharpPngOptions: {
|
||||
compressionLevel: 9 // max compression for PNGs
|
||||
},
|
||||
urlPath: '/img/', // prefix for generated <img src>
|
||||
outputDir: './public/img/', // put all transformed images in the same place
|
||||
defaultAttributes: {
|
||||
loading: 'lazy', // only load images once they come into view
|
||||
decoding: 'async', // render images after DOM content has rendered
|
||||
sizes: '100vw' // when more than 1 value in widths, set this
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
> [!attention] Attention
|
||||
> When specifying multiple values for `widths` you will also have to specify a `sizes` option in `defaultAttributes`. From [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img):
|
||||
> > If the `srcset` attribute uses width descriptors, the `sizes` attribute must also be present, or the `srcset` itself will be ignored.
|
||||
>
|
||||
> The Eleventy Image docs don't explicitly mention this[^docs].
|
||||
|
||||
> [!info] Info
|
||||
> The order of `formats` corresponds to the order of the `<source>` elements in the final output. The browser picks the first format in the list it supports. If you want it to use AVIF before WebP, make sure the list starts with `avif`.
|
||||
|
||||
Now you can just point to any image, local or remote, and the image transform plugin will download it, convert and transform it and include all sizes and formats in the output.
|
||||
|
||||
### Responsive CSS Background Images with `image-set()`
|
||||
|
||||
The docs advertise the image transform plugin to also handle CSS `background-image` but I don't find it working. Neither by adding `css` to the `extensions` option, nor assigning `background-image` in the CSS for my site or directly in my `base.njk` template in a `<style>` tag. That's not an issue, however, as I can just write my own shortcode to generate the CSS properties I need.
|
||||
|
||||
In the same vein as the `<picture>` element with multiple `<source>` tags, there's a counterpart in CSS called [`image-set()`](https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set). The idea is pretty similar, albeit a little more restricted.
|
||||
|
||||
The `image-set()` CSS function takes up to 3 arguments per entry:
|
||||
|
||||
- a `url()` pointing to the image file
|
||||
- an optional `type()` indicating what format it is
|
||||
- an optional pixel density for which resolution to use
|
||||
|
||||
```css
|
||||
.class {
|
||||
background-image: image-set(
|
||||
url("/img/background-1x.avif") type("image/avif") 1x,
|
||||
url("/img/background-2x.avif") type("image/avif") 2x,
|
||||
url("/img/background-3x.avif") type("image/avif") 3x,
|
||||
|
||||
url("/img/background-1x.webp") type("image/webp") 1x,
|
||||
url("/img/background-2x.webp") type("image/webp") 2x,
|
||||
url("/img/background-3x.webp") type("image/webp") 3x,
|
||||
|
||||
url("/img/background-1x.jpg") type("image/jpeg") 1x,
|
||||
url("/img/background-2x.jpg") type("image/jpeg") 2x,
|
||||
url("/img/background-3x.jpg") type("image/jpeg") 3x
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
It sadly does not take widths like a `<source>`, only pixel densities, which is kinda limiting.
|
||||
|
||||
In order to make use of this and have it be dynamic, we need to write our own little shortcode.
|
||||
|
||||
First we're importing the `Image` function from `@11ty/eleventy-img` in the `eleventy.config.js`, will do most of the heavy lifting for us:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
```
|
||||
|
||||
Then we'll add a new shortcode (call it `bgimgset`) and make it call an async function with a single argument of `src`:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
// shortcode inner workings here
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The function needs to be async because `Image` works asynchronously, as it can fetch remote resources. Doing it synchronously would block everything else from running until the resource has been fetched. We don't want that.
|
||||
|
||||
Next, we'll pass `Image` the path or URL our shortcode receives as the `src` argument, which can be a local path or a URL to an image, and hold onto the result:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
If we want it to convert the image, we need to pass `Image` some options, too:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
widths: [1920, 2560, 3840],
|
||||
sharpJpegOptions: { mozjpeg: true, optimiseScans: true, quality: 95 },
|
||||
sharpPngOptions: { compressionLevel: 9 },
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/'
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
You might notice, these are the same options we also passed the image transform plugin earlier. That's because they pretty much share the same functionality, we're just calling `Image` manually here.
|
||||
|
||||
After that, `imgset` holds all the stats of all the variants of our image. The data structure looks something like this:
|
||||
|
||||
```js
|
||||
{
|
||||
avif: [
|
||||
{
|
||||
format: 'avif',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.avif',
|
||||
sourceType: 'image/avif',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.avif 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.avif',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.avif',
|
||||
size: 107616
|
||||
},
|
||||
{
|
||||
format: 'avif',
|
||||
width: 2560,
|
||||
height: 2353,
|
||||
url: '/img/Pcl6PSU1Rc-2560.avif',
|
||||
sourceType: 'image/avif',
|
||||
srcset: '/img/Pcl6PSU1Rc-2560.avif 2560w',
|
||||
filename: 'Pcl6PSU1Rc-2560.avif',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-2560.avif',
|
||||
size: 152680
|
||||
},
|
||||
{
|
||||
format: 'avif',
|
||||
width: 3840,
|
||||
height: 3530,
|
||||
url: '/img/Pcl6PSU1Rc-3840.avif',
|
||||
sourceType: 'image/avif',
|
||||
srcset: '/img/Pcl6PSU1Rc-3840.avif 3840w',
|
||||
filename: 'Pcl6PSU1Rc-3840.avif',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-3840.avif',
|
||||
size: 261934
|
||||
}
|
||||
],
|
||||
webp: [
|
||||
{
|
||||
format: 'webp',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.webp',
|
||||
sourceType: 'image/webp',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.webp 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.webp',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.webp',
|
||||
size: 214036
|
||||
},
|
||||
{
|
||||
format: 'webp',
|
||||
width: 2560,
|
||||
height: 2353,
|
||||
url: '/img/Pcl6PSU1Rc-2560.webp',
|
||||
sourceType: 'image/webp',
|
||||
srcset: '/img/Pcl6PSU1Rc-2560.webp 2560w',
|
||||
filename: 'Pcl6PSU1Rc-2560.webp',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-2560.webp',
|
||||
size: 317192
|
||||
},
|
||||
{
|
||||
format: 'webp',
|
||||
width: 3840,
|
||||
height: 3530,
|
||||
url: '/img/Pcl6PSU1Rc-3840.webp',
|
||||
sourceType: 'image/webp',
|
||||
srcset: '/img/Pcl6PSU1Rc-3840.webp 3840w',
|
||||
filename: 'Pcl6PSU1Rc-3840.webp',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-3840.webp',
|
||||
size: 599626
|
||||
}
|
||||
],
|
||||
jpeg: [
|
||||
{
|
||||
format: 'jpeg',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.jpeg',
|
||||
sourceType: 'image/jpeg',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.jpeg 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.jpeg',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.jpeg',
|
||||
size: 756970
|
||||
},
|
||||
{
|
||||
format: 'jpeg',
|
||||
width: 2560,
|
||||
height: 2353,
|
||||
url: '/img/Pcl6PSU1Rc-2560.jpeg',
|
||||
sourceType: 'image/jpeg',
|
||||
srcset: '/img/Pcl6PSU1Rc-2560.jpeg 2560w',
|
||||
filename: 'Pcl6PSU1Rc-2560.jpeg',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-2560.jpeg',
|
||||
size: 1281555
|
||||
},
|
||||
{
|
||||
format: 'jpeg',
|
||||
width: 3840,
|
||||
height: 3530,
|
||||
url: '/img/Pcl6PSU1Rc-3840.jpeg',
|
||||
sourceType: 'image/jpeg',
|
||||
srcset: '/img/Pcl6PSU1Rc-3840.jpeg 3840w',
|
||||
filename: 'Pcl6PSU1Rc-3840.jpeg',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-3840.jpeg',
|
||||
size: 2746900
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<!-- We need to transform this data structure a bit for our needs. What we need to do is loop over every format and get the `url` and `sourceType` for each image. -->
|
||||
|
||||
<!-- We can loop over an object's values with the aptly named `Object.values()`, which will give us an array containing all the values of the object we pass it. The value of each key is an array itself, containing objects with the actual data we're interested in. We end up with arrays inside an array, which is a bit messy to work with. We can flatten this structure with `Array.prototype.flat()`: -->
|
||||
|
||||
As you can see, the resulting object contains all the stats for our image in all the specified formats and resolutions. What we need to do now is iterate over each format and extract the `url` and `sourceType` for each individual variant of the image. This is easily achieved with `Object.values()` which will return the value of each key in the object in an array:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
widths: [1920, 2560, 3840],
|
||||
sharpJpegOptions: { mozjpeg: true, optimiseScans: true, quality: 95 },
|
||||
sharpPngOptions: { compressionLevel: 9 },
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/'
|
||||
});
|
||||
|
||||
// all the values of each image format key
|
||||
const images = Object.values(imgset)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
That gives us a multi-dimensional array (shortened for clarity):
|
||||
|
||||
```js
|
||||
[
|
||||
[
|
||||
{
|
||||
format: 'avif',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.avif',
|
||||
sourceType: 'image/avif',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.avif 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.avif',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.avif',
|
||||
size: 107616
|
||||
},
|
||||
// ...
|
||||
],
|
||||
[
|
||||
{
|
||||
format: 'webp',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.webp',
|
||||
sourceType: 'image/webp',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.webp 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.webp',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.webp',
|
||||
size: 214036
|
||||
},
|
||||
// ...
|
||||
],
|
||||
[
|
||||
{
|
||||
format: 'jpeg',
|
||||
width: 1920,
|
||||
height: 1765,
|
||||
url: '/img/Pcl6PSU1Rc-1920.jpeg',
|
||||
sourceType: 'image/jpeg',
|
||||
srcset: '/img/Pcl6PSU1Rc-1920.jpeg 1920w',
|
||||
filename: 'Pcl6PSU1Rc-1920.jpeg',
|
||||
outputPath: 'public/img/Pcl6PSU1Rc-1920.jpeg',
|
||||
size: 756970
|
||||
},
|
||||
// ...
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
We can loop over this with nested [`Array.prototype.map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to get the data we need to build valid CSS that `image-set()` will accept:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
widths: [1920, 2560, 3840],
|
||||
sharpJpegOptions: { mozjpeg: true, optimiseScans: true, quality: 95 },
|
||||
sharpPngOptions: { compressionLevel: 9 },
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/'
|
||||
});
|
||||
|
||||
const images = Object.values(imgset);
|
||||
|
||||
// loop over every image format
|
||||
const cssImageSet = images.map(function (format) {
|
||||
|
||||
// loop over every resolution of current format
|
||||
format.map(function (resolution, index) {
|
||||
|
||||
// get the relevant data of the current resolution
|
||||
const { url, sourceType } = resolution;
|
||||
|
||||
// return a string in the format CSS image-set() expects
|
||||
// e.g. `url(/img/Pcl6PSU1Rc-1920.avif) type('image/avif') 1x`
|
||||
return `url(${url}) type('${sourceType}') ${index + 1}x`;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
We can shorten this a bit by using arrow functions and destructuring the object immediately in the callback function's arguments:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
widths: [1920, 2560, 3840],
|
||||
sharpJpegOptions: { mozjpeg: true, optimiseScans: true, quality: 95 },
|
||||
sharpPngOptions: { compressionLevel: 9 },
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/'
|
||||
});
|
||||
|
||||
const images = Object.values(imgset);
|
||||
|
||||
const cssImageSet = images.map((format) =>
|
||||
format.map(({ url, sourceType }, i) =>
|
||||
`url(${url}) type('${sourceType}') ${i + 1}x`
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Now we have a data structure that's looking pretty close to what we need:
|
||||
|
||||
```js
|
||||
[
|
||||
[
|
||||
"url(/img/Pcl6PSU1Rc-1920.avif) type('image/avif') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.avif) type('image/avif') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.avif) type('image/avif') 3x"
|
||||
],
|
||||
[
|
||||
"url(/img/Pcl6PSU1Rc-1920.webp) type('image/webp') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.webp) type('image/webp') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.webp) type('image/webp') 3x"
|
||||
],
|
||||
[
|
||||
"url(/img/Pcl6PSU1Rc-1920.jpeg) type('image/jpeg') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.jpeg) type('image/jpeg') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.jpeg) type('image/jpeg') 3x"
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
Before we can return the result, however, we need to flatten the multi-dimensional array we've been working with so far:
|
||||
|
||||
```js
|
||||
import Image from '@11ty/eleventy-img';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addShortcode('bgimgset', async (src) => {
|
||||
const imgset = await Image(src, {
|
||||
widths: [1920, 2560, 3840],
|
||||
sharpJpegOptions: { mozjpeg: true, optimiseScans: true, quality: 95 },
|
||||
sharpPngOptions: { compressionLevel: 9 },
|
||||
formats: ['avif', 'webp', 'auto'],
|
||||
urlPath: '/img/',
|
||||
outputDir: './public/img/'
|
||||
});
|
||||
|
||||
const images = Object.values(imgset);
|
||||
|
||||
const cssImageSet = images.map((format) =>
|
||||
format.map(({ url, sourceType }, i) =>
|
||||
`url(${url}) type('${sourceType}') ${i + 1}x`
|
||||
)
|
||||
);
|
||||
|
||||
return cssImageSet.flat();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Which gives us the final data structure we actually want:
|
||||
|
||||
```js
|
||||
[
|
||||
"url(/img/Pcl6PSU1Rc-1920.avif) type('image/avif') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.avif) type('image/avif') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.avif) type('image/avif') 3x",
|
||||
"url(/img/Pcl6PSU1Rc-1920.webp) type('image/webp') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.webp) type('image/webp') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.webp) type('image/webp') 3x",
|
||||
"url(/img/Pcl6PSU1Rc-1920.jpeg) type('image/jpeg') 1x",
|
||||
"url(/img/Pcl6PSU1Rc-2560.jpeg) type('image/jpeg') 2x",
|
||||
"url(/img/Pcl6PSU1Rc-3840.jpeg) type('image/jpeg') 3x"
|
||||
]
|
||||
```
|
||||
|
||||
Now we're ready to make use of our new shortcode to get an `image-set()` from a single image:
|
||||
|
||||
```css
|
||||
.class {
|
||||
background-image: image-set({% raw %}{% bgimgset "https://cdn.imagesfrom.space/coffee-nebula_8150x7493.jpg" %}{% endraw %});
|
||||
}
|
||||
```
|
||||
|
||||
And get this:
|
||||
|
||||
```css
|
||||
.class {
|
||||
background-image: image-set(
|
||||
url(/img/Pcl6PSU1Rc-1920.avif) type('image/avif') 1x,
|
||||
url(/img/Pcl6PSU1Rc-2560.avif) type('image/avif') 2x,
|
||||
url(/img/Pcl6PSU1Rc-3840.avif) type('image/avif') 3x,
|
||||
|
||||
url(/img/Pcl6PSU1Rc-1920.webp) type('image/webp') 1x,
|
||||
url(/img/Pcl6PSU1Rc-2560.webp) type('image/webp') 2x,
|
||||
url(/img/Pcl6PSU1Rc-3840.webp) type('image/webp') 3x,
|
||||
|
||||
url(/img/Pcl6PSU1Rc-1920.jpeg) type('image/jpeg') 1x,
|
||||
url(/img/Pcl6PSU1Rc-2560.jpeg) type('image/jpeg') 2x,
|
||||
url(/img/Pcl6PSU1Rc-3840.jpeg) type('image/jpeg') 3x
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now the browser can make smarter choices about which image to use for the background, giving visitors the best quality image for their device's resolution and saving you both valuable bandwidth!
|
||||
|
||||
*[LFS]: Large File Storage
|
||||
|
||||
[^docs]: It took me a while to figure out this error specifically:
|
||||
> Error: Missing `sizes` attribute on eleventy-img shortcode from: (url)
|
||||
|
||||
What mystified me about this error was that I wasn't using the eleventy-img shortcode anywhere. At first, I thought it was complaining about some Nunjucks template, so I went and added the `sizes` attribute on there and was fine for a while. Then when I added an image to one of my posts for the first time the error returned.
|
||||
|
||||
What's not immediately self-apparent is, that it seems like the shortcode gets used internally somewhere in between the transformation from Markdown to the final static HTML output, where it fails an internal check. That check is entirely justified, since it would be even more confusing if the `srcset` would get ignored and the images would misbehave. However, since Markdown doesn't offer any way to add arbitrary attributes to images linked in Markdown files (not [natively](https://www.npmjs.com/package/markdown-it-attrs) at least), the only way to satisfy that check is to add the `defaultAttributes` option when setting up the plugin and include a `sizes` attribute there.
|
||||
|
||||
The docs aren't as explicit about that fact as I'd have liked. I had to once again scour the web and read about it in someone else's [blog post](https://www.aleksandrhovhannisyan.com/blog/eleventy-image-transform/#configuration-api), a pattern that, much to my annoyance, bears repeating…
|
153
src/posts/2024-10-24_apple-clueless-about-ipad.md
Normal file
153
src/posts/2024-10-24_apple-clueless-about-ipad.md
Normal file
|
@ -0,0 +1,153 @@
|
|||
---
|
||||
title: Apple has no idea what to do with the iPad
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/e634e666-bdd0-4694-bd59-929ada1cd30a
|
||||
alt: iOS face with swirly eyes emoji surrounded by a word cloud of Apple marketing jargon on red background with jagged lines
|
||||
credit: Made with GIMP
|
||||
tags: ["apple"]
|
||||
---
|
||||
|
||||
Apple recently unveiled their refresh of the iPad mini. Most surprising to me is that it comes with the A17 Pro chip. That got me thinking… Wasn't the iPad mini meant as a small casual device? Why does it need a Pro chip?! But in conversations with friends I came to realize that the iPad lineup has been getting ever more confusing as time went on. And I'm not alone.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Since the launch of the iPhone 15 Apple has split the A-line of chips into Pro and non-Pro chips. The non-Pro iPhone 15 got the A16 Bionic, same as the iPhone 14 of the previous year. The iPhone 15 Pro got the improved A17 Pro. That certainly was met with criticism, but was hardly out of the ordinary for a company such as Apple. But with the launch of the iPhone 16 lineup this year, Apple did a complete 180 and both the Pro and non-Pro models of iPhones got the same chip again.
|
||||
|
||||
The iPhone lineup has always been pretty easy to navigate. If you don't care and just want an iPhone, you buy a non-Pro iPhone. If you want the best iPhone money can buy, you go Pro. The number of each generation increases by 1 each year, so you can clearly distinguish them.
|
||||
|
||||
## How it started…
|
||||
|
||||
The iPad lineup, in the beginning, shared it's chip with the iPhone until Apple started branching out and added the X suffix to A-series chips mean for iPads, packing more punch than their iPhone counterparts. The iPad mini meanwhile stayed with the same A-series chips as the iPhone.
|
||||
|
||||
The consistent numbering scheme of iPad hardware generations was only shared with the iPhone briefly. After the iPad 2 of 2011, the generational number was dropped in favor of just calling it the iPad and refer to the "nth generation" in support documents.
|
||||
|
||||
After the 3rd generation in March 2012 and the 4th generation in November 2012 the naming of the main iPad lineup transitioned to the iPad Air branding in 2013. It was launched alongside the iPad mini 2. Both iPad variants followed up with a new model in 2014.
|
||||
|
||||
In 2015 Apple unveiled the iPad Pro lineup along with the Apple Pencil in an attempt to cater to creative professionals and enthusiasts. To this day, this paid off to a certain extend. The iPad has become very popular with a lot of digital artists—who've over time gotten very fed up with the shaky reliability of dedicated drawing tablets from the likes of Wacom.
|
||||
|
||||
Then, in 2017, Apple launched the 2nd generation iPad Pro line and revitalized the regular iPad line for a 5th generation (putting the iPad Air line on hold). Alongside the launch of the new hardware, they also ran the "What's a Computer?" ad campaign, in which they suggested that the iPad can serve as a replacement for most computers/laptops on the market at that time. A 6th generation refresh of the regular iPad came in March 2018, followed by the 3rd generation of the iPad Pro in November 2018, along with a 2nd generation of the Apple Pencil.
|
||||
|
||||
## …how it's going
|
||||
|
||||
In March 2019 Apple revitalized the iPad Air lineup with a 3rd generation and a 5th generation of iPad mini. That meant Apple now offered five (!) different variants of the same product line:
|
||||
|
||||
- iPad (6th gen)
|
||||
- iPad Air (3th gen)
|
||||
- iPad mini (5th gen)
|
||||
- iPad Pro 11" (1st gen)[^ipadprogen]
|
||||
- iPad Pro 12.9" (3rd gen)
|
||||
|
||||
What made things more confusing is that Apple went ahead and put the M1 of their ARM-based 2020 MacBook lineup into the 5th generation of iPad Pros in 2021. which muddied the waters even more in terms of product strategy. Now the iPad Pro was as capable as their lauded M-chip-based MacBooks but there was one glaring issue: they were still iPads with iPad software running on them. So putting the M1 in the iPad Pro wasn't much more than a weird flex, since the software running on the iPad was still very much designed with iPad idiosyncrasies in mind[^ipadbabies].
|
||||
|
||||
## Weird flex, but okay
|
||||
|
||||
Multiple reviewers were quick to point out that the iPad Pro's hardware is held back by its software. Only a handful of apps really supported external screens and iPadOS doesn't allow for multiple apps on different screens, like you would expect from a device that's being touted as a laptop replacement. Multiple productivity apps were and still aren't available for the iPad and then again it's often just watered down versions that can't compete with their desktop counterparts.
|
||||
|
||||
[Caitlin McGarry at Gizmodo](https://gizmodo.com/the-ipad-pro-is-as-powerful-as-it-can-be-now-what-1846914479):
|
||||
|
||||
> The iPad's hardware is a non-issue at this point. Apple's tablet gets better with every iteration, and the M1 iPad with miniLED display is truly impressive. There are no other tablets that can compare.
|
||||
>
|
||||
> But the iPad Pro isn't competing against other tablets. It's competing against the Mac. And though the iPad is very, very capable, its software often feels hamstrung compared to the Mac's.
|
||||
|
||||
[Scott Stein at CNET](https://www.cnet.com/news/apple-ipad-pro-review-m1-plus-5g-make-for-an-almost-perfect-tablet):
|
||||
|
||||
> Monitor support is a big example. The iPad Pro can only use an external monitor for apps that choose to support it, which is limited now to some games, video-editing tools... and that's mostly it. It doesn't extend your iPad to a second desktop area, or allow multiple apps on different screens. This is what you'd expect monitor support on an M1-equipped iPad would add, and yet here we are.
|
||||
|
||||
[Samuel Axon at Ars Technica](https://arstechnica.com/gadgets/2021/05/2021-ipad-pro-review-more-of-the-same-but-way-way-faster-thanks-to-m1/):
|
||||
|
||||
> If you're willing to stay within the lane that iPadOS and its apps provide, the iPad can be a delight to use. The software represents a fresh start for computer operating systems compared to legacy-laden desktop OSes like Windows and macOS, and it's loaded with cool voice control, handwriting recognition, and multitasking features that can make it feel downright futuristic.
|
||||
>
|
||||
> But beyond the operating system itself, iPadOS struggles to stand toe to toe with similarly priced laptops. That shortcoming is primarily because most of the really powerful creative applications—from Adobe Premiere to Autodesk Maya to Unity to Apple's own Final Cut or Xcode—don't have robust iPad versions.
|
||||
|
||||
Apple tried to address these criticisms with iPadOS 16's Stage Manager, an app switcher for when you use your iPad with an external monitor. But reviewers were equally unimpressed, as it was still lagging behind the window management capabilities of a full-fledged desktop OS.
|
||||
|
||||
[David Pierce at The Verge](https://www.theverge.com/23420280/ipados-16-stage-manager-review)
|
||||
|
||||
> Stage Manager, as a concept, makes sense on a Mac because it adds some structure to the free-form system, letting you quickly collect your mess. In that way, it reminds me of the Mac’s desktop Stacks feature, which automatically creates folders for different file types on your desktop. It’s a simple way to rein in the chaos. On the iPad, though, Stage Manager is just more and different structure on top of all the existing structure. And all that structure just turns back into chaos.
|
||||
|
||||
A year later with iPadOS 17, things improved a little bit, but it was still a long shot from actual window managing power users were used to from a desktop OS.
|
||||
|
||||
[David Pierce again](https://www.theverge.com/23787477/apple-ipados-17-stage-manager-ipad-multitasking):
|
||||
|
||||
> Really, my biggest remaining gripe with Stage Manager is the same one as last year: it has nothing to do with the rest of the iPad’s software. You can’t Command-Tab through spaces, spaces don’t show up in your dock, and you can’t save a space in any meaningful way. As ever, the app switcher is the only useful way to navigate your spaces. If you’re going to use Stage Manager, my recommendation is simple: make five spaces and stick with them. If you find yourself needing more than you can see on the screen at once, Stage Manager isn’t for you.
|
||||
|
||||
As of 2024, the iPad Pro is still not a suitable laptop replacement.
|
||||
|
||||
[Tony Polanco at tom's guide](https://www.tomsguide.com/tablets/ipads/i-tried-using-the-ipad-pro-2024-as-a-laptop-for-a-week-it-went-exactly-as-expected)
|
||||
|
||||
> For all its strengths, the iPad Pro still doesn’t make a great laptop replacement. Not only is iPadOS not good for laptop-like productivity, but you can get a real laptop like the MacBook Air M2 for hundreds of dollars less.
|
||||
|
||||
Which brings me to my next point in Apple's confused iPad lineup.
|
||||
|
||||
## Upsell me if you can
|
||||
|
||||
When the base-model iPad launched in its 10th generation in 2022, reviews were mixed.
|
||||
|
||||
There certainly were some notable improvements, like the switch to USB-C, the general chip upgrades and applying the new hardware design to the chassis, making it buttonless and moving the biometric sensor to the power button up top. But this was accompanied by the removal of the headphone jack, a lack of support for the 2nd generation Apple Pencil and an increased price compared to models of years prior ($329 9th gen vs. $449 10th gen).
|
||||
|
||||
Especially the situation with the Apple Pencil was hilariously misguided, since the 1st generation Apple Pencil was still using Lightning and the 10th generation iPad switched to USB-C (the fact its design was begging for the thing to snap off if you weren't careful notwithstanding). This necessitated a dongle, which you'd plug into a USB-C cable, which plugged into the iPad.
|
||||
|
||||
Making things more awkward was the keyboard situation. Despite the 10th generation base-model iPad sharing the design of its siblings, it was just ever so slightly sized differently that it couldn't share any accessories with them. Apple would rather sell you an entirely separate variant of its Magic Keyboard Folio for an eye-watering $249 on top of the iPad's already increased price.
|
||||
|
||||
Adding that all up:
|
||||
|
||||
| Item | Price |
|
||||
| -------------------- | -------: |
|
||||
| iPad 10th gen | $449 |
|
||||
| Magic Keyboard Folio | $249 |
|
||||
| Apple Pencil | $99 |
|
||||
| **Total** | **$797** |
|
||||
|
||||
Now you could argue that you neither need the keyboard nor the pencil. And you'd be right. You can still connect any old Bluetooth keyboard to it and be good to go. The point is to illustrate how Apple's promise about the iPad replacing your laptop runs you as steep a price as an iPad Air with 256 GB of storage at $749[^ipadairstor]. What makes it all appear even more hilariously out of touch is that the same Magic Keyboard Folio for the iPad Air used to only cost you $179[^ipadairfolio].
|
||||
|
||||
But just for the sake of argument, let's do the calculation above again but with the iPad Air in mind:
|
||||
|
||||
| Item | Price |
|
||||
| --------------------------------- | --------: |
|
||||
| iPad Air 5th gen | $749 |
|
||||
| Magic Keyboard Folio | $179 |
|
||||
| Apple Pencil 2nd gen[^ipadairpen] | $129 |
|
||||
| **Total** | **$1057** |
|
||||
|
||||
We've now approached the price of a 5th generation iPad Pro 12.9" (128 GB base storage, $1,099). Add another $100 and you could get an M2 MacBook Air. As in, like, an *actual* laptop. To do actual productive things with you'd have gotten a laptop for in the first place.
|
||||
|
||||
Of particular note in that regard is that Apple has gone out of their way to also make choosing the right Apple Pencil for your chosen iPad (or vice-versa) absolute misery as well. Specifically making the 2nd generation Apple Pencil, compatible with the 3rd to 6th generation iPad Pro, incompatible with with the 7th generation iPad Pro, in favor of the Apple Pencil Pro. So, if you're upgrading from any of those generations to the latest iPad Pro, you can't use your $149 fancy pen anymore, even if it still works just fine.
|
||||
|
||||
Worse still, the specs of the newly released M4 chip inside depend on you *storage configuration* at the time of purchase and iPadOS is still a struggle for productivity use and a laptop replacement, as noted by reviews.
|
||||
|
||||
[Brenda Stolyar at Wired](https://www.wired.com/review/apple-ipad-pro-m4-2024/):
|
||||
|
||||
> The iPad Pro feels unfinished. With no evidence of what exactly makes the M4 chip all that revolutionary just yet, it's tough to recommend right now—especially for the price. The most expensive iPad Pro configuration (13-inch with nano-texture glass, 2 TB of storage, and cellular connectivity) and the latest accessories (Magic Keyboard Case and Apple Pencil Pro) come out to a whopping $3,077. That's only slightly less than a 14-inch MacBook Pro with an M3 Max chip (1 TB of storage), which starts at $3,199.
|
||||
|
||||
You notice the pattern here? In an effort to sell you the iPad as a better and more capable laptop replacement, they also need to sell you on the accessories. And it's these accessories that bump up the price of the proposed laptop replacement to that of the actual laptops in Apple's lineup. Why would you go with an iPad the price of a laptop, when that money would actually get you a laptop?
|
||||
|
||||
## All that power and nowhere to put it… unless…
|
||||
|
||||
Apple remains the undisputed leader of the tablet market with the iPad. They don't really need to keep stuffing more and more processing power into it. As far as I'm concerned, they could just bring out a new iPad every two years and they would still run circles around every other competitor[^androidmeh].
|
||||
|
||||
But I don't think that's why they're doing it. As mentioned in the beginning, the iPad mini has been equipped with an A17 Pro, the same chip as in the iPhone 15 Pro. If you've been keeping up with news in the IT space lately, you've probably noticed a very specific trend getting mentioned quite often: "AI"
|
||||
|
||||

|
||||
|
||||
Apple has been very hesitant to jump on this bandwagon, but in the end, they are also following Silicon Valley's Hail Mary moment and calling it "Apple Intelligence". The fact is that "AI" calculations are very computationally intensive and require special compute units (NPUs) in processors to be able to come up with results in a reasonable amount of time without draining the battery.
|
||||
|
||||
And this is probably also the crux of why Apple has given a device like the iPad mini such a powerful chip. Only two of Apple's A-series chips are equipped with an NPU to perform such "AI" calculations: the A17 Pro and A18 Pro. The other Apple chips sporting these specialized compute units are the M-series. So to not further muddle the spec sheets between iPad models, Apple couldn't *also* put an M-series chip inside the iPad mini as well and ridiculously out-spec that one as well. That only leaves out the base-model iPad from being able to make use of Apple Intelligence.
|
||||
|
||||
But if the 7th generation iPad mini is any indication, I'm expecting that to change real soon.
|
||||
|
||||
Big Tech is hell-bent on making this whole "AI" tomfoolery work out for them. And for Apple this now also means, going forward, releasing devices that can run their flavor of pixel-guesstimation and word-salad-parrots, always one wrong prompt away from spouting absolute nonsense at you as answers to 3rd grade questions, is now a core business strategy.
|
||||
|
||||
As is often the case, any single company dominating a market segment with their product makes for some real questionable choices in the iterations of said product over time. Apple seems outright clueless and without any sense of direction of where to take the iPad lineup in general, since they have absolutely no competition to fight off. The only "competition" is the Mac lineup, and even then, all the iPad lineup does is make people wish they would've gone for a real computer instead.
|
||||
|
||||
Having been the top-dog in tablets for years (decades?) have made Apple become complacent and so they do what any company does when they run out of ideas and can basically do whatever: adding bullshit nobody asked for and changing stuff around for the sake of changing stuff around.
|
||||
|
||||
The iPad, as a product line, is poised to repeat [the same mistake](https://en.wikipedia.org/wiki/Apple_Inc.#1990%E2%80%931997:_Decline_and_restructuring) Apple made in the 90s, when they were in dire straits because their Mac lineup became a confusing mess to navigate. Too many choices with little to no real significance, alienating customers on which model to buy.
|
||||
|
||||
Customers trying to make sense of which iPad best serves their needs or why they should bother with any iPad in the first place face the same confusing, alienating product line today.
|
||||
|
||||
[^ipadprogen]: Yes, Apple counts the iPad Pro 11" generations differently, being preceded by the iPad Pro 9.7" and iPad Pro 10.5" before it (of course, also each of those separately, to make it even more confusing).
|
||||
[^ipadbabies]: Or, as senior vice president of software engineering [Craig Federighi himself](https://www.theverge.com/2021/5/20/22444471/epic-apple-fortnite-antitrust-trial-craig-federighi-ios-security) puts it: "With iOS, we were able to create something where children — heck, even infants — can operate an iOS device, and be safe in doing so."
|
||||
[^ipadairstor]: 64 GB of base storage was laughably small, even in 2022, especially with how atrocious storage management is on iOS/iPadOS. If you only meant to do light stuff on it you'd go for the base-model iPad without any accessories anyways and take the savings over niceness of a fancier slab of glass.
|
||||
[^ipadairfolio]: Apple seems to have since discontinued the Folio for the iPad Air for the more expensive magnetically attached Magic Keyboard.
|
||||
[^ipadairpen]: The iPad Air is incompatible with the 1st generation Apple Pencil, so you *have* to go for the more expensive 2nd generation.
|
||||
[^androidmeh]: Especially with how "meh" Android tablets are, Apple isn't really facing the stiffest competition here, either.
|
326
src/posts/2024-10-26_opengraph-data.md
Normal file
326
src/posts/2024-10-26_opengraph-data.md
Normal file
|
@ -0,0 +1,326 @@
|
|||
---
|
||||
title: Open Graph Metadata and Images in Eleventy Made Easy
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/28d944cf-3630-4db8-bc4e-469769c83d00
|
||||
alt: The Open Graph protocol logo surrounded by the logos of Twitter, Mastodon, Telegram, Discord and the Fediverse
|
||||
credit: Made with GIMP, logos © their respective owners
|
||||
tags: ["eleventy"]
|
||||
---
|
||||
|
||||
Blog posts are meant to be shared. When sharing links, you'll often see a preview on social media and instant messengers. But how does it work and how can you do it with Eleventy?
|
||||
|
||||
<!-- more -->
|
||||
|
||||
The metadata that makes share previews on social media and messengers work is called the [Open Graph protocol](https://ogp.me/). Originally conceived by Facebook to map relationships between people, it is now used primarily on social platforms to generate an eye-catching preview of a shared website that contains an image, a URL, a title and a short teaser of the content.
|
||||
|
||||
Open Graph metadata is basically just a bunch of `<meta>` tags in your site's `<head>` with attributes the social platforms recognize and know how to deal with. Now, you *could* write them all by hand, but that's tedious and pretty not fun. Luckily, there's plugins for Eleventy available that automate this and make it both easy to implement and highly customizable.
|
||||
|
||||
## Picture-Perfect
|
||||
|
||||
The plugin that takes care of generating preview images is the [Eleventy Open Graph image plugin](https://www.npmjs.com/package/eleventy-plugin-og-image). It's based on Vercel's [Satori](https://github.com/vercel/satori). It takes a file with a dedicated name to generate any kind of preview image from the HTML and CSS markup that you author.
|
||||
|
||||
By default, the Open Graph image plugin looks for a file with `*.og.*` in its file name. I'm using Nunjucks for my examples, because that's the template language I started out with and because it allows me to use variables to make the template more dynamic.
|
||||
|
||||
Then proceed to adding the plugin to Eleventy in its `eleventy.config.js`:
|
||||
|
||||
```js
|
||||
import fs from 'node:fs';
|
||||
import EleventyPluginOgImage from 'eleventy-plugin-og-image';
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(EleventyPluginOgImage, {
|
||||
satoriOptions: {
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fs.readFileSync('../path/to/font-file/inter.woff'),
|
||||
weight: 700,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
},
|
||||
width: 3000,
|
||||
height: 2000
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The Open Graph image plugin allows you to define some options for the `width` and `height` of the preview image canvas (default: 1200 × 630). Additionally, you can pass Satori some font files in the `satoriOptions` object to make them available in the preview image. These need to be either TrueType (`*.ttf`) or Web Open Font Format (`*.woff`). WOFF2 is **not** supported! The fonts you can download from [Google Fonts](https://fonts.google.com/) usually are TrueType, even the variable fonts. So go nuts!
|
||||
|
||||
Start by creating an `og-image.og.njk` file in the source directory of your Eleventy project (I'm using `./src/`). In here you can build your preview with just HTML and CSS. However, Satori only supports a limited number of HTML elements and CSS properties. Refer to [Satori's GitHub](https://github.com/vercel/satori#html-elements) for a list of HTML elements it supports, as well as [Yoga's documentation](https://www.yogalayout.dev/) on the CSS properties that are supported.
|
||||
|
||||
Since the markup and styling is converted to SVG and then to images, it really doesn't matter if you use semantic HTML or not. Satori is perfectly happy to convert a bunch of `<div>` elements.
|
||||
|
||||
The Yoga layout engine is heavily based around [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) and it will likely shout at you if you forget to use `display: flex` on any of the elements in your markup. Make sure to keep an eye out for error messages in your terminal in case the preview stops updating. For example, Yoga does **not** like pseudo-elements and will stop updating previews if it detects them.
|
||||
|
||||
If you want a live preview of what Satori does to your preview and if it converts properly, you can do so by simply opening the preview HTML files it generates during live serving your Eleventy site. The Open Graph image plugin puts all the previews inside an `og-images` directory in your Eleventy output folder, e.g. if your output directory is located at `./public/` you'll find the previews in `./public/og-images/preview/`. It mirrors the same file structure as your site. Whenever you update your `og-image.og.njk` template it will also update the preview in your browser with `browsersync` included in Eleventy. This way you can use the browser dev tools to adjust things to your liking and copy it back to your `og-image.og.njk` template (just like old days!), then save and update the preview to verify Satori converts the template correctly.
|
||||
|
||||
It's recommended to put your styles in a `<style>` block inside your `og-image.og.njk` for easier readability and better maintainability. You can use IDs or class names to apply styles. Some examples around Satori put it in the `style` *attribute* but this gets quite messy and hard to maintain after just a couple of CSS properties. Using HTML style attributes is best reserved for something that changes based on the content, like a background image linked in the blog post.
|
||||
|
||||
It might take some time to get used to how Satori needs you to write your HTML and CSS exactly, but it's not as bad as it might sound. My recommendation is to start with really simple, basic HTML and CSS, save often and go a little more old-school about the authoring process (especially making use of `width` and `height` CSS properties in more complex layouts so elements actualy span the whole area you're trying to fill).
|
||||
|
||||
Here's the template I use to generate my Open Graph images:
|
||||
|
||||
```twig
|
||||
<style>
|
||||
#top {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#main {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#heading {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex: 1 0 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
padding: 64px 112px;
|
||||
font-size: 72px;
|
||||
text-wrap: balance;
|
||||
background-color: rgb(15 23 42 / 0.65);
|
||||
}
|
||||
|
||||
#footer {
|
||||
flex: 1 0 96px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
background-color: rgb(8 47 73);
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer #avatar {
|
||||
flex: 0 0 64px;
|
||||
display: flex;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: 100% 100%;
|
||||
border: 4px solid currentColor;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#footer #meta {
|
||||
flex: 1 0 0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
{% raw %}
|
||||
<div id="top">
|
||||
<div id="main">
|
||||
<div id="heading">
|
||||
{% if image and image.src != '' %}
|
||||
<img id="background" src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
{% endif %}
|
||||
<h1>{{ title | safe }}</h1>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<div id="avatar" style="{% if author and author.image != '' %} background-image: url({{ author.image }}){% endif %}"></div>
|
||||
<div id="meta">
|
||||
<p id="author">by {{ author.name }}</p>
|
||||
<p id="blog">blog.sebin-nyshkim.net</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
If you need to refer to some data inside your Open Graph image template, you have to explicitly pass it when using the `ogImage` shortcode in your base layout template, as the plugin does not pull these automatically from the data cascade! Stuff like page title, author, site address, image URLs, etc.
|
||||
|
||||
You first declare the name of the variable you want to make available in the Open Graph image template and the data you want to pass down to it:
|
||||
|
||||
```twig
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% raw %}{% ogImage "og-image.og.njk", { title: pageTitle, author: pageAuthor, image: heroImage } %}{% endraw %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- site content here -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
This makes the data of `pageTitle`, `pageAuthor` and `heroImage` available inside the template `og-image.og.njk` under the variable names `title`, `author` and `image` respectively.
|
||||
|
||||
## Getting Meta
|
||||
|
||||
The Open Graph image is just one piece of the puzzle when it comes to making your previews stand out. Sites and apps processing your site also need a little more info to generate meaningful previews.
|
||||
|
||||
This is where another Eleventy plugin comes in, called [Metagen](https://www.npmjs.com/package/eleventy-plugin-metagen). It allows you to add a bunch of `<meta>` tags to the `<head>` section of your layouts via the `metagen` shortcode. For a list of supported options refer to the [Metagen docs](https://metagendocs.netlify.app/docs/intro/).
|
||||
|
||||
The `metagen` shortcode takes values as either literals or dynamically, passed via Eleventy's [data cascade](https://www.11ty.dev/docs/data-cascade/). This is useful if you have data defined in [data files](https://www.11ty.dev/docs/data-template-dir/) or [front matter](https://www.11ty.dev/docs/data-frontmatter/) data somewhere along the cascade and want every page to have Open Graph metadata specific to its content. This means you can have something like this in your base layout:
|
||||
|
||||
```twig
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% raw %}{% metagen
|
||||
title = title + ' - Sebin\'s Blog',
|
||||
desc = description,
|
||||
url = 'https://blog.sebin-nyshkim.net' + page.url,
|
||||
type = type,
|
||||
site_name = 'Sebin\'s Blog',
|
||||
og_image_width = image.width,
|
||||
og_image_height = image.height,
|
||||
og_image_alt = image.alt,
|
||||
og_image_type = image.type,
|
||||
twitter_card_type = twitter.cardType,
|
||||
twitter_handle = twitter.account,
|
||||
name = author.name,
|
||||
generator = 'eleventy',
|
||||
preconnect = ['https://cdn.sebin-nyshkim.net'],
|
||||
dns_prefetch = ['https://cdn.sebin-nyshkim.net'],
|
||||
css = ['/fonts/tilt-warp/tilt-warp.css',
|
||||
'/fonts/encode-sans/encode-sans.css',
|
||||
'/fonts/m-plus-1-code/m-plus-1-code.css',
|
||||
'/css/style.css',
|
||||
'/css/prism.css']
|
||||
%}
|
||||
{% ogImage "og-image.og.njk", { title: title, author: author, image: image } %}{% endraw %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- page content here -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Have a data file in a sub-directory somewhere:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": "blogpost.njk",
|
||||
"permalink": "/posts/{{ title | slugify }}/",
|
||||
"date": "git Created",
|
||||
"type": "article",
|
||||
"author": {
|
||||
"name": "Sebin Nyshkim",
|
||||
"href": "https://blog.sebin-nyshkim.net",
|
||||
"image": "https://blog.sebin-nyshkim.net/img/sebin.png"
|
||||
},
|
||||
"twitter": {
|
||||
"cardType": "summary_large_image",
|
||||
"account": "SebinNyshkim"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
And have front matter that looks like this:
|
||||
|
||||
```md
|
||||
---
|
||||
title: Responsive, Self-hosted Images for Your Eleventy Blog
|
||||
description: While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/f33879d8-3f98-4c8e-a1b3-08edf6f174ac.jpg
|
||||
alt: Close-up of SVG code on a computer screen
|
||||
width: 1200
|
||||
height: 630
|
||||
type: 'image/png'
|
||||
tags: ["self-hosting", "docker", "eleventy"]
|
||||
---
|
||||
|
||||
## Getting visual
|
||||
|
||||
Images make articles a lot prettier to look at, don't you think?
|
||||
```
|
||||
|
||||
This results in output that looks like this:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog</title>
|
||||
<meta name="title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
|
||||
<link rel="preconnect" href="https://cdn.sebin-nyshkim.net">
|
||||
<link rel="dns-prefetch" href="https://cdn.sebin-nyshkim.net">
|
||||
<meta name="author" content="Sebin Nyshkim">
|
||||
<meta name="description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
|
||||
<meta name="generator" content="eleventy">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
|
||||
<meta property="og:site_name" content="Sebin's Blog">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
|
||||
<meta property="og:description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
|
||||
<meta property="og:image:alt" content="Close-up of SVG code on a computer screen">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@SebinNyshkim">
|
||||
<meta name="twitter:creator" content="@SebinNyshkim">
|
||||
<meta name="twitter:url" content="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
|
||||
<meta name="twitter:title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
|
||||
<meta name="twitter:description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
|
||||
<link rel="canonical" href="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
|
||||
<link rel="stylesheet" href="/fonts/tilt-warp/tilt-warp.css">
|
||||
<link rel="stylesheet" href="/fonts/encode-sans/encode-sans.css">
|
||||
<link rel="stylesheet" href="/fonts/m-plus-1-code/m-plus-1-code.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/prism.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- page content here -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Metagen will take care of filling in as much relevant metadata as you provide it with. Setting a `title`, `url` or `desc` option in Metagen will set the generic `<meta>` tags, as well as all the relevant `og:` and `twitter:` tags. If you set the more specific `og_*` or `twitter_*` Metagen options, they will take priority over the more general ones.
|
||||
|
||||
If any of the values passed to the shortcode options come up empty, i.e. there is no `image.alt`, `image.width`, `image.height` or `image.type` metadata anywhere along the data cascade, the tags will be left out.
|
||||
|
||||
You do not need to set the `og_image` or `twitter_image` options in Metagen. These will be provided by the Open Graph image generator plugin already.
|
||||
|
||||
## Gettin' the Look
|
||||
|
||||
If you want to verify that all went according to plan, there a few ways to achieve that.
|
||||
|
||||
I like to use a little tool on Linux called [Share Preview](https://apps.gnome.org/SharePreview/) by [Rafael Mardojai CM](https://mardojai.com/). It will mimic how your site's preview will look like when posted on social media. It even works with `localhost` URLs! It's available from [Flathub](https://flathub.org/apps/com.rafaelmardojai.SharePreview).
|
||||
|
||||
The other method is using one of the [couple dozen websites](https://duckduckgo.com/?q=sharing+preview+tester) that will give you the same functionality, and maybe provide some insights and tips on how to optimize them for maximum sharability (if that's your thing).
|
||||
|
||||
And that's about it! Now go and make your site previews awesome! 😎
|
27
src/posts/2024-11-14_twitter-exit.md
Normal file
27
src/posts/2024-11-14_twitter-exit.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: Screw the Bird
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/b884ca38-6d3c-4ecc-940c-a4137fa79cba
|
||||
alt: Person throwing the blue Twitter logo into a trash can
|
||||
credit: Made with GIMP
|
||||
tags: ["social media"]
|
||||
---
|
||||
|
||||
Today is the day. I've been mulling over whether I should keep it around for good ol' times or to reserve the handle, but I've ultimately decided it's time to call it quits and no longer maintain a presence. Today I'm quitting Twitter, full stop.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
At first, I thought about just locking the account and leaving it to collect dust. That way, at least nobody could steal the handle and impersonate me. Then I was reminded that Twitter would start using all content on their platform for "AI" training [starting November 15](https://www.pcmag.com/news/xs-new-rules-blocked-posts-will-no-longer-be-hidden-tweets-will-train-ai). It's unclear if this means the opt-out option will be removed by then, but I'm not waiting to find out. Of course, I could have just removed all media from my account, but this thing is over 10+ years old. It would take me hours to hand pick every post I wouldn't want to get slurped up. In the end, I decided, no, this isn't worth it, and I went with the nuclear option.
|
||||
|
||||
The writing had been on the wall for quite some time now, ever since Musk took over in 2022. To say the past two years were messy would be an understatement. And there's no sign it's going to get better. At first, I leaned hard into pushing for Mastodon, it seemed like the right thing to do, since it felt like such a breath of fresh air. The open source social network that's community owned! What I didn't realize in my fervor is that while it's a great idea in theory, the execution is alienating to most other people who aren't IT nerds. And even when people could get past the confusing parts of signing up (made confusing by a lot of bickering between Mastodon users with what instance to sign up in the first place) and the implications of choosing an instance therein, people don't enjoy their time on there[^mastodon].
|
||||
|
||||
Meanwhile, Bluesky has been [taking off considerably](https://www.theverge.com/2024/11/13/24295484/bluesky-15-million-users-social-media-x-musk), with many of my fellow furry friends finally migrating away from Twitter. It gives me some hope that we're finally at a tipping point where we can collectively move on. It will take some time to rebuild and undo the collective damage that 10+ years of Twitter have caused. From my personal experience, though, things are looking up and the Bluesky team has been adding more and more features people are asking for and is very deliberate in also adding anti-harassment features (i.e. unlinking quote posts to curb quote dunking and dog-piling).
|
||||
|
||||
To be honest, the most refreshing thing about all of this is the atmosphere of optimism and rediscovery. People sharing tips to get the most out of Bluesky[^bluesky] and rediscovering a sense of community that I feel was lost in the never-ending pursuit of Twitter's numbers game and how to best game the dreaded algorithm.
|
||||
|
||||
The algorithmic web has been the cause of nothing but suffering for artists. They're the ones who make these online spaces worth our time to begin with! And I'll be damned if I'll continue to maintain a presence on a platform that wants to steal from those artists to pump more "AI" generated sludge onto the web to fuel a senseless doomscrolling frenzy that only benefits the platform.
|
||||
|
||||
So here's to new frontiers beneath bluer skies! 🦋
|
||||
|
||||
[^mastodon]: Specifically, continously hassling new people by telling them how they're "using Mastodon wrong" (missing alt text, CWs, hashtags), the constant air of superiority towards any other platform and belittling people just trying to figure it out for themselves certainly doesn't help make the place any more welcoming. But mention these interpersonal shortcomings on there and you get angry Mastodon die-hards in your mentions accusing you of trying to act like a "HoA", which is very ironic, given how they show very little self-restraint or self-reflection in telling other people off for their use of the platform. So after several failed attempts to get people on board and experiencing the exclusive nature first-hand, I decided after some introspection that this is not a platform or community I wanted to continue advocating for. Imagine wanting people to join your supposedly superior platform so badly, yet when they do they’re met with nothing but spite and everyone gets all surprised and huffy if those people would rather hang out anywhere else. You know you screwed up if people start saying stuff like "The Mastodon of XYZ" in a derogatory way.
|
||||
[^bluesky]: Ironically, I've seen people use alt text for image posts on Bluesky more frequently. The vibe is very much educational, emphasizing the benefits of providing alt text (beyond accesibility, alt text on Bluesky is searchable) rather than pointing out individual failures of not having it. Only NSFW labels on posts are expressly requested, and even then they are directed at the general crowd instead of picking out individuals.
|
165
src/posts/2024-12-08_mastodon-vs-bluesky.md
Normal file
165
src/posts/2024-12-08_mastodon-vs-bluesky.md
Normal file
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
title: Why Bluesky is running circles around Mastodon
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/4cc7b20e-6a50-4674-af98-2db254c7737e
|
||||
alt: An upset Mastodon logo next to a group of people talking, with one person jumping out from the group with a Bluesky logo on its head giving the Mastodon logo a big, indifferent thumbs up
|
||||
credit: Made with GIMP
|
||||
tags: ["mastodon", "fediverse"]
|
||||
---
|
||||
|
||||
Recently, the question of why Bluesky was gaining significantly more traction than Mastodon from yet another Twitter exodus came up again. I used to ask myself the same question, so I went digging.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
That question usually gets asked in tech-centric circles where nobody can meaningfully answer or it's agreed upon rather hand-wavingly that it must be *the usual reasons.*
|
||||
|
||||
So I took it upon myself to ask the question on, well, [Bluesky](https://bsky.app/profile/sebin-nyshkim.net/post/3lbzac2x5oc2l). I got about the responses I thought I would:
|
||||
|
||||
* Didn't grasp the federation concept
|
||||
* Onboarding isn't a smooth experience
|
||||
* Finding an instance to one's needs is a chore
|
||||
* Signing up for the "wrong" instance isolates you from your peers
|
||||
* Lack of content
|
||||
* Lack of familiar people
|
||||
* Mostly just IT related discussions
|
||||
* Difficulties to connect to other interest groups when communities don't overlap
|
||||
* Little to no traction on posts
|
||||
|
||||
Mastodon[^mastosimple] is a different beast from all the other social medias out there, so there is a learning curve involved. That learning curve involves navigating a confusing maze of technical terms and feature pitches that miss the mark in terms of relevance to the average internet browsing people.
|
||||
|
||||
[^mastosimple]: I use Mastodon as a short-hand for simplicity to mean the wider microblogging Fediverse software cosmos, e.g. Pleroma, Iceshrimp, Misskey, etc.
|
||||
|
||||
It's a bit of a chicken and egg problem. Not many non-tech people sign up for Mastodon, so there's not much actionable feedback from them on how to make it more approachable to most people, hence the platform is largely steered in a direction the tech-savvy people want it to go.
|
||||
|
||||
And that would be totally fine, if it weren't for these Mastodon users that keep asking that question as if to imply Mastodon is the *inherently* better choice.
|
||||
|
||||
Because it isn't. Far from it.
|
||||
|
||||
## Choice Paralysis
|
||||
|
||||
You see, when Musk bought Twitter, it was the perfect opportunity for Mastodon to get people on board. Mastodon was the only somewhat established alternative around, so people went to check that one out.
|
||||
|
||||
What they found was something very different from what they were used to. The biggest hurdle was (and still seems to be) where to even sign up. Mastodon users kept reiterating: "Just sign up anywhere, it doesn't really matter."
|
||||
|
||||
Except, it does.
|
||||
|
||||
To prevent users from harm and harassment, instance admins often outright block or mute the [mastodon.social](https://mastodon.social/) instance over concerns of its lackluster moderation. When an instance is blocked (or "defederated"), no cross-instance communication of any kind can take place. When an instance is only muted, communication across instances is still possible, albeit in a more shielded way, i.e. follows from the instance in question will always have to be approved by users, replies are not shown in the notifications by default and search results do not include posts and user accounts from that instance.
|
||||
|
||||
That means if some of your friends happen to sign up with that instance because they didn't know any better, you will never really know they're even around. That's a decision that is made for you and there's not much recourse except making a new account on another instance, that doesn't block the instance you want to be able to communicate with. That entails reading over the blocklist of every instance you might wanna sign up with, but then it will probably block others and the whole dilemma starts over. That is *a hell of a lot* to ask of someone who doesn't care about any of this and just wants the hell off of Twitter.
|
||||
|
||||
You also mustn't sign up for big instances. Mastodon users will want you to sign up with smaller instances, so the overall network becomes more resilient in case one of them blows up. The point is warranted, because it's not the first time a big instance with thousands of users has gone offline for good (more on that later). They'll have you know federation will make it all work, as if it's a topic that comes up in every-day conversation and is inherently understood.
|
||||
|
||||
The benefits of Mastodon's federated approach are not as immediately apparent as its users think. The often cited email analogy also certainly didn't help, as most people do not use email for active communication anymore. What email is to most people nowadays, is an inbox full of unread messages they'll just let pile up. Saying "Mastodon is like email" does make sense *technically*[^mastoemailtech], but it doesn't evoke the same mutual understanding about the overall network that Mastodon users think it does. It all goes back to how Mastodon's user base is heavily tech-savvy and they care a lot about conveying the basics of it in a way that satisfies other tech-savvy people. It's a disservice to Mastodon and causes more miscomprehensions than it solves[^mastoemail].
|
||||
|
||||
[^mastoemailtech]: Mastodon (or rather, [the Fediverse](https://en.wikipedia.org/wiki/Fediverse) and its underlying protocol [ActivityPub](https://en.wikipedia.org/wiki/ActivityPub)) is built not unlike email in the way messages between instances are exchanged under the hood, hence the email analogy checks out on a technical level.
|
||||
|
||||
[^mastoemail]: A better way to describe this to non-tech people would probably be how single-sign on with Google/Facebook/Twitter works. It's one account and it allows you to be signed into one service to access multiple ones. Just without the privacy implications. Or hell, call it "inverse Discord", one account, all the servers. Anything is better than using email as an analogy because you let yourself get hung up by nerdy semantics!
|
||||
|
||||
Then there's the app situation. Mastodon being open source software means an ecosystem can easily form around it to make it usable in native apps for any platform imaginable. Independent apps for Mastodon were available long before an official Mastodon app was [launched in 2021](https://www.theverge.com/2021/7/30/22602275/mastodon-decentralized-social-network-official-ios-app-launches) on the iOS App Store and Google Play Store. The network itself has been around [since 2016](https://en.wikipedia.org/wiki/Mastodon_(social_network)). "Use any app you like," they would say. "But not the official one. That one is *bad!*"
|
||||
|
||||
Mastodon users were quick to point out, that the official app was lacking in what they felt were core features of the network. The lack of access to local and federated timelines got frequently cited as reasons to use any other app[^mastotimelines]. I remember a ton of posts that were attempting to guide new users on how to get the most out of the network whenever there was an influx. But it was also Mastodon users assuming a lot about the technical prowess of people just coming from Twitter — a site that only had one server and one app — and their tolerance to technicalities of a network that, from their perspective, still needed to prove itself fit for their needs. The apps that were recommended also were of varying quality. If they are even still maintained[^mastoapps].
|
||||
|
||||
[^mastotimelines]: I don't think I've ever really checked out the local or federated timelines for long because they're just chaotic and insanely noisy. So the argument could be made that that's the official app actually shielding new users from an onslaught of noise and potentially undesired content they're not prepared for.
|
||||
|
||||
[^mastoapps]: [Furry Fediverse](https://furryfediverse.org/) still lists [Megalodon](https://github.com/sk22/megalodon) as a Mastodon client for Android, an app that didn't have an updated release since 2023 and has been archived as of 2024. Its successor is [Moshidon](https://github.com/LucasGGamerM/moshidon). I've seen user reviews on the Google Play Store conflate Moshidon as "stolen" code from Megalodon, because of their stark visual similarities and feature set. Moshidon is a fork of Megalodon, which itself is a fork of the official Mastodon app. But these things aren't immediately apparent to non-tech people and it's unrealistic to expect that deep an understanding from them.
|
||||
|
||||
The biggest issue however is and always was the network effect. People join a social network to keep up-to-date with friends they already know and the issues I described earlier are big hurdles for people to join. And even if people joined they are left with a lot of questions that either go unanswered, the answers provided are too technical and incomprehensible or they get a backhanded comment that that's not how things work on Mastodon.
|
||||
|
||||
Now, you might say, "What's the matter? It's not that different from Twitter. You post, you boost, you like. It's pretty similar."
|
||||
|
||||
## Rules and Regulations
|
||||
|
||||
If the broader Mastodon user base would only *let* it be that easy. They frequently betray a certain ignorance of the realities of actual people's online habits and expect them to adapt to the idiosyncrasies of how the established Mastodon user base uses the service.
|
||||
|
||||
A friend of mine, who is a freelance artist, sums it up as follows:
|
||||
|
||||
> Mastodon is quite militant and aggressive in their inclusion policies. They feel like they insist on talking on behalf of those with special requirements, which often times feels a little too presumptuous for their own good, up to policing uploads with an unwieldy list of requirements to ensure absolutely every potential offence your post could possibly make can be filtered out (including but not limited to - eye contact, potential personal space violations, certain abrasive colour choices and then your more typical things like porn, kink etc.
|
||||
>
|
||||
> You are incentivised to use alt text on both platforms, but Mastodon's requirements feel like you must provide a picture essay to ensure those hard of sight can enjoy the picture too. It wouldn't be such a bad thing if all that work paid off in real interaction and engagement with said work, but it seldom ever did (for me.)
|
||||
>
|
||||
> The hashtag system in both eat into your text box, but again, Bluesky allows you to use simple tags to make sure your art gets to at least a few places you want it to be. Mastodon, on top of the exhaustive alt texting, wants tags to be filterable such that — as mentioned before — any potential offence can be avoided. There's simply far too much pre-emptive apology for my tastes.
|
||||
|
||||
Mastodon users are quick to instruct you to add alt text on attached media, without really specifying how detailed it should be. Accessibility gets cited a lot as the reason, but I've yet to see a visually impaired user come forth and complain about missing alt text. It's almost exclusively sighted users talking for visually impaired people.
|
||||
|
||||
Same with CWs (content warnings). Seemingly innocuous things are subject to self-censorship, e.g. among other things photos of food (people with eating disorders), images with people — real or drawn — making eye-contact with any potential viewer (neurodivergent people), seeking financial aid ("begging") or self-promotion (live streams).
|
||||
|
||||
Posting to Mastodon is anything but simple. Something that should come as easy as talking to someone on the street becomes a walk on eggshells with an unwieldy list of rules and regulations on what constitutes acceptable post policy.
|
||||
|
||||
If users on Mastodon want their little cove on the internet, that is totally fine. They pride themselves on the freedom of choice to connect, that is, choosing who they want connecting with them. But there's one big thing that really rubs me the wrong way about the ways they conduct themselves.
|
||||
|
||||
## Pearl-clutching Galore
|
||||
|
||||
Mastodon users can't seem to make up their mind if they want people to join or not.
|
||||
|
||||
What I mean by that is any time there's some sort of user migration and the choice doesn't fall on Mastodon, there's almost always great indignation along the lines of:
|
||||
|
||||
* "How can't they see that this will end badly for them?"
|
||||
* "Have fun in your new corporate silo…"
|
||||
* "Can't help loving corporate social media kool-aid, huh?"
|
||||
* "Bluesky is just cosplaying federation."
|
||||
|
||||
First of all, that's assuming a lot about what people want and how much they even know about all of this stuff, or even care for that matter.
|
||||
|
||||
The underlying systems and protocols are of *no importance to regular people.* At the end of the day, people want something that works and that doesn't require a comprehensive technical understanding of what makes it tick. The whole argument of whether something does "true federation" or is run by a corporation when trying to convince people to join Mastodon is irrelevant and pointless. The average Mastodon user really struggles coming to terms with the fact that most people do not care about these things because they're neither IT nerds nor privacy advocates. It just gets in the way when it should ideally be completely transparent.
|
||||
|
||||
It's not that people love corporate social media so much. It is highly likely that the reason is much simpler: People had a decision to make, chose the one that had the lowest barrier to entry and most of the people they know were headed and then stayed for the vibe.
|
||||
|
||||
The barrier to entry for Mastodon is high, nobody they know is there and if you stay long enough the vibe is extremely off.
|
||||
|
||||

|
||||
|
||||
The way anything remotely corporate is not only frowned upon, but actively despised. The way users of other platforms are constantly ridiculed and made the target of spite begs the question if the average user base on Mastodon has the emotional intelligence and self-awareness of a sponge[^mastodig]. I hate what the internet has become under the corporate leadership of pump & dump scheme Silicon Valley as much as the next guy, but to imply that people are "too brainwashed" to make a "better informed" decision so it gives the invested Mastodon user a reason to pout to make them feel better makes them look like an immature child that didn't get the birthday present it wanted. But point that out to the larger group and be expected to be tarred and feathered by an angry mob because you dared question the sanctity of Mastodon's noble principles.
|
||||
|
||||
[^mastodig]: Thought, that might be unfair to sponges.
|
||||
|
||||
Mastodon die-hards are always quick to point out that Bluesky *might* go the way of The Bird, i.e. once the VC money dries up and they feel the pressure to be making money they're going to nickel and dime their user base, "just like Twitter!"
|
||||
|
||||
That is a glasshouse argument if I ever saw one, because Mastodon instances have been shut down over [much less way quicker](https://web.archive.org/web/20230303095807/https://mastodon.lol/@jeanburgess/109837309981257160), with money not even being a factor[^mastolol]. Instance admins and moderators are all volunteers, investing their own time and money into running these things; for which they have my utmost respect. They're not the ones this addresses. The incident around mastodon.lol shows, however, that it also doesn't take much to bring down an entire instance by wearing out the admin until they go "fuck this shit, I'm out" and shut the whole thing down on short notice, because of a rabid, self-important, vigilante justice pitchfork mob.
|
||||
|
||||
[^mastolol]: *mastodon.lol* blew up over disagreement about content moderation policies with regards to allowing/removing/banning content about [a certain video game](https://en.wikipedia.org/wiki/Hogwarts_Legacy). There were slurs and other nasty recommendations of life choices being flung around by users demanding moderation (a single person) to take certain actions until that person caved under the pressure and the abuse. An entire Mastodon instance of 16,000 users. Gone. *Over content moderation disputes regarding a video game.* The boycott of which, by the way, having largely no if not the opposite effect. But now we have a Mastodon instance less to show for it, too. Good ***fucking*** job!
|
||||
|
||||
And while Mastodon users will tell you it's easy to export your data and transfer it to new instances, they consistently fail to mention that you can only restore follower lists, [not the contents](https://www.reddit.com/r/Mastodon/comments/1gosrxk/) you've been posting. You have to start over from scratch when you (have to) move, which is especially painful when you're trying to run a business or maintain a portfolio in the form of a backlog of posts[^mastoexport].
|
||||
|
||||
[^mastoexport]: "Well then they should not use social media as a replacement for a portfolio!" If you thought that, congrats, you're missing the point. Not everybody has the time, money and know-how to maintain their own separate internet presence. A social media presence might be the only thing they ever had and it worked for them. Who're you to tell them they're wrong? Artists especially have to wear so many hats already that maintaining a whole-ass website is often just not in the cards.
|
||||
|
||||
## Disenchanting Delusions
|
||||
|
||||
Apart from the technical merits of decentralization, community-owned instances and the privacy aspect it all brings, Mastodon does not, and never did, have a unique selling point to offer to most people. At best it just didn't help them achieve their goal for what they tried to use it, at worst they've had an actively bad time. The average Mastodon user's insistence that the inherent qualities of their social media network explain away its shortcomings or outweigh the perceived shortcomings of any corporate offering, for that matter, hits the same notes as the open source enthusiast's insistence that Linux and open source software are inherently better than commercial software offerings, simply by virtue of being open source — and I don't think that comes as much of a surprise when you consider *who* the majority of the user base represents. And they're in charge.
|
||||
|
||||
There's this weird tendency in FOSS circles to assume that because the mechanics and interactions of things are of interest and/or concern to *them*, they should be of interest and/or concern to *everyone*, and anyone who doesn't show interest and/or concern is an idiot who has been brainwashed in some sense. But this only ever seems to apply to products or services that involve computers.
|
||||
|
||||
*[FOSS]: Free and Open Source Software
|
||||
|
||||
I need both of these parties to understand something very basic: Given the choice between a tool that's immediately useful to achieve a certain goal, but conflicts with a person's ideologies, and a tool that's not as useful, but they agree with ideologically, the choice will almost always fall on the former.
|
||||
|
||||
You won't sell people on merits that aren't important in their day to day.
|
||||
|
||||

|
||||
|
||||
For the same reason we've still not arrived at end-to-end encryption being a standard in our daily communication, Mastodon is not the standard for social media. It offers a worse experience without offering a clear and concise reason how that is better than the thing that serves people's means of self-actualization right now.
|
||||
|
||||
People shouldn't need to understand how the electrical grid works in order to operate a light switch for the same reason they shouldn't need to understand how every last bit of their computing devices work and all the implications therein in order to have an enjoyable experience sharing photos of their pets, art and game captures. As far as most people are concerned, it is as inconsequential where they do that as it comes. The only people making a big stink of it are on Mastodon.
|
||||
|
||||
Tech being beyond most people's understanding does not reflect badly on them as a person. Belittling them for how they choose to connect to other people and express themselves, however, says more about you than it does about them.
|
||||
|
||||
Rather than Mastodon users taking a look inward and coming to terms with the fact that the way Mastodon is run is actively failing everyone else, they're huffing and puffing, making up reasons why those people are either "blind", "ignorant", "drinking the corporate kool-aid", "helpless", "careless", "clueless", "brainwashed", or what have you, and indulge in the only coping mechanism they know at this point, perpetuated by an insular, exclusionary culture. All the consternation over people on other corporate social media platforms, not realizing their allegedly obvious pitfalls, is just digging the hole deeper and living in denial. Anything is better than having to admit to one's own vitriol-flinging behavior, because that requires admitting that you're wrong and have wronged other people; a character trait sorely missing in a lot of people that spout this nonsense.
|
||||
|
||||
If Bluesky is an echo chamber ignorant of the dangers of corporate social media, Mastodon is an echo chamber full of self-righteous pricks, too full of themselves to realize the ignorance to their own behavior is putting off all the people outside of their tech- and privacy-centric bubble. The people who actually gave it a shot came to the conclusion that they don't want to deal with that smug superiority complex bullshit[^mastolinux].
|
||||
|
||||
[^mastolinux]: I've seen both "Linux is the Mastodon of operating systems" and "Mastodon is the Linux of social media" as a testimony of how the involvement of seasoned users of both made the experience actively worse for people just taking their first foray into each. Let that sink in.
|
||||
|
||||
There is a point to be made about corporate internet silos for sure, but if Mastodon users want to decry those and act like their social network's approach to breaking them up be the better solution, there better be a way implemented into the software directly for people on an instance to dispute which Fediverse instances get blocked and/or muted, because so far there is little to no recourse for them to do so, other than to approach instance admins directly. They call it moderation, I call it an overreaching, over-protective power-imbalance that pushes their ideologies and world views on everyone else in an attempt to protect people from external harm, but in doing so they have created yet another silo where people then have to put up with abuse from the inside[^mastosilo].
|
||||
|
||||
[^mastosilo]: Sure, people could again switch instances or run their own that doesn't block certain instances, but, again, that's asking people who aren't mechanics to build their own car because they've expressed discontent with what's readily available to use. Not to mention federation being so incredibly clunky that when abuse *does* happen, it's dependent on instances being federated with one another for other people to even perceive it. It's like the very protocol of Mastodon is gas-lighting people.
|
||||
|
||||
They're so preoccupied preventing abuse on a technical level, introducing friction that made it come easy elsewhere, that they've completely missed the mark on how not to obstruct genuine people's natural craving for connecting to one another. Rules and regulations, written as well as unwritten, make people so insecure to engage at all, out of fear for getting berated over simply doing it wrong, spamming too many hashtags, being very anti-engagement, anti-self-promotion, that the actual "social" part of a social media network gets nipped in the bud almost instantly. The constant berating of new users in and of itself is its own form of abuse, but nobody bats an eye about that, because they feel they have the norms of the network on their side to make it acceptable.
|
||||
|
||||
Hell, *even I wanted to see Mastodon making it at some point,* but I'm beyond tired coming up with excuses why other people should suffer this absolute clownage. You don't get to complain with the many structural and societal problems entrenched in your culture and processes that turn off large swaths of people to your idea how social media should work when you don't also take steps to fix that shit.
|
||||
|
||||
But that didn't happen in November 2022, when Mastodon actually would've had a chance at mainstream adoption, and it probably won't happen now when everyone has already decided the trade-offs Mastodon requires are not worth anybody's time when something simpler like Bluesky exists and people actually working towards making it an enjoyable ride. None of the previously outlined issues with Mastodon are easily fixable, because the people in charge don't see them as a problem, and it needs the existing user base of Mastodon to make compromises in how their network works which they likely will not want make.
|
||||
|
||||
I'm [not the first one](https://erinkissane.com/mastodon-is-easy-and-fun-except-when-it-isnt) to point out the shortcomings of Mastodon's approach to social networking, and I'm sure I won't be the last. I think the people on Mastodon are well aware that they have issues to sort out, but I also think I've seen enough of how the dynamics play out every time an opportunity to garner good-will presents itself that the community will work diligently to squander it, to the detriment of everybody.
|
||||
|
||||
So you all can drop the act how you're baffled that people would choose the "wrong" side of internet history, *again.* You've demonstrated that your software, model and culture are unfit for what most people expect of a social media platform and you're unwilling to compromise on that. You'll stay the relatively small place lacking mass appeal. If that's what *you're looking for,* more power to you! But when you're even starting to [disenchant believers](https://akko.wtf/objects/a6f1ea29-8982-4acc-8e99-84ca1adfe532) in the cause, because the climate on the Fediverse grows ever more exclusionary, toxic and disdainful, I think you have different issues to sort out than explaining to people how decentralization should play a bigger role in their life and why they should care.
|
||||
|
||||
The #TwitterMigration didn't fail because of people not joining, but because *the model and culture of Mastodon is its own worst enemy!* Bluesky gaining traction is honestly the least of your problems.
|
169
src/posts/2025-02-02_start-using-linux.md
Normal file
169
src/posts/2025-02-02_start-using-linux.md
Normal file
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
title: How I stopped worrying and ended up using Linux instead
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/b9fd20eb-e4c5-4f76-ae8a-b57773c3acc9
|
||||
alt: Photo of a penguin flapping its wings
|
||||
credit: Photo by Sander Crombach on Unsplash
|
||||
tags: ["linux"]
|
||||
---
|
||||
|
||||
News about tech has been very grim lately. Microsoft keeps pushing Copilot on everyone in Windows, GitHub, even keyboards. The "AI" craze has seen companies burn down years of goodwill in a flash in the search of *The Next Big Thing*. Enthusiasm in tech is at an all time low and people are looking for a way out of the cycle of trash.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
The only alternative that remains is the one people keep shunning: For as much as the [meme](https://yotld.com) tries to suggest it'll never happen, Linux has made incredible strides on the desktop. Not least of which can be attributed to Valve's Steam Deck hitting big and showing a device shipping with Linux out of the box can sell, next to their [continued](https://www.phoronix.com/news/XDC-2023-AMD-Colors-HDR) [involvement](https://www.phoronix.com/news/Steam-Audio-SDK-Fully-Open) [with](https://www.digitaltrends.com/computing/nier-automata-steam-deck/) [Linux](https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/RIZSKIBDSLY4S5J2E2STNP5DH4XZGJMR/) [at large](https://www.phoronix.com/news/Valve-Upstream-Everything-OSS). It showed there is [rising interest](https://www.gamingonlinux.com/2025/01/gdc-2025-survey-shows-pc-game-development-growing-with-lots-interested-in-valves-steam-deck/) in bringing games to the Steam Deck, which could benefit Linux as platform as a whole.
|
||||
|
||||
Linux on the desktop might not be ready this year. Or the next year. Or the year after that. But it's getting *more ready* each passing year and that's the more important part if you ask me.
|
||||
|
||||
I've been running Linux full time for the past 10 years, but not for the reason that might seem obvious at first. Sure, there were grievances with Windows, but these come dime a dozen from many people every day.
|
||||
|
||||
The story of how I ended up on Linux as my daily driver of choice started out a little different.
|
||||
|
||||
## Throwing my sanity out the Window(s)
|
||||
|
||||
In 2012, I built myself a PC for the first time in a long time. For a while, the machine ran quite well. But problems started cropping up not long after.
|
||||
|
||||
I wanted to get into the Let's Play and video game scene on YouTube. I got myself a USB3 video capture device, because USB 3.0 was the latest hot shit (ah yes, [simpler days](https://en.wikipedia.org/wiki/USB_3.0#USB_3.2)...) and I naively thought that the U in USB really stood for "universal". Unfortunately, the video grabber was only compatible with Intel CPUs, not the AMD Bulldozer CPU I had bought[^usbcontroller].
|
||||
|
||||
[^usbcontroller]: Or rather, it was incompatible with the 3rd party USB 3.0 controllers AMD used compared to the 1st party ones by Intel.
|
||||
|
||||
So I sent it back and bought a PCIe card that was compatible. At least it was recognized, but I kept getting crashes and BSODs — not to mention the fact that the PS3 encrypts everything via HDCP and I now also needed an HDMI splitter to get that out of the signal…
|
||||
|
||||
*[HDCP]: High-bandwidth Digital Content Protection
|
||||
|
||||
However, the USB problems were not limited to the video grabber. It happened time and again that USB hard disks simply lost the connection during larger transfers. If this was not the case, then the transfer was agonizingly slow - so much for USB3 speeds…
|
||||
|
||||
To top it all off, after upgrading to Windows 10, the computer simply shut down without warning, as if someone had pulled the plug.
|
||||
|
||||
I was growing quite desperate trying to figure out what was causing the random shut offs. I ran RAM checks for hours and tried to reproduce the conditions that made it shut off, but no dice. The shut offs remained random. Sometimes they would even happen several times in a row while Windows was booting up, which prompted the OS to boot into rescue mode after enough failed attempts.
|
||||
|
||||
## Enter the penguin
|
||||
|
||||
I began running out of things to try to remedy the situation. Being at my wit's end, I decided that, y'know what, fuck it, just install Linux on the damn thing. What's the worst that can still happen at this point?
|
||||
|
||||
After a few days of running [Linux Mint](https://linuxmint.com) on my machine, it didn’t shut off unexpectedly one – single – time.
|
||||
|
||||
I thought I was going crazy. How could a change in OS be the solution to this seemingly unsolvable mess?! But it was and it allowed me to use my machine again. That was all that really mattered to me and so I started to adapt to my new situation.
|
||||
|
||||
+++ Side Note
|
||||

|
||||
|
||||
The first time I tried getting into Linux was during my teens in secondary school, starting with SuSE Linux 9.0 around 2004, included on a CD in a magazine with an installation guide. I was fascinated about the little things open source software did differently from what I was used to, e.g. tabs in the file manager, installing any software in a centralized software center, customizing the look and feel by just downloading files from the internet, copy them somewhere and BOOM new theme! I even held presentations in school showcasing how similar Linux and Windows were in terms of day to day use-cases, like listening to music, watching videos, writing documents and surfing the web.
|
||||
|
||||
I tried other Linux distributions as well, like Debian and eventually Ubuntu. They were very different but that didn't dampen my curiosity. Back then, internet speeds were quite slow, so downloading several hundred megabytes of ISO files was very arduous. At one point that saw me actually *order* a Debian DVD online, so I wouldn't have to wait for downloads or change through a set of 14 CDs when installing literally anything. Shit was wild.
|
||||
|
||||
I even played games on Linux back then, like Doom 3 and Neverwinter Nights (these actually had official Linux versions readily available). But the majority of games I played were exclusive to Windows and unworkable with the version of Wine that was available at the time. So I was missing a reason to stay on Linux for longer because for most of the things I did with a computer, I still needed to boot back into Windows. Alas.
|
||||
+++
|
||||
|
||||

|
||||
|
||||
I was still very "Windows pilled", in that I avoided doing things via the terminal like the plague. Luckily, Linux Mint being very beginner focused allowed me to avoid it for most stuff. I only updated core system packages with `apt`, the rest of my apps I took from somewhere else as a `*.deb` package or I went to the software store app that came with Linux Mint.
|
||||
|
||||
Over time I did get more experimental, thought, and started using the Terminal more. The reason for this was that a lot of troubleshooting stuff I found online would be terminal commands. It kind of clicked that if I wanted to do something on Linux in the fewest steps possible and without scurrying around in a GUI that nobody touched in the past 3 years, the terminal was the way to go. It also helped that I learned what the `man` command was and that any terminal tool came with a `-h` or `--help` option that explained how to operate it.
|
||||
|
||||
It made me realize that if I just chilled out for a sec and read about what things did and how they worked, that I could do things quicker and the solution was always rather obvious the more I learned the ins and outs. It felt like I was back in control and could set up my computer to service me and my computing needs, not the other way around. The fact that Valve hit the scene with Proton and Steam on Linux sealed the deal. I could just launch any game in my Steam library with Proton and play it like it made no difference.
|
||||
|
||||
This was it. I wasn't using Linux out of a necessity anymore. I started preferring it this way because Linux started to serve my needs better. It just stayed out of my way and didn't hassle me with stuff that had no relevance to what I was doing at any given moment. It wouldn't push something on me that I had no desire for and left my system settings alone. I felt like it respected my time and intelligence as a user. My computer worked the way that I wanted, not how someone else thought it should work.
|
||||
|
||||
It felt like I was back in control!
|
||||
|
||||
But, just in case I would need it, I was looking into ways to banish Windows into a virtual machine that I'd boot up only ever so occasionally, for the stuff that absolutely would not run under Wine[^winvm]. That's when I learned that it was possible to have a VM use the host machine's actual hardware and people were actually doing it successfully. My mind was blown such a thing even existed!
|
||||
|
||||
[^winvm]: Stuff like games that come with kernel-level anti-cheat (a whole can of worms in and of itself), which Wine lacks the capabilities and resources to translate.
|
||||
|
||||
But it needed recent software, which Linux Mint, hailing from Ubuntu LTS roots, would most likely not provide in a timely manner. Most people who were passing their computer's GPU directly into a VM to run graphically intensive games were on… Arch Linux. I knew Arch was a whole different beast in terms of Linux distributions, where you had to do **everything** yourself. I neither felt I was willing to do that nor did I feel cut out to be able to do it.
|
||||
|
||||
*[LTS]: Long-term support
|
||||
|
||||
## Keep rolling, rolling, rolling
|
||||
|
||||
Lucky for me, I noticed a certain distribution climb the ranks on [distrowatch.com](https://distrowatch.com/) going by the name of [Manjaro](https://manjaro.org/).
|
||||
|
||||
I looked further into it and learned it was *Arch-based*. I've never heard of any distro being based on anything else other than Debian or Ubuntu. It presented itself as both bleeding edge and user friendly and I was gonna give it a shot, if that meant I would also get to do more tinkering.
|
||||
|
||||

|
||||
|
||||
And for the most part, I pretty much got what I expected. I was able to enjoy very recent software, on a rolling release basis, without the frills of having to manage everything myself. Also, I learned of a neat little thing called the [AUR](https://aur.archlinux.org/), with tons of more great software that was just waiting for me to install it. I was very excited of the possibilities! Also, Manjaro being a rolling-release disto, just like Arch, meant I did not have to deal with major distribution release cycles, receiving updates to the latest versions when they become available. I was always up to date, as long as I just kept installing periodic system updates. What a concept!
|
||||
|
||||
It wasn't soon after that my tinkering continued. I learned of a thing called [LVM](https://en.wikipedia.org/wiki/Logical_volume_management). I knew of it from the Ubuntu/Mint installer as an option for partitioning but it mystified me. Since I was on an Arch base with Manjaro and I felt comfortable with the inner workings of my system more and more, I consulted [Arch Wiki](https://wiki.archlinux.org) a lot more, too. I learned that LVM allowed for some really flexible partitioning setups, while also allowing for something called LVM caching, combining the speed of SSDs with the cheap capacity of spinning HDDs. I got it done pretty comfortably with Manjaro's text-based installer. Calamares (the GUI installer) was… limiting and buggy[^calamaresbugs]. Once Manjaro discontinued their text-based installer for lack of maintainership, and LVM cache had gotten pretty integral to my setup, I started looking into making the jump to proper Arch Linux.
|
||||
|
||||
[^calamaresbugs]: Specifically, despite Calamares seemingly being capable of LVM-based setups, it kept destroying the ones I prepared on the terminal and complained about the root file system having disappeared once I started the intallation. Turns out Calamares, for some reason, still calls `mkfs` on the partition directly instead of the LV that was selected. Only way around that was formatting the LV with the desired file system ahead of time and just point Calamares to a completely prepared setup.
|
||||
|
||||
## Look Ma'! No GUI!
|
||||
|
||||
I first started dipping my toes in by installing Arch into a VM. It went well up to the point I had a typo trying to set my timezone and my TTY (**T**ele**TY**pewriter, the "DOS prompt" of the Linux kernel) got completely fucked up. After all the trouble I went through of even getting to that point, I shut it all down and decided to come back later. I knew installing Arch would be a lot of manual work, but I was more worried about the execution, when I should've been worried about the amount of research required to learn about all the concepts that graphical installers have been abstracting away from me all this time. I never had to think about how to partition disks and formatting them on the terminal before, or how to install the bootloader from hand, or what went into building an initramfs to end up with a bootable system.
|
||||
|
||||
Eventually, I managed to complete the installation process and have the machine boot into a graphical environment. Let me tell you, I was never more excited logging into *any* desktop environment. It was a personal breakthrough! I felt like I was equipped with the knowledge and prowess to run Arch on bare metal. Before doing so, I spent considerable amount of time researching what my ideal setup would look like.
|
||||
|
||||
My ideal stack at the time looked something like this:
|
||||
|
||||
- **Wayland:** A modern and secure display server
|
||||
- **Pipewire:** A modern, low-level multimedia framework
|
||||
- **KDE:** Highly customizable desktop
|
||||
- **LVM Cache:** Combine the speed of an SSD with the cheap capacity of a large HDD
|
||||
- **btrfs:** Filesystem with modern features like CoW, subvolumes and snapshots
|
||||
- **zram:** Compressed swap space in RAM
|
||||
|
||||
*[CoW]: Copy-on-Write
|
||||
|
||||
The pragmatic DIY nature of Arch essentially allowed me to build and maintain my own distro, and I was eager to build the most cutting edge system to be at the forefront of what was going on in the open source world.
|
||||
|
||||
## Deeper down the rabbit hole
|
||||
|
||||
Once I went pure Arch, it felt like I could do anything. It wasn't long before I looked into how to secure my system with encryption. It was another big hurdle for me to take and these days I encrypt all my hard drives wherever I can.
|
||||
|
||||
Since I was handed a laptop at work and was free to put Linux on it, it was important to me to maximize the security on a portable device. I knew Linux was Secure Boot capable, but not exactly how it worked. So naturally, I looked into that, too.
|
||||
|
||||
Turns out, Linux distributions that are able to boot with Secure Boot enabled use something called a "shim", that is signed by Microsoft, and hands the boot process over to Linux once verified by the computer's main firmware. It's kind of a workaround, since Microsoft has an iron grip on the whole Secure Boot landscape[^secboot]. But I also learned that nothing stops me from putting my own keys and signatures into it so OSes that I sign with them can boot with Secure Boot enabled. So that's what I did! Tools like `sbctl` made it trivial to generate keys, push them into firmware and sign bootable binaries[^sbctl].
|
||||
|
||||
[^secboot]: This is where the prevailing opinion comes from that Secure Boot is the vehicle with which Microsoft is trying to lock out alternative operating systems. But most of the time it's enough to simply turn the entire feature off in the firmware settings and you can boot anything you want again.
|
||||
|
||||
[^sbctl]: The only tricky part about Secure Boot is how to get the firmware into "Setup" mode. Usually this involves setting the firmware Secure Boot mode to "Custom" and wiping the keystore clean.
|
||||
|
||||
I also knew that my computer had a [TPM](https://en.wikipedia.org/wiki/Trusted_Platform_Module) and I was curious to see if I could utilize it for anything on my Linux machine. Turns out, I can: It is possible to store [LUKS](https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup) encryption keys inside the TPM and have the encrypted disk only unlock if the system is in a known good state, i.e. it has not been tampered with since it was set up.
|
||||
|
||||
<iframe style="width: 100%; aspect-ratio: 16/9;" src="https://media.ccc.de/v/arch-conf-online-2020-6385-protecting-secrets-and-securing-the-boot-process-using-a-trusted-platform-module-tpm-/oembed" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
A presentation by Jonas Witschel (diabonas), who demonstrated this on Arch Linux, was what initially convinced me to try for myself. Combined with the work by the `systemd` maintainers, it is now really easy to deploy a TPM-backed LUKS encryption key via `systemd-cryptenroll --tpm2=device=auto`.
|
||||
|
||||
Together with this my default setup now consists of (signed) `systemd-boot`, which automatically detects the (signed) UKI on the EFI system partition, and only unlocks the TPM-backed LUKS encrypted disk if the machine's state is the same as when I sealed it against a particular set of values in the TPM.
|
||||
|
||||
*[UKI]: Unified Kernel Image
|
||||
|
||||
## Beyond the desktop
|
||||
|
||||
Getting this deep into the weeds also prompted my entry into server and network stuff. In turn, a lot of my digital live has also shifted heavily to self-hosted services and open source tools, just by virtue of there being no barriers to entry. It's only a matter how much time you have on your hands and are willing to invest.
|
||||
|
||||
I host my own [Nextcloud](https://nextcloud.com) for sharing and syncing files, calendar appointments, contacts, and editing documents right in my browser. Free from prying corporate eyes, seeking to feed their bumbling "AI" with my data.
|
||||
|
||||
[Immich](https://immich.app) backs up my photos and makes them easily accessible like Google Photos, but without the Google parts.
|
||||
|
||||
I use [Jellyfin](https://jellyfin.org) to stream my movies and TV shows at home and on the go, without the fear of stuff just disappearing from my library or getting pestered with ads despite paying a monthly fee.
|
||||
|
||||
I have my music collection with me wherever I go with [Navidrome](https://www.navidrome.org). I continuously expand it by buying from platforms like [Bandcamp](https://bandcamp.com) which compensate artists fairer than Big Stream.
|
||||
|
||||
I have my own [Bookstack](https://www.bookstackapp.com) wiki where I document a lot of the technical stuff I do for future reference.
|
||||
|
||||
I host my own websites from home with [Nginx](https://nginx.org).
|
||||
|
||||
I stream my games with OBS to my [Owncast](https://owncast.online) live streaming server while not being beholden to what a corporation thinks I can and can't broadcast.
|
||||
|
||||
I curate my news sources in [FreshRSS](https://freshrss.org) to read them at home and on the go. In chronological order, free from pushy algorithms and on my terms.
|
||||
|
||||
Digital sovereignty rulz!
|
||||
|
||||
## Looking back
|
||||
|
||||
If you had told me in 2015 that I'd be using a Notoriously-laborious-to-install Linux distro full time and do all kinds of crazy things with it, I'd have laughed you out of the room. In the 10 years I've daily-driven Linux exclusively[^amdgpuglitch] by now, doing it any other way seems weirdly restrictive to me now. And Arch has been treating me exceptionally well so far. Only once did my system break so badly, that I had to reinstall (and no, it wasn't because an update [deleted my bootloader](https://www.reddit.com/r/linuxmasterrace/comments/x3v5sd/fuckers_stole_my_grub_cant_have_shit_in_arch_linux/)).
|
||||
|
||||
Getting into Arch also taught me a valuable lesson: That I could learn anything I set my mind to and overcome seemingly insurmountable hurdles. The freedom to have complete control over how my system operates is something I wouldn't want to miss in any future computer I buy.
|
||||
|
||||
[^amdgpuglitch]: Safe for that one time I couldn't get Linux to even boot on my new 2019 machine because of an [AMDGPU bug](https://gitlab.freedesktop.org/drm/amd/-/issues/1237) that crashed the driver for my GPU.
|
||||
|
||||
However, if it wasn't for the efforts of Valve to make gaming on Linux finally a viable reality, I probably wouldn't have stuck around for as long as I did[^anticheat]. In the end, gaming is still the biggest thing people use their computers for. Bringing Steam to Linux was a boon to PC gaming and Valve is in it for the long haul, as the Steam Deck and SteamOS continue to prove their [commitment](https://www.gamedeveloper.com/pc/steamos-is-officially-heading-to-third-party-handhelds) to Linux as a gaming platform.
|
||||
|
||||
[^anticheat]: And more would give it an honest look if the biggest online games weren't loaded with invasive, festering multi-million dollar anti-cheat software that is [still helpless](https://www.youtube.com/watch?v=RwzIq04vd0M) against Raspberry Pi or Arduino boards.
|
||||
|
||||
Witnessing Linux improve rapidly year over year, from the obscure thing that only runs in data centers to the operating system that inches its way to the desktop, slowly evolving into a desktop operating system that is ready to be daily-driven, is a sight to behold for me.
|
||||
|
||||
Here's to another exciting 10 years on the Linux desktop! 🍻
|
67
src/posts/2025-04-20_mario-kart-stickershock.md
Normal file
67
src/posts/2025-04-20_mario-kart-stickershock.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: Mario Kart World costing $80 is not the issue
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/273d250e-177c-4054-a20b-f9f3e83bb7b2
|
||||
alt: Mario Kart World Key Art
|
||||
credit: © Nintendo
|
||||
tags: ["games"]
|
||||
---
|
||||
|
||||
The Switch 2 was revealed and alongside it the new Mario Kart World which will be a launch title for the upcoming Nintendo console. The reveal certainly has been the talk of the town for a while now, but aren't raving on about how good they think the new console is or how good games on it look.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
The Switch 2 will see a 50% price increase over its predecessor, from around $300 to around $450. This already upset people during the reveal live stream. The chat was flooded with messages for pretty much the entire stream with a single message:
|
||||
|
||||
***"DROP THE PRICE"***
|
||||
|
||||
And while console games in general have started to go from $60 to $70 since the PlayStation 5 launched, Nintendo has steadily kept their prices locked in at $60 for their original Switch games (a notable exception to this being *The Legend of Zelda: Tears of the Kingdom*, which also came in at $70).
|
||||
|
||||
Mario Kart World for the Switch 2 will cost **$80.**
|
||||
|
||||
People were ***aghast.***
|
||||
|
||||
Now, with Mario Kart also going open world with this newest installment, one *could* argue that that's a bit more involved of a design process to have the tracks all interconnect and fill it with stuff to explore off-road. One *could* argue since it's on a more advanced piece of hardware, it's more involved technologically.
|
||||
|
||||
However, I think the issue with the complaints lies entirely elsewhere.
|
||||
|
||||
## It's about literally every other game
|
||||
|
||||
On a surface level, it doesn't seem too unreasonable to get worked up about a Nintendo game costing $80. Framing the whole discussion around a single game, however, is missing the point. People aren't really mad that Nintendo games, which come out without egregious bugs and day one patches and are generally very polished on release, will cost between $70 and $80 going forward.
|
||||
|
||||
What made people fall into a frenzy, was the fear that Nintendo will set a precedent with pricing games this high[^gta6] and that a lot of people will be priced out of their favorite hobby. Because if Nintendo can do it and people still buy the game at that price point, anybody else can charge this much, too.
|
||||
|
||||
[^gta6]: There's already rumors that Rockstar Games is [considering charging $100](https://www.ign.com/articles/gta-6-price-many-in-the-video-game-industry-hope-for-80-if-not-100-analyst-says) for their upcoming GTA 6. Whether *that's* justified will be a debate for when it launches. But I have no doubt people well be up in arms about that once more when that time actually comes.
|
||||
|
||||
That's what I think this is actually about.
|
||||
|
||||
The games industry has seen better days, to be honest, with most AAA games making headlines for their unpolished state teetering on the unplayable and big publishers seemingly thinking "release now, patch later" is a viable long-term business strategy.
|
||||
|
||||
People have made it very clear they think AAA games aren't worth their asking price anymore. They're [grossly over-budgeted][callofduty], [samey and uninspired][bethesda], [rushed to market with tons of bugs][cp2077], [do not perform well, even on powerful and expensive hardware][mohuwilds], [chock-full of predatory micro transactions][lootboxes] and even if they're lauded, [their developers still get laid off][insomniaclayoffs][^votewallet].
|
||||
|
||||
[callofduty]: https://www.gamefile.news/p/call-of-duty-budgets-development-costs-black-ops-modern-warfare
|
||||
[bethesda]: https://www.youtube.com/watch?v=hS2emKDlGmE
|
||||
[cp2077]: https://www.bbc.com/news/business-55359568
|
||||
[mohuwilds]: https://www.pcgamer.com/games/action/it-runs-awful-it-looks-awful-monster-hunter-wilds-performance-issues-put-a-dampener-on-its-record-breaking-concurrents-as-it-settles-into-an-early-mixed-rating-on-steam/
|
||||
[lootboxes]: https://www.youtube.com/watch?v=7S-DGTBZU14
|
||||
[insomniaclayoffs]: https://sonyinteractive.com/en/news/blog/difficult-news-about-our-workforce/
|
||||
|
||||
[^votewallet]: And don't give me any of that "Vote with your wallet" bullshit! People *are* voting with their wallets, but sadly, they're voting for the exact opposite of what you're trying to invoke with that idiom.
|
||||
|
||||
## Video games? In *this* economy?!
|
||||
|
||||
Also, [ongoing](https://www.abc.net.au/news/2025-04-12/donald-trump-tariff-trade-war-week-roils-americans/105164924) [economic](https://www.newsweek.com/2025-eggs-prices-per-dozen-economy-donald-trump-2044401) [uncertainty](https://apnews.com/article/trump-tariffs-pause-businesses-reaction-a61a1adcaf6332f6188ae1d70664b898) is certainly contributing to the outrage.
|
||||
|
||||
So getting anxious about getting priced out of a hobby to tune that shitshow out I feel is pretty justified.
|
||||
|
||||
But I think focusing that on a single game is misguided. This has been brooding for years and Nintendo just had the misfortune to be, well, their usual disconnected selves, not read the room ahead of time and go ahead with their announcements anyway. So them getting a shitstorm of their own doesn't surprise me.
|
||||
|
||||
I also think [arguing why the price of the Switch 2 "makes sense"](https://www.youtube.com/watch?v=953iQVQ9lYE) for a number of reasons that are pretty much irrelevant to the majority of people is pretty tone deaf and misses the point of what their actual pain points are. People aren't arguing for Nintendo to price an obviously more capable device the same as its 8 year old predecessor.
|
||||
|
||||
On the other hand, I also think people aren't exactly arguing their point very concisely right now. Because I believe neither the price of the Switch 2 nor the price of Mario Kart World is at the core of the issue here. It's just getting over the initial sticker shock. The actual issue is people's financial realities clashing with the expectations of companies that people will keep spending money as they've been for years, when they're still reeling from an inflation caused by a global pandemic and an economy that has become reliant on people going into debt for literally *anything.*
|
||||
|
||||
[The top 10% of earners in the US carry nearly half of the entire country's economy](https://qz.com/us-economy-consumer-spending-wealthy-1851766072). Everyone else is feeling the squeeze of inflation, everything's getting more expensive, yet everybody's paycheck stays the same. At some point, something's got to give.
|
||||
|
||||
Friggin' [Door Dash and Klarna started partnering](https://about.doordash.com/en-us/news/doordash-partners-with-klarna) in the US, so people can buy their takeout now and pay it later. If that's not the sign of an economy about to fail, I don't know what is.
|
||||
|
||||
So, yeah, people are rightfully angry at a Mario Kart game costing $80. But that anger is better directed at the games industry and policy makers of their countries as a whole. Nintendo's latest announcements were just the straw the broke the camel's back — and something tells me it won't be the last either.
|
38
src/posts/2025-04-28_nfts-lost-media.md
Normal file
38
src/posts/2025-04-28_nfts-lost-media.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: NFTs are becoming lost media
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/f48cf018-687d-4fc1-99f2-e1f53bebf454
|
||||
alt: Ornate picture frame on a noisy background with a broken image icon in the middle
|
||||
credit: Made with GIMP, picture frame by cgordon8527 on Pixabay
|
||||
tags: ["I told you so"]
|
||||
---
|
||||
|
||||
NFTs arrived on the scene with big promises. They were pitched as the future of digital ownership. Using the blockchain as their basis, they were supposed to last forever. With an NFT one was supposed to prove beyond any doubt a digital asset was unique and truly owned. Fast forward only a couple years and the only thing that got owned were the people who blew their live savings on JPEGs of ugly-ass apes and having nothing to show for it.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Why is this happening? For the exact reason people with a functioning brain and knowledge of how things actually work have been calling bullshit on the idea the entire time: the assets were never stored on the chain itself.
|
||||
|
||||
The blockchain is nothing if not a giant, distributed log file of transactions. That's your receipt of "ownership": something some people attached value to has changed hands. But the actual thing money changed hands over doesn't live on the blockchain. Instead, the NFT only contains a link in its meta data to the thing, because storing the thing on the blockchain itself would bog the whole network down after just a few files.
|
||||
|
||||

|
||||
|
||||
In pure technical terms, it wouldn't be impossible. However, storing binary data on the blockchain would be prohibitively expensive (computationally as well as financially, in transaction fees) and also incredibly inefficient and slow.
|
||||
|
||||
To participate in a blockchain, every participant has to download the chain in its entirety[^centralized]. With how much bigger binary files are in comparison to simple text strings, this would dramatically increase the time needed to download and verify the entire chain. Blockchains also usually have a limit on how large a block can be, usually just a couple of megabytes, which isn't even enough to store your favorite song in decent quality. Remember, it's just a ledger, a giant log file that proves transactions between two parties took place, not what the contents of the transaction were.
|
||||
|
||||
[^centralized]: At least in theory. Blockchain is supposed to be a globally distributed, decentralized ledger between participants who do not trust each other. Marketplaces such as OpenSea, where a lot of NFTs are traded, reintroduce a trusted third party, which runs counter to the entire idea behind blockchain, which explicitly seeks to eliminate the need for one. Think "Blockchain as a Service", you don't run it yourself, someone else runs the chain for you and grants you centralized access to it. That should've already raised alarms this was all bullshit. But people were too dazed by big promises of easy money.
|
||||
|
||||
Yet, crypto bros sold people on the idea that NFTs being on the blockchain is some sort of sophisticated copy protection. We already had something like this when iTunes entered the scene: It's called "Digital Rights Management" (DRM), is very much anti-consumer and seeing how NFTs are now becoming inaccessible due to the hosting services shutting down, they follow [right in DRM's footsteps](https://arstechnica.com/information-technology/2008/04/drm-sucks-redux-microsoft-to-nuke-msn-music-drm-keys/).
|
||||
|
||||
Imagine paying thousands of bucks for a link to someone's Dropbox. Then something happens to their account (e.g. they generate so much traffic that Dropbox terminates their account) and it takes your JPEGs "worth" thousands of bucks with it.
|
||||
|
||||
As stupid as that sounds, that's [exactly what happened](https://x.com/PixOnChain/status/1915352785626845289).
|
||||
|
||||

|
||||
|
||||
Seems like some people pointed to a Cloudflare link in the NFT meta data and it generated enough traffic that Cloudflare caught on and terminated hosting because of violation of their terms of service. That's $795,189 or €699,053 down the shitter.
|
||||
|
||||
NFTs promised provenance and permanence, yet what it didn't solve was a problem that is as old as the internet itself: link rot. What believers are left with are a lot of broken promises and really expensive broken links. My sympathy is severely limited.
|
||||
|
||||
Remember this the next time some tech bro comes along and tries to pitch something to you as "the future" with religious fervor.
|
336
src/posts/2025-05-19_endof10-switch-to-linux.md
Normal file
336
src/posts/2025-05-19_endof10-switch-to-linux.md
Normal file
|
@ -0,0 +1,336 @@
|
|||
---
|
||||
title: The time to make your computer truly yours again is now
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/cfc29fc1-62ff-4205-93e1-c020dbc14d5e
|
||||
alt: Windows 11 full screen ad
|
||||
credit: The Shining © Warner Bros., edit made with GIMP
|
||||
tags: ['windows', 'linux']
|
||||
---
|
||||
|
||||
Windows 10 support [ends for good October 14, 2025][win10end] and from what I've been able to tell, people aren't looking forward to upgrading to Windows 11 any time soon. Microsoft surely isn't doing itself any favors with the heavy push into "AI" and stuffing Copilot into everything. Also, full screen ads at start-up basically nagging people to upgrade—if their computer can even run Windows 11—or buy an entirely new computer certainly isn't helping that sentiment. Where to go from here?
|
||||
|
||||
[win10end]: https://support.microsoft.com/en-us/windows/windows-10-supports-ends-on-october-14-2025-2ca8b313-1946-43d3-b55c-2b95b107f281
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Your computer will not simply stop working after October 14. However, Microsoft will stop providing your computer with feature updates, bug fixes, and security updates. The end of support for Windows 10 also means that third-party hardware and software vendors will no longer be required to support the aging operating system—at least in terms of development, as they will no longer receive support from Microsoft either.
|
||||
|
||||
For you, this may mean that newer software (e.g., your web browser) will no longer receive updates unless you install a newer version of Windows[^oldwinsupport], and newer hardware, released a few months or years after support eventually ends, may become incompatible with your Windows 10 machine.
|
||||
|
||||
[^oldwinsupport]: As an example, Google Chrome supported Windows 7 [until version 109][chromewin7end] in early 2023, when Windows 10 had been out for 7 years. Similarly, Valve dropped support for Windows 7 in Steam [as late as 2024][steamwin7end], 15 years after Windows 7 originally shipped.
|
||||
|
||||
[chromewin7end]: https://support.google.com/chrome/thread/185534985
|
||||
[steamwin7end]: https://help.steampowered.com/en/faqs/view/4784-4F2B-1321-800A
|
||||
|
||||
Furthermore, if anything happens to your working Windows 10 installation and you find yourself needing to reinstall, Microsoft may very well remove the ability to download installation media from [their website][win10iso]. Of course, you could download it through other means, but that opens you up to security risks, because you happen to download installation media that has been maliciously tampered with[^win10isorisk].
|
||||
|
||||
[^win10isorisk]: And I expect this risk to not be negligible. If enough people are looking for ways to stay on Windows 10 no matter what and they *have* to go somewhere else to download an ISO, you can bet your sweet bippy there's gonna be a *lot* of dodgy websites on Google that ship with malware preinstalled.
|
||||
|
||||
[win10iso]: https://www.microsoft.com/software-download/windows10ISO
|
||||
|
||||
Look, we all know where I'm gonna go with this. If you hate the idea of having Windows 11 forced on you, but don't want to spend upwards of $1,000 for the privilege of using macOS in a Starbucks near you, Linux is the only viable option left if you want to keep your current computer going. And hey, [if PewDiePie can do it][pewdslinux] and become a crack at it out of nowhere, who's to say you can't?
|
||||
|
||||
[pewdslinux]: https://www.youtube.com/watch?v=pVI_smLgTY0
|
||||
|
||||
If you've never had to think about installing an operating system before, it can feel very daunting. It doesn't have to be, though. What I'm going to attempt is try to make it feel less daunting and maybe give some guidance on how to get started if you're willing to indulge me.
|
||||
|
||||
But before I do that, let's get some things cleared up first.
|
||||
|
||||
## Dispelling Myths
|
||||
|
||||
Despite the progress Linux has made on the desktop in recent years, some myths still persist. The most common ones center around Linux being complicated and only suitable for programmers and tech enthusiasts. Some think of Linux as the operating system that powers servers and supercomputers only, not a desktop machine or a laptop. Others dismiss it outright as not suitable for daily use because of lacking support for standard software and common hardware that people use.
|
||||
|
||||

|
||||
|
||||
While that might have been true 10-15 years ago, a lot has happened in that time and considerable effort went into making Linux into a more approachable and user-friendly operating system. What I want to do in this article is dispel these myths and show you how things look today.
|
||||
|
||||
### You don't need to know code to use Linux
|
||||
|
||||
This is probably the one that's most persistent. And I get it, in most people's heads, Linux is the thing that runs on servers, not your laptop, and is maintained by people who know their stuff. Nothing the average user could do on their own, let alone learn all the ins and outs of how computers actually work to even get started, right?
|
||||
|
||||
Nothing could be further from the truth!
|
||||
|
||||

|
||||
|
||||
A significant amount of work has been put into making Linux more approachable and accessible to everyone, even if they know very little about computers. The installation process of most distributions is done graphically in the majority of cases, and takes only a few clicks.
|
||||
|
||||
+++ What's a "distribution"?
|
||||
Unlike with Windows or macOS, Linux comes in many different "flavors", which people call a *distribution*, or *distro* for short. Distributions differ in the pre-selection of software and tools that you start out with and how software is managed. Distributions can share some of their characteristics with and sometimes are based off of each other.
|
||||
|
||||
The mobile operating system Android can be thought of as a distribution. Manufacturers like Samsung, OnePlus, Nothing, Motorola, etc. pre-install their selection of apps and launchers (e.g. Samsung One UI) with them, but at the core it's still Android and you can use a different launcher if you want.
|
||||
|
||||
Linux distributions are very much like that, in that they come with a pre-selected desktop environment (which you can replace with a different one), maybe a few custom apps for maintenance and customized with the distribution's look & feel.
|
||||
+++
|
||||
|
||||
Distributions that have dedicated themselves to be as user-friendly as possible include [Linux Mint], [Ubuntu], [Pop!_OS], and [Zorin OS]. They usually also come with a pre-selection of apps and tools that give you a fully functional desktop for everyday tasks: web browser, email, word processing, spreadsheet, presentations, music/video player, etc. with tons of more apps available from app stores.
|
||||
|
||||
[Linux Mint]: https://www.linuxmint.com/
|
||||
[Ubuntu]: https://www.ubuntu.com/
|
||||
[Pop!_OS]: https://system76.com/pop/
|
||||
[Zorin OS]: https://zorin.com/os/
|
||||
|
||||

|
||||
|
||||
What probably keeps this myth going strong after so many years is that whenever non-tech focused websites report on anything regarding Linux or tech in general, they come with header images of code editors or terminals running commands that look like they're lifted straight out of The Matrix. Compared to articles about Windows, that show normal screenshots of the Windows user interface everybody is familiar with, that makes everything that is *not* Windows seem intimidatingly arcane.
|
||||
|
||||
Speaking of which…
|
||||
|
||||
### You don't need to use the terminal for standard stuff
|
||||
|
||||
Another contender for one of the more persistent myths: everything on Linux has to be done through a command line terminal. Just like the misconception that you need to know code, this hasn't been true for years. While more advanced users might *prefer* the terminal or even swear by it, it's by no means the *only way!*
|
||||
|
||||

|
||||
|
||||
In 2025, installing apps on Linux can be done as easily as you're used to from Windows or your phone. App stores like [Flathub] offer a variety of many different apps and *Discover* or *GNOME Software* offer a convenient way to install apps with a single click — no command line necessary. All of your app management can be done graphically through these store fronts. You also don't have to sign up to get them, you just click *install* and you're set.
|
||||
|
||||
[Flathub]: https://flathub.org/
|
||||
|
||||
There's also apps you should already be very familiar with: Steam, Discord, Chrome, Firefox, OBS Studio, Spotify, Telegram, VLC, RetroArch are some of the [most popular apps][flatpop] on Flathub.
|
||||
|
||||
[flatpop]: https://flathub.org/apps/collection/popular/1
|
||||
|
||||
")
|
||||
|
||||
[GNOME Settings]: https://apps.gnome.org/Settings/
|
||||
|
||||
Similarly, configuring your system is also done graphically. Connecting to a Wi-Fi network, pairing Bluetooth devices, adjust your screen resolution, setting system language, configuring peripherals like keyboard, mouse, printers, audio outputs/inputs, volume levels, signing into your online accounts for email, calendar, notes, files and contacts, setting default applications, it's all done through a control panel app.
|
||||
|
||||
What the command line terminal *is* still invaluable for is **troubleshooting.** Most apps on Linux output logging information when launched via a terminal. If something goes wrong or the app behaves strangely and you wanna know why, the terminal will most likely be the place to start looking for hints as to why.
|
||||
|
||||

|
||||
|
||||
When asking for help online these outputs are invaluable to people trying to get to the bottom of things. They *might* give you some terminal commands to run to get them info on what hardware you use, how your system is actually set up, what the last error was the app that's acting up threw at you, etc.
|
||||
|
||||
But here's the thing about that: It's not some kind of weird flex because they know their terminals and you don't. It's actually the *easiest* and *quickest* way to get these infos for them than instructing you to click through menus that could vary wildly between different setups that they might not even be familiar with themselves.
|
||||
|
||||
And that's why some people swear by the terminal: it's quick, targeted and universal across a wide array of system configurations. Every major distribution has these terminal tools installed by default because they are the *base tools* that make things tick in the first place.
|
||||
|
||||
**You absolutely still don't have to use the terminal,** but if you do make an effort to get to know it and start to use it more, things will click after a certain point and just start to fall into place in a logical, systematic kind of way.
|
||||
|
||||
### Gaming on Linux is real
|
||||
|
||||
Another myth that doesn't hold water at this point is that Linux is unsuitable for gaming because all games target Windows.
|
||||
|
||||
I expect most of the people that follow me on the net are in their 20s or 30s who probably play games on their computer. The gaming landscape has been improving steadily on Linux, thanks to Valve and their [continued investment][steamdeckverifiednumbers] into Linux as a gaming platform.
|
||||
|
||||
[steamdeckverifiednumbers]: https://steamdeckhq.com/news/17000-steam-deck-verified-playable-games/
|
||||
|
||||
Valve is putting remarkable effort into making the transition as painless as possible. They're doing this with *Proton*, which is based on the open source software called [Wine]. Proton/Wine translate system calls of Windows applications into Linux system calls in the background, tricking applications into believing they're running on Windows. Paired with [DXVK], another translation layer for 3D graphics, games made for Windows run on Linux as if they were made for it.
|
||||
|
||||
[Wine]: https://www.winehq.org/
|
||||
[DXVK]: https://github.com/doitsujin/dxvk
|
||||
|
||||

|
||||
|
||||
What's especially nuts about all of this is that all this came to be because one guy didn't want to accept *Nier: Automata* wasn't playable on his Linux PC, he just went "Nah, I'mma do it anyway" [*and made it happen*][dxvkstory]. It's this dedication and hard work by someone who just didn't want to accept the status quo that laid the groundwork that gave us the Steam Deck.
|
||||
|
||||
[dxvkstory]: https://www.digitaltrends.com/computing/nier-automata-steam-deck/
|
||||
|
||||
From my own experience, gaming on Linux has been exceptionally solid. I can play the critically acclaimed MMORPG _Final Fantasy XIV_ comfortably on my non-Windows system. I play with a PlayStation 5 DualSense controller connected via Bluetooth and it works just like one would expect. With the help of [XIVLauncher] (installed from Flathub!) getting up and running is a breeze.
|
||||
|
||||
[XIVLauncher]: https://goatcorp.github.io/
|
||||
|
||||

|
||||
|
||||
[ProtonDB]: https://www.protondb.com/
|
||||
|
||||
Same with a lot of my games on Steam: Out of the 184 games in my Steam library[^smolsteamlib], they are rated 52% platinum (runs flawlessly), 40% gold (runs well after minimal tweaks), 6% silver (might need some tinkering, but still playable), 1% bronze (issues impacting gameplay) and 1% borked (don't even bother).
|
||||
|
||||
[^smolsteamlib]: Yes, I'm not as spending happy on Steam as one might expect. I'm rather selective when it comes to which games I add to my library, Steam Sales be damned.
|
||||
|
||||
You can check your own library by visiting [ProtonDB], searching for the game by name or logging in with your Steam account and bulk-check your entire library, like in the screenshot above.
|
||||
|
||||
If the game has gone through any kind of Steam Deck verification, chances are it will run on desktop Linux through Proton.
|
||||
|
||||
Other launchers such as the [Battle.net app], [Ubisoft Connect] and the [EA app] also work on Linux through Wine. For the Epic Games Store, there's an alternative called [Heroic Games Launcher], which not only grants you access to your games on the Epic Games Store, but also GOG and Amazon Prime Gaming. There's also [Lutris] and [Bottles], two other apps that make it really easy to run Windows apps on Linux with guided installation procedures. Xbox Game Pass is probably the only party pooper in that regard, since the Xbox app and the Windows Store app are tightly integrated with Windows and not available as a separate download to run through Wine/Proton.
|
||||
|
||||
[Battle.net app]: https://download.battle.net/desktop
|
||||
[Ubisoft Connect]: https://www.ubisoft.com/ubisoft-connect
|
||||
[EA app]: https://www.ea.com/ea-app
|
||||
[Heroic Games Launcher]: https://heroicgameslauncher.com/
|
||||
[Lutris]: https://lutris.net/
|
||||
[Bottles]: https://usebottles.com/
|
||||
|
||||
### Don't stress over your choice of distro
|
||||
|
||||
There's no shortage of "best distro for beginners" or "best distro for gamers" articles and videos and lots of those come from somewhat seasoned people who already took the plunge. I'm here to tell you that the choice of your first Linux distribution doesn't matter as much as it used to.
|
||||
|
||||
That's not me saying all those articles and videos on the topic are wrong! The choice of distro is a very personal one and the sheer number of distros out there can give some people choice paralysis. Don't fret! There is no right or wrong here. You won't be locked into any kind of ecosystem. Nothing will prevent you from trying stuff out for yourself, change stuff that isn't to your liking and switch to another distro if you notice the one you initially chose isn't for you!
|
||||
|
||||

|
||||
|
||||
Here's the kicker: Most of the distros out there allow you to test drive Linux from a USB thumb drive before making any changes to your computer! That way you can test if your hardware is compatible, that everything works as expected, see if you like the Look & Feel and play around in it. If you don't you can just restart, flash another distro on it and try again.
|
||||
|
||||
### The Linux community is here to help you help yourself
|
||||
|
||||
Using Linux is a choice the people who're using it made willingly. They put their own time and effort into it because they want to make things work, no matter how quirky their hardware is. They want an operating system that works how they need it to work and doesn't create artificial barriers what they can do with it.
|
||||
|
||||
I often read or hear that people think the Linux community is very abrasive when asking it for help. While the chance of meeting someone with an immensely inflated ego on Reddit is certainly not zero, I wouldn't consider these types representative of the community as a whole.
|
||||
|
||||
Distributions tend to run their own forums where one can ask for help, usually with a forum specifically for [beginner questions][mintbeginnerquest].
|
||||
|
||||
[mintbeginnerquest]: https://forums.linuxmint.com/viewtopic.php?t=445827&sid=0938c8c668c79b32bbee0baa1b68be3e
|
||||
|
||||
I think the impression that the Linux community is unfriendly to newcomers is largely a misunderstanding where and how to ask for help: if you're coming from a background that tried to anticipate every possible need you might have and take it off of you, it might feel very jarring to be asked to do some of the problem solving yourself and upfront. This is not meant as an elitist dunk on you, we want to skip the part where we suggest things you might have already tried and get you the help you actually need, faster.
|
||||
|
||||
We largely do this in our spare time for fun. There's no big company behind this, it's people like you and me. We'd love for nothing more than to make people fall back in love with computing again and enable you to do so on your own terms. If you're new to this, there's no shame in it. We all started somewhere. Just say that you've just started using Linux and trying to figure it out. It helps manage expectations and people will be more than happy to show you the ropes and clear up any misunderstandings you might have.
|
||||
|
||||
However, what the Linux community is generally not fond of is people expecting to have their problems solved for them. Since this is a community effort, people will expect that you've tried at least *some* things, show that you did a *little* research into your problem and communicate clearly what you tried, what didn't work and if there's questions you might have. That way you help us enable you to help yourself.
|
||||
|
||||
We want to work *with* you, not *for* you.
|
||||
|
||||
## Show-stoppers
|
||||
|
||||
While you've read me write in a very enthusiastic tone about switching to Linux, I'm not going to pretend that ripping out a core part of your computer is going to be inconsequential. Quite the opposite, actually. While Linux has come a long way, the world at large is still focused on Windows and there **will** be show-stoppers that I want you to be aware about.
|
||||
|
||||
### Games with invasive anti-cheat
|
||||
|
||||
Earlier I said that gaming on Linux is very much a thing and most games will run just fine. But I will tell you right now, that my experience is only this smooth because I don't play popular online games with invasive anti-cheat.
|
||||
|
||||
I call it "invasive" because in recent years game developers have increasingly begun pushing for their anti-cheat software to have complete control over gamers' computers: in the form of kernel-level anti-cheat. Since these anti-cheats have to be implemented as a *Windows driver* to do their thing, Wine/Proton [cannot emulate it][protonanticheat] and some game developers explicitly do this to lock Linux gamers out[^linuxanticheat].
|
||||
|
||||
[protonanticheat]: https://www.reddit.com/r/SteamDeckModded/comments/1b62ayh/error_when_installing_game_with_anticheat/
|
||||
|
||||
[^linuxanticheat]: They usually suggest that it's impossible to know all the different system configurations you can have with Linux and that allowing their games to run on Linux creates an "attack surface" to their anti-cheat systems. I'm here to point out that these kernel-level anti-cheat systems are [easily defeated with a Raspberry Pi][anticheatraspi], while gaming and cheating comfortably on Windows.
|
||||
|
||||
[anticheatraspi]: https://www.youtube.com/watch?v=RwzIq04vd0M
|
||||
|
||||
+++ What's a "kernel" and why is this such a big deal?
|
||||
When you turn on your computer, the system firmware (commonly referred to as "BIOS") initializes the hardware, then hands off control to an operating system. The first component to be loaded in this operating system is the **kernel.**
|
||||
|
||||
The kernel has full access to the hardware of the computer and manages system resources such as the CPU, memory, storage, network communications, input/output peripherals and other devices. It also manages processes and their access to these system resources. A kernel's functionality can be extended via device drivers so that it can work with hardware it doesn't know. These device drivers operate in the same space as the kernel itself. The kernel resides in a protected area of memory where normal applications don't have access to, so its core functions cannot be disrupted—whether intentional or unintentional—and robust, fault-free operation is ensured.
|
||||
|
||||
Kernel-level anti-cheat installs itself as a kernel driver in order to get more direct access to system resources and inspect processes running on the system. This enables it to block things like memory inspection, code injection and block or shut down cheat programs. The game then talks to this anti-cheat driver to get the information it needs to trigger certain functions, like banning cheaters.
|
||||
|
||||
However, this also comes with the security implication that when this anti-cheat has security vulnerabilities, a malicious actor can leverage the anti-cheat software to circumvent malware detection and wreak havoc without being detected and disabling further security features in your operating system to stay undetected. This is why people are criticizing game developers pushing kernel-level anti-cheat, because it's the equivalent of having a CIA agent sitting in your living room, watching everything you do at all times, and you can't get rid of them because their presence was obligatory to agree to in order for you to be allowed to move into the house.
|
||||
+++
|
||||
|
||||
Notable games with anti-cheat known to make them unplayable on Linux include:
|
||||
|
||||
- Fortnite
|
||||
- PUBG
|
||||
- Apex Legends
|
||||
- Roblox
|
||||
- Valorant
|
||||
- League of Legends
|
||||
- Rainbow Six: Siege
|
||||
- Destiny 2
|
||||
- GTA 5
|
||||
- Battlefield
|
||||
- EA Sports FC (previously: FIFA)
|
||||
|
||||
The website [Are We Anti-Cheat Yet][linuxanticheat] (sort by status, descending) has a more comprehensive list if you want to check for a game yourself. If your game is listed as "Denied" or "Broken", you might wanna sit this one out.
|
||||
|
||||
[linuxanticheat]: https://areweanticheatyet.com/
|
||||
|
||||
### Specialized hardware
|
||||
|
||||
There's a lot of specialized hardware out there, that is Windows or macOS only. You might get lucky and Linux support is provided by the community, but if manufacturers decide that they need to do something fancy, chances are this might lock Linux out.
|
||||
|
||||
Back when Linus Tech Tips did their Linux challenge in 2021 (to "celebrate" the release of Windows 11, no less), [several commentators](https://youtu.be/up2Za7luucU?t=427) recognized Linus uses a GoXLR mixer panel, which receives no *official* Linux support from the manufacturer. Naturally, Linus got not sound from it and was very confused. Since then, the Linux community has worked to improve support and [basic functionality][goxlrlinux] is available.
|
||||
|
||||
[goxlrlinux]: https://github.com/GoXLR-on-Linux
|
||||
|
||||
If you use such devices, check beforehand if they're compatible.
|
||||
|
||||
I've had similar experiences with older Elgato video grabbers that just wouldn't show up in OBS Studio. They required specialized firmware only available with the Windows drivers, so that was a bust.
|
||||
|
||||
Newer Elgato video grabbers work flawlessly, however, because they use standard USB protocols (i.e. UVC or [USB Video Class][usbvideoclass]). I was able to stream the entirety of Metroid Dread from my Nintendo Switch without a hitch. Check your video capture device for UVC compatibility.
|
||||
|
||||
[usbvideoclass]: https://en.wikipedia.org/wiki/USB_video_device_class
|
||||
|
||||
As a rule of thumb: if your USB devices need specialized drivers in order to work, the likeliness of them not working properly or at all under Linux is almost guaranteed and you might wanna sit this one out.
|
||||
|
||||
### NVIDIA
|
||||
|
||||
Unlike AMD, NVIDIA has a long history of not really wanting to work closely with the Linux community to make their graphics cards work as smoothly as on other platforms[^nvidialinuxgripes]. This causes some moderate to big pains with their graphics cards under the open operating system. In most cases these days, you don't really need to install any drivers for your hardware on Linux—they all come pre-packaged with the Linux kernel itself. Some hardware vendors do not want their driver code to be fully open, however, so they ship closed-source drivers that hook into the Linux kernel, like any other driver would.
|
||||
|
||||
Distributions like [Linux Mint][nvidiamint], [Ubuntu][nvidiaubuntu] and [Manjaro][nvidiamanjaro] offer driver installers that take care of correctly installing NVIDIA's proprietary drivers for you and setting everything. This is the concerted effort of the Linux community [trying hard to make NVIDIA work for Linux users][fedoranvidiarant] when NVIDIA themselves won't.
|
||||
|
||||
[^nvidialinuxgripes]: Linus Torvalds, the creator of the Linux kernel, [famously gave NVIDIA the finger](https://www.youtube.com/watch?v=MShbP3OpASA&t=2890s) during a Q&A panel in 2012, calling it the single worst company the kernel development community has ever had to deal with for how unconstructive and uncooperative they have been in the past.
|
||||
|
||||
[nvidiamint]: https://itsfoss.com/nvidia-linux-mint/
|
||||
[nvidiaubuntu]: https://www.linuxbabe.com/ubuntu/install-nvidia-driver-ubuntu
|
||||
[nvidiamanjaro]: https://wiki.manjaro.org/index.php/Manjaro_Settings_Manager
|
||||
[fedoranvidiarant]: https://archive.is/AVXSS
|
||||
|
||||
That's not to say it can't work. People report using their NVIDIA GPU under Linux most comfortably and NVIDIA has since also relented and is working on an [open source kernel module][nvidiaopen] for their newer cards, starting with the RTX series, and they're [contributing][nvidiacontrib] more to already existing projects. Gaming at full speed and CUDA can work under Linux, even on modern GPUs.
|
||||
|
||||
[nvidiacontrib]: https://www.phoronix.com/news/NVIDIA-Nouveau-Hopper-Blackwell
|
||||
|
||||
However, I've experienced considerable issues getting hardware-accelerated video playback to work in Firefox or Google Chrome on NVIDIA graphics cards before the RTX 2000 series in the past. NVIDIA requires you to use their closed-source tools and APIs that don't integrate into the ones any other Linux application uses to render videos on a modern Linux desktop. There's workarounds and other tools that try to bridge that gap, but it's finnicky and dependent on what NVIDIA GPU you actually happen to use. That is especially a bummer if you use a laptop because that results in faster battery drain.
|
||||
|
||||
There's also still some flakiness attached to dual-GPU setups, e.g. when there's a CPU with an integrated GPU and a dedicated NVIDIA GPU next to it in the same machine. Standby functionality and waking the machine back up from hibernation can, under specific circumstances, lead to a corrupted screen, because the Linux graphics subsystem was unable to get the display memory contents back. That means either having to hard reset the machine or turn off standby and hibernation entirely. Again, shitty if you're on a laptop.
|
||||
|
||||
[nvidiaopen]: https://github.com/NVIDIA/open-gpu-kernel-modules
|
||||
|
||||
So if you're on NVIDIA, know that there can still be some roadblocks ahead, *but it generally works for gaming and productivity* and things are improving slowly but surely.
|
||||
|
||||
If that's too much of finicking around for your tastes, however, you might wanna sit this one out.
|
||||
|
||||
### Creative software
|
||||
|
||||
If you're using your computer to do creative work you're likely using applications such as Adobe Photoshop, Cubase Pro and Adobe Premiere. These are, you guessed it, *not* available on Linux. However, alternatives both open source and commercial (paid) are available natively on Linux.
|
||||
|
||||
| Category | Application | Alternative on Linux |
|
||||
|------------------------|-------------------------------------------------------------------|-------------------------------------------|
|
||||
| Drawing, Photo Editing | Adobe Photoshop, Affinity Photo, Clip Studio Paint, PaintTool SAI | [GIMP] ([PhotoGIMP]), [Krita] |
|
||||
| Music Production (DAW) | Fl Studio, Avid Pro Tools, Cubase Pro | [Ardour], [Zrythm], [LMMS], [Reaper] |
|
||||
| Video Editing | Adobe Premiere, Vegas Pro, DaVinci Resolve | [Kdenlive], [OpenShot], [DaVinci Resolve] |
|
||||
|
||||
[GIMP]: https://www.gimp.org/
|
||||
[PhotoGIMP]: https://github.com/Diolinux/PhotoGIMP
|
||||
[Krita]: https://krita.org/
|
||||
|
||||
[Ardour]: https://ardour.org/
|
||||
[Zrythm]: https://www.zrythm.org/
|
||||
[LMMS]: https://lmms.io/
|
||||
[Reaper]: https://www.reaper.fm/
|
||||
|
||||
[Kdenlive]: https://kdenlive.org/
|
||||
[OpenShot]: https://www.openshot.org/
|
||||
[DaVinci Resolve]: https://www.blackmagicdesign.com/products/davinciresolve/
|
||||
|
||||
+++ Why not use Wine?
|
||||
While Wine has made tremendous improvements over the years, it isn't a magic bullet to make everything just work. Some applications do really weird stuff and requires extensive testing to find all of the edge cases. Going by the compatibility charts on [Wine's website][wineappdb] (because I don't have access to the software), I can deduct the following:
|
||||
|
||||
**Photo editing:** [Adobe software][adobelinux] especially runs very poorly, if it even runs at all. [Affinity Photo][affinitylinux] and Co. don't look good either. [Clip Studio Paint][csplinux] runs passible apparently. [PaintTool SAI][sailinux] also seems to be doing well.
|
||||
|
||||
**Audio production:** [Fl Studio][flstudiolinux] kinda works apparently. [Avid Pro][avidlinux] and [Cubase][cubaselinux] are a mixed bag. Add to that, that the audio programming landscape on Linux is [rather complex and convoluted][linuxaudioapi] and there exists no cross-platform API to ease the burden on developers of professional audio applications.
|
||||
|
||||
**Video editing:** As already stated, don't bother with Adobe. [Vegas Pro][vegaslinux] is also a mixed bag. DaVinci Resolve has a native Linux version of their editor, which I hear Linux YouTubers are very settled on, next to Kdenlive.
|
||||
+++
|
||||
|
||||
[wineappdb]: https://appdb.winehq.org/
|
||||
|
||||
[adobelinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=12&sAction=view&sTitle=View+Developer
|
||||
[affinitylinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=580&sAction=view&sTitle=View+Developer
|
||||
[csplinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=6462&sAction=view&sTitle=View+Developer
|
||||
[sailinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=2403&sAction=view&sTitle=View+Developer
|
||||
|
||||
[flstudiolinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=84&sAction=view&sTitle=View+Developer
|
||||
[avidlinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=264&sAction=view&sTitle=View+Developer
|
||||
[cubaselinux]: https://appdb.winehq.org/objectManager.php?bIsQueue=false&bIsRejected=false&sClass=vendor&iId=708&sAction=view&sTitle=View+Developer
|
||||
[linuxaudioapi]: https://0pointer.de/blog/projects/guide-to-sound-apis.html
|
||||
|
||||
[vegaslinux]: https://appdb.winehq.org/objectManager.php?sClass=application&iId=3467
|
||||
|
||||
https://www.youtube.com/watch?v=lm51xZHZI6g
|
||||
|
||||
I'm not gonna pretend that such a switch is easy or even viable. Habits die hard and having to learn entirely new tools from the ones that *worked before* slows you down to a crawl. I totally get that's a tough pill to swallow. One good thing about these alternatives is that you don't have to switch to Linux to give most of them a try, as they're cross-platform. You can try them out on your current operating system and see how you like it, if it works for you and whether you'd be willing to make a switch.
|
||||
|
||||
If you absolutely need your current creative software tools as your daily driver for work and can't afford to switch to something else, you might wanna sit this one out.
|
||||
|
||||
## It's up to you to make Windows 10 the last version of Windows you'll ever need
|
||||
|
||||
The forced upgrade to Windows 11 looms, and people are growing increasingly tired of Microsoft's antics—pushing changes no one asked for and slowly and covertly limiting how you can use your computer with every update. With the walls closing in, it begs the question: Is there a way out?
|
||||
|
||||
I hope I was able to show you that with Linux, there can be! It’s not perfect—there are still hurdles, especially if you rely on certain hardware or software—but [for as long as I've been using it][switchedtolinux], it’s come a long, long way. Linux is the operating system that gets out of your way. It doesn't decide for you, doesn't push updates you didn’t ask for, and doesn't patronize you. Where Microsoft keeps tightening its grip, Linux keeps the door wide open.
|
||||
|
||||
[switchedtolinux]: /posts/how-i-stopped-worrying-and-ended-up-using-linux-instead/
|
||||
|
||||
In the end, the choice is yours to make. But if I managed to make you even a little curious, I invite you to give it a try. And if you want me to, I'd be more than happy to show you how in future blog posts. Or if you prefer it face-to-face, look for a local tech support group or repair shop to [show you in person][endof10] and help you with any issues you run into.
|
||||
|
||||
[endof10]: https://endof10.org/
|
||||
|
||||
Linux is not only free as in free beer, but also as in freedom—the freedom for you to decide: Will ***you*** work for ***your computer?*** Or is ***it*** gonna work for ***you?***
|
261
src/posts/2025-07-03_nvidia-is-full-of-shit.md
Normal file
261
src/posts/2025-07-03_nvidia-is-full-of-shit.md
Normal file
|
@ -0,0 +1,261 @@
|
|||
---
|
||||
title: NVIDIA is full of shit
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/3cd4b2a8-7209-4e8c-9882-d80114a5cd9d
|
||||
alt: NVIDIA logo on a red background with jagged lines
|
||||
credit: Made with GIMP
|
||||
tags: ['nvidia']
|
||||
---
|
||||
|
||||
Since the disastrous launch of the RTX 50 series, NVIDIA has been unable to escape negative headlines: scalper bots are snatching GPUs away from consumers before official sales even begin, power connectors continue to melt, with no fix in sight, marketing is becoming increasingly deceptive, GPUs are missing processing units when they leave the factory, and the drivers, for which NVIDIA has always been praised, are currently falling apart. And to top it all off, NVIDIA is becoming increasingly insistent that media push a certain narrative when reporting on their hardware.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## What's an MSRP anyway?
|
||||
|
||||
Just like with every other GPU launch in recent memory, this one has also been ripe with scalper bots snatching up stock before any real person could get any for themselves. Retailers have reported that they've received [very little stock to begin with][nvidiagpustock]. This in turn sparked rumors about NVIDIA purposefully keeping stock low to make it look like the cards are in high demand to drive prices. And sure enough, on secondary markets, the cards go *way above* MSRP and some retailers have started to [bundle the cards with other inventory][nvidiabullshitbundles] (PSUs, monitors, keyboards and mice, etc.) to inflate the price even further and get rid of stuff in their warehouse people wouldn't buy otherwise—and you don't even get a working computer out of spending over twice as much as a GPU alone would cost you.
|
||||
|
||||
[nvidiagpustock]: https://forums.overclockers.co.uk/threads/patience-if-planning-to-buy-a-50-series.18998084/#post-37602115
|
||||
[nvidiabullshitbundles]: https://www.youtube.com/watch?v=8s4hxa2TjWY&t=464s
|
||||
|
||||
")
|
||||
|
||||
")
|
||||
|
||||
I had a look at GPU prices for previous generation models for both AMD and NVIDIA as recently as May 2025 and I wasn't surprised to find even RTX 40 series are still very much overpriced, with the GeForce RTX 4070 (lower mid-tier) starting at $800 (MSRP: $599), whereas the same money can get you a Radeon RX 7900 XT (the second best GPU in AMD's last generation lineup). The discrepancy in bang for buck couldn't be more jarring. And that's before considering that NVIDIA gave out defective chips to board partners that were [missing ROPs][nvidiamissrops] (Raster Operations Pipelines) from the factory, thus reducing their performance. Or, how NVIDIA put it in a statement to [The Verge][nvidiaropstheverge]:
|
||||
|
||||
[nvidiamissrops]: https://www.techpowerup.com/332884/nvidia-geforce-rtx-50-cards-spotted-with-missing-rops-nvidia-confirms-the-issue-multiple-vendors-affected
|
||||
|
||||
[nvidiaropstheverge]: https://www.theverge.com/news/617901/nvidia-confirms-rare-rtx-5090-and-5070-ti-manufacturing-issue
|
||||
|
||||
> We have identified a rare issue affecting less than 0.5% (half a percent) of GeForce RTX 5090 / 5090D and 5070 Ti GPUs which have one fewer ROP than specified. The average graphical performance impact is 4%, with no impact on AI and Compute workloads. Affected consumers can contact the board manufacturer for a replacement. The production anomaly has been corrected.
|
||||
|
||||
Those 4% can make an RTX 5070 Ti perform at the levels of an RTX 4070 Ti Super, completely eradicating the reason you'd get an RTX 5070 Ti in the first place. Not to mention that the generational performance uplift over the RTX 40 series was already received quite poorly in general. NVIDIA also had to later amend their statement to The Verge and admit the RTX 5080 was also missing ROPs.
|
||||
|
||||
It's adding insult to injury with the cards' general *unobtainium* and it becomes even more ridiculous when you compare NVIDIA to another trillion dollar company that is also in the business of selling hardware to consumers: Apple.
|
||||
|
||||
How is it that one can supply customers with enough stock on launch consistently for decades, and the other can't? The only reason I can think of is, that NVIDIA just doesn't care. They're making the big bucks with data center GPUs now, selling the shovels that drive the "AI" bullshit gold rush, to the point that selling to consumers is [increasingly becoming][nvidiadatacenter] a rounding error on their balance sheets.
|
||||
|
||||
[nvidiadatacenter]: https://www.visualcapitalist.com/nvidia-revenue-by-product-line/
|
||||
|
||||
## These cards are 🔥🔥🔥 (and not the good kind)
|
||||
|
||||
The RTX 50 series are the second generation of NVIDIA cards to use the 12VHPWR connector. The RTX 40 series became infamous as the GPU series with melting power connectors. So did they fix that?
|
||||
|
||||
[No][nvidiastillburn]. The cables can still melt, both on the GPU and PSU. It's a design flaw in the board of the GPU itself which cannot be fixed unless the circuitry of the cards is replaced with a new design.
|
||||
|
||||
[nvidiastillburn]: https://www.youtube.com/watch?v=Ndmoi1s0ZaY
|
||||
|
||||
With the RTX 30 cards, each power input (i.e. the cables from the power supply) had its own shunt resistor[^shuntresistor]. If one pin in a power input had not been connected properly, another pin would have had to take over in its stead. If both pins were not carrying any current, there would have been no phase on the shunt resistor and the card would not have started up. You'd get a black screen, but the hardware would still be fine.
|
||||
|
||||
[^shuntresistor]: A shunt resistor is a small electrical component in a circuit that measures how much current is flowing through a connection (typically from the PCIe power connectors from the power supply and optionally from the PCIe port). A graphics card uses this information to manage its power consumption, detect if that consumption is within safe operating parameters and, if not, perform an emergency shutdown to prevent damages.
|
||||
|
||||
https://www.youtube.com/watch?v=kb5YzMoVQyw
|
||||
|
||||
NVIDIA, in its infinite wisdom, changed this design starting with the RTX 40 series.
|
||||
|
||||
Instead of individual shunt resistors for each power input, the shunt resistors are now connected in parallel to all pins of the power input from a single 12VHPWR connector. Additionally, the lines are recombined behind the resistors. This mind-boggling design flaw makes it impossible for the card to detect if pins are unevenly loaded, since as much as the card is concerned, everything comes in through the same single line.
|
||||
|
||||
Connecting the shunt resistors in parallel also makes them pretty much useless since if one fails, the other will still have a phase and the card will happily keep drawing power and not be any the wiser. If the card is supplied with 100W on each pin and 5 of the 6 pins don't supply a current, then a single pin has to supply the entire 600W the card demands. No wire is designed for this amount of power draw. As a result, excessive friction occurs from too many electrons traveling through the cable all at once and it melts (see: [Joule heating]).
|
||||
|
||||
[Joule heating]: https://en.wikipedia.org/wiki/Joule_heating
|
||||
|
||||
NVIDIA realized that the design around the shunt resistors in the RTX 40 series was kinda stupid, so they revised it: by eliminating the redundant shunt resistor, but changing nothing else about the flawed design.
|
||||
|
||||
https://www.youtube.com/watch?v=oB75fEt7tH0
|
||||
|
||||
There's something to be said about the fact NVIDIA introduced the 12VHPWR connector to the ATX standard to allow for only a single connector to supply their cards with up to 600W of power but making it way less safe to operate at these loads. Worse yet, NVIDIA says the four "sensing pins" on top of the load bearing 12 pins are *supposed* to prevent the GPU from pulling too much power. The fact of the matter is, however, that the "sensing pins" only tell the GPU how much it's allowed to pull when the system *turns on*, but they **do not** continuously monitor the power draw—that would be for the shunt resistors on the GPU board, which we established, NVIDIA kept taking out.
|
||||
|
||||
If I had to guess, NVIDIA must've been *very confident* that the "sensing pins" are a suitable substitution for those shunt resistors in theory, but practice showed that they were not at all accounting for user error. That was their main excuse after after it blew up in their face and they investigated. And indeed, if the 12VHPWR connector isn't properly inserted, pins could not make proper contact, causing the remaining wires to carry more load. This is something that the "sensing pins" **cannot** detect, despite their name and NVIDIA selling it as some sort of safety measure.
|
||||
|
||||
 and its predecessor, the RTX 4090 FE (left) © [ZMASLO] ([CC BY 3.0]) via [Wikimedia]")
|
||||
|
||||
[ZMASLO]: https://www.youtube.com/watch?v=5nj1qLazPlk
|
||||
[CC BY 3.0]: https://creativecommons.org/licenses/by/3.0
|
||||
[Wikimedia]: https://commons.wikimedia.org/wiki/File:RTX_5090_-_du%C5%BCa_wydajno%C5%9B%C4%87_du%C5%BCym_kosztem_(2160p_30fps_VP9_LQ-96kbit_AAC)-00.05.04.568.png
|
||||
|
||||
NVIDIA also clearly did not factor in the computer cases on the market that people would pair these cards with. The RTX 4090 was ***massive,*** a real heccin chonker. It was so huge in fact, that it kicked off the trend of needing [support brackets] to keep the GPU from sagging and straining the PCIe slot. It also had its power connector sticking out to the side of the card and computer cases were not providing enough clearance to not bend the plug. As was [clarified][12vhpwrclarification] after the first reports of molten cables came up, bending a 12VHPWR cable without at least 35mm (1.38in) clearance could loosen the connection of the pins and create the problem of the melting connectors—something that wasn't a problem with the battle tested 6- and 8-pin PCIe connectors we've been using up to this point[^amdpowerconn].
|
||||
|
||||
[support brackets]: https://www.antec.com/product/accessory#gpu_bracket
|
||||
[12vhpwrclarification]: https://cablemod.com/12vhpwr/
|
||||
|
||||
[^amdpowerconn]: Which NVIDIA's main rival AMD [is not getting tired][amddunk] of pointing out.
|
||||
|
||||
[amddunk]: https://x.com/SasaMarinkovic/status/1593243804538372096
|
||||
|
||||
Board partners like ASUS try to work around that design flaw by introducing intermediate shunt resistors for each individual load bearing pin before the ones according to NVIDIA's designs, but these don't solve the underlying issue, that the card won't shut itself down if any of the lines aren't drawing enough or any power. What you get at most is an indicator LED lighting up and some software telling you "Hey, uh, something seems off, maybe take a look?"
|
||||
|
||||
The fact NVIDIA insists on keeping the 12VHPWR connector around and not do jack shit about the design flaws in their cards to prevent it from destroying itself from the slightest misuse should deter you from considering any card from them that uses it.
|
||||
|
||||
## A carefully constructed moat
|
||||
|
||||
Over the years NVIDIA has released a number of proprietary technologies to market that only work on their hardware—DLSS, CUDA, NVENC and G-Sync to just name a few. The tight coupling with with NVIDIA's hardware guarantees compatibility and performance.
|
||||
|
||||
However, this comes at a considerable price these days, as mentioned earlier. If you're thinking about an upgrade you're either looking at a down-payment on a house or an uprooting of your entire hardware and software stack if you switch vendors.
|
||||
|
||||
If you're a creator, CUDA and NVENC are pretty much indispensable, or editing and exporting videos in Adobe Premiere or DaVinci Resolve will take you a lot longer[^cudavideo]. Same for live streaming, as using NVENC in OBS offloads video rendering to the GPU for smooth frame rates while streaming high-quality video.
|
||||
|
||||
[^cudavideo]: AMD also has accelerated video transcoding tech but for some reason nobody seems to be willing to implement it into their products. I read that this might be because for the longest time AMD's AMF has been missing a crucial feature (namely b-frames) causing a significant drop in image quality compared to NVIDIA's NVENC. But still, the option would be nice, if only for people to not be artificially stuck on NVIDIA.
|
||||
|
||||
Speaking of games: G-Sync in gaming monitors also requires a lock-in with NVIDIA hardware, both on the GPU side and the monitor itself. G-Sync monitors have a special chip inside that NVIDIA GPUs can talk to in order to align frame timings. This chip is expensive and monitor manufacturers have to get certified by NVIDIA. Therefore monitor manufacturers charge a premium for such monitors.
|
||||
|
||||
The competing open standard is FreeSync, spearheaded by AMD. Since 2019, NVIDIA also supports FreeSync, but under their "G-Sync Compatible" branding. Personally, I wouldn't bother with G-Sync when a competing, open standard exists and differences are negligible[^gsyncstandby].
|
||||
|
||||
[^gsyncstandby]: Also, I would expect my display to not draw any power after I've physically powered it off—not stand-by, ***off.*** G-Sync displays were shown to still draw as much as [14W when turned off][gsyncwattoff], while a FreeSync display drew none, like you would expect.
|
||||
|
||||
[gsyncwattoff]: https://www.youtube.com/watch?v=Gxs5YxY2xXI&t=973s
|
||||
|
||||
### NVIDIA giveth, NVIDIA taketh away
|
||||
|
||||
https://www.youtube.com/watch?v=_dUjUNrbHis
|
||||
|
||||
The PC, as gaming platform, has long been held in high regards for its backwards compatibility. With the RTX 50 series, NVIDIA broke that going forward.
|
||||
|
||||
PhysX, which NVIDIA introduced into their GPU lineup with the acquisition of Ageia in 2008, is a technology that allows a game to calculate game world physics on an NVIDIA GPU. After the launch of the RTX 50 series cards it was revealed that they lack support for the 32-bit variant of the tech. This causes games like Mirror's Edge (2009) and Borderlands 2 (2012) that still run on today's computers to take ungodly dips into single digit frame rates, because the physics calculations are forcibly performed on the CPU instead of the GPU[^gamespreserv].
|
||||
|
||||
[^gamespreserv]: Obviously, this is bad for game preservation and backwards compatibility that the PC platform is known and lauded for. Another case of this is 3dfx's [Glide 3D graphics API], which was exclusive to their Voodoo graphics cards. It was superseded by general purpose technologies like Direct3D and OpenGL after 3dfx became defunct. NVIDIA's proprietary tech isn't becoming general purpose, as to allow competitors to compete on equal footing and on their own merits.
|
||||
|
||||
[Glide 3D graphics API]: https://en.wikipedia.org/wiki/Glide_(API)
|
||||
|
||||
Even though the first 64-bit consumer CPUs hit the market as early as 2003 (AMD Opteron, Athlon 64), 32-bit games were still very common around these times, as Microsoft would not release 64-bit versions of Windows to consumers until Vista in 2006[^64bitwindows]. NVIDIA later released the source code for the GPU simulation kernel on [GitHub][nvidiasimkernel]. The pessimist in me thinks they did this because they can't be bothered to maintain this themselves and offload that maintenance burden to the public.
|
||||
|
||||
[nvidiasimkernel]: https://github.com/NVIDIA-Omniverse/PhysX/discussions/384
|
||||
|
||||
[^64bitwindows]: The 64-bit version of Windows XP doesn't count, because it wasn't available to consumers.
|
||||
|
||||
### DLSS is, and always was, snake oil
|
||||
|
||||
Back in 2018 when the RTX 20 series launched as the first GPUs with hardware accelerated ray tracing, it sure was impressive and novel to have this tech in consumer graphics cards. However, NVIDIA also introduced upscaling tech alongside it to counterbalance the insane computational expense it introduced. From the beginning, the two were closely interlinked. If you wanted ray tracing in Cyberpunk 2077 (the only game at the time that really made use of the tech), you also had to enable upscaling if you didn't want your gameplay experience to become a (ridiculously pretty) PowerPoint slide show.
|
||||
|
||||
That upscaling tech is the now ubiquitous DLSS, or *Deep Learning Super Sampling*[^dlssmisnomer]. It renders a game at a lower resolution internally and then upscales it to the target resolution with specialized accelerator chips on the GPU die. The only issue back then was that because the tech was so new, barely any game made use of it.
|
||||
|
||||
[^dlssmisnomer]: The "Super Sampling" part of DLSS is already a misnomer. Super sampling in the traditional sense means rendering at a higher resolution and then downsampling the rendered images to the target resolution (e.g. render at 4K, downsample to 1440p). The point of this is to achieve better anti-aliasing results. Starting with DLSS 2.0 the NVIDIA tech does the *exact opposite*—rendering at a *lower* resolution and *upscaling* to the target resolution. The term might have had the correct meaning in DLSS 1.0, but not anymore with DLSS 2.0 onwards. Also, in DLSS 1.0 game devs needed to train the models themselves with high resolution footage of their game from every conceivable angle, light setting, environments, etc. which was probably prohibitively time consuming and hurt the tech's adoption. Later versions of DLSS changed this for a more generally trained model and uses information from the rendered frames of the game itself.
|
||||
|
||||
What always rubbed me the wrong way about how DLSS was marketed is that it wasn’t only for the less powerful GPUs in NVIDIA’s line-up. No, it was marketed for the top of the line $1,000+ RTX 20 series flagship models to achieve the graphical fidelity with all the bells and whistles. That, to me, was a warning sign that maybe, just maybe, ray tracing was introduced prematurely and half-baked. Back then I theorized, that by tightly coupling this sort of upscaling tech to high-end cards and ray traced graphics, it sets a bad precedent. The kind of graphics NVIDIA was selling us on were beyond the cards' actual capabilities.
|
||||
|
||||
Needing to upscale to keep frame rates smooth already seemed "fake" to me. If that amount of money for a single PC component still can't produce those graphics without using software trickery to achieve acceptable frame rates, then what am I spending that money for to begin with exactly?
|
||||
|
||||
Fast-forward to today and nothing has really changed, besides NVIDIA now charging double the amount for the flagship RTX 5090. And guess what? It still doesn't do Cyberpunk 2077—*the* flagship ray tracing game—with full ray tracing at a playable framerate in native 4K, only with DLSS enabled.
|
||||
|
||||
From the RTX 4090 website:
|
||||
|
||||
https://www.youtube.com/watch?v=QGI8EIgf8Y8
|
||||
|
||||
From the RTX 5090 website:
|
||||
|
||||
https://www.youtube.com/watch?v=_YXbkGuw3O8
|
||||
|
||||
| GPU | MSRP | CP2077 4K native RT Overdrive FPS |
|
||||
| :------: | :----: | :-------------------------------: |
|
||||
| RTX 4090 | $1,599 | ~20 FPS |
|
||||
| RTX 5090 | $1,999 | ~27 FPS |
|
||||
|
||||
So 7 years into ray traced real-time computer graphics and we're still nowhere near 4K gaming at 60 FPS, even at $1,999. Sure, you could argue to simply turn RT off and performance improves. But then, that's not why you spent all that money for, right? Pure generational uplift in performance of the hardware itself is miniscule. They're selling us a solution to a problem they themselves introduced and co-opted every developer to include the tech into their games. Now they're doing an even more computationally expensive version of ray tracing: path tracing. So all the generational improvements we could've had are nullified again.
|
||||
|
||||
And even if you didn't spend a lot of money on a GPU, what you get isn't going to be powerful enough to make those ray traced graphics pop and still run well. So most peoples' experience with ray tracing is: turn it on to see how it looks, realize it eats almost all your FPS and never turn it on ever again, thinking ray tracing is a waste. So whatever benefits in realistic lighting was to be achieved is also nullified, because developers will still need to do lighting the old-fashioned way for the people who don't or can't use ray tracing[^doomdarkages].
|
||||
|
||||
[^doomdarkages]: Unless you're *Doom: The Dark Ages* and don't allow people to turn it off.
|
||||
|
||||
Making the use of upscaling tech a requirement, at every GPU price point, for every AAA game, to achieve acceptable levels of performance gives the impression that the games we're sold are targeting hardware that either doesn't even exist yet or nobody can afford, and we need constant band-aids to make it work. Pretty much all upscalers force TAA[^upscalingtaa] for anti-aliasing and it makes the entire image on the screen look blurry as fuck the lower the resolution is.
|
||||
|
||||
[^upscalingtaa]: TAA, or Temporal Anti-Aliasing, is an anti-aliasing technique that uses past rendered frames to estimate where to apply smoothing to jagged edges of rendered graphics, especially with moving objects. TAA is very fast with minimal performance impact. The downside, however, is that using past frames causes ghosting artifacts and blurs motion much more visibly than FXAA (Fast Approximate Anti-Aliasing) or MSAA (Multi-Sampling Anti-Aliasing). The issue is, however, that rendering pipelines shifted to deferred rendering and heavy use of shaders that anti-aliasing techniques like MSAA don't work with, so TAA is the only viable option left, as outlined in this [DigitalFoundry deep-dive][dftaadeep].
|
||||
|
||||
[dftaadeep]: https://www.youtube.com/watch?v=WG8w9Yg5B3g
|
||||
|
||||
Take for example this *Red Dead Redemption 2* footage showing TAA "in action", your $1,000+ at work:
|
||||
|
||||
https://www.youtube.com/watch?v=GJ0eFYJYkkw
|
||||
|
||||
Frame generation exacerbates this problem further by adding to the ghosting of TAA because it guesstimates where pixels will *probably* go in an "AI" generated frame in between actually rendered frames. And when it's off it really **looks off.** Both in tandem look like someone smeared your screen with vaseline. And this is what they expect us to pay a premium for? For the hardware **and** the games?!
|
||||
|
||||
Combine that with GPU prices being absolutely ridiculous in recent years and it all takes on the form of a scam.
|
||||
|
||||
As useful or impressive a technology as DLSS might be, game studios relying as heavily on it as they do, is turning out to be detrimental to the visual quality of their games and incentivizes aiming for a level of graphical fidelity and complexity with diminishing returns. Games from 2025 don't look that dramatically different or better than games 10 years prior, yet they run way worse despite more modern and powerful hardware. Games these days demand such a high amount of compute that the use of upscaling tech like DLSS is becoming ***mandatory.*** The most egregious example of this being *Monster Hunter Wilds*, which states in its system requirements, that it **needs** frame generation to run at acceptable levels.
|
||||
|
||||

|
||||
|
||||
Meanwhile, Jensen Huang came up on stage during the keynote for the RTX 50 series cards and [proudly proclaimed][huangbullshit]:
|
||||
|
||||
> RTX 5070, 4090 performance at $549, impossible without artificial intelligence.
|
||||
|
||||
[huangbullshit]: https://www.youtube.com/live/k82RwXqZHY8?t=1125
|
||||
|
||||
What he meant by that, as it turns out, is the RTX 5070 only getting there with every trick DLSS has to offer, including new DLSS 4 Multi-Frame Generation only available on RTX 50 cards at the lowest quality setting and all DLSS trickery turned up to the max.
|
||||
|
||||
You cannot tell me this is anywhere near acceptable levels of image quality for thousands of bucks (video time-stamped):
|
||||
|
||||
https://www.youtube.com/watch?v=3nfEkuqNX4k&t=1176
|
||||
|
||||
Not only does that entail rendering games at a lower internal resolution, you also have to tell your GPU to pull 3 additional made up frames out of its ass so NVIDIA can waltz around claiming "Runs [insanely demanding game here] as 5,000 FPS!!!" for the *higher number = better* masturbator crowd. All the while the image gets smeared to shit, because NVIDIA just reinvented the motion smoothing option from your TV's settings menu, but badly and also it's "AI" now. Else what would all those Tensor-cores be doing than waste space on the GPU die that could've gone to actual render units? NVIDIA likes you to believe DLSS can create FPS out of thin air and they're trying to prove it with [dubious statistics][nvidiadlssstats]—only disclosing in barely readable fine print, that it's a deliberately chosen very small sample size, so the numbers look more impressive.
|
||||
|
||||
[nvidiadlssstats]: https://www.pcgamer.com/hardware/graphics-cards/92-percent-of-nvidia-users-turn-on-dlss-if-theyve-been-lucky-enough-to-bag-an-rtx-50-series-card-at-launch-and-have-the-nvidia-app-installed/
|
||||
|
||||
The resolution is fake, the frames are fake, too, and so is the marketed performance. Never mind that frame generation introduces input lag that NVIDIA needs to counter-balance with their "Reflex" technology, lest what you see on your screen isn't actually where you think it is because, again, the frames faked in by Frame Generation didn't originate from the game logic. They create problems for themselves, that they then create "solutions" for in an endless cycle of trying to keep up the smoke screen that these cards do more than they're actually equipped to do, so a 20% premium for a 10% uplift in performance has the faintest resemblance of justification[^dlsslatency].
|
||||
|
||||
[^dlsslatency]: And people just gobble it up because tech literacy and common sense are fucking dead!
|
||||
|
||||
I was afraid DLSS would get used to fake improvements where there are barely any back then and I feel nothing if not vindicated for how NVIDIA is playing it up, while jacking up prices further and further with each generation. None of that is raw performance of their cards. This is downright deceitful bullshit.
|
||||
|
||||
## The intimidations will continue until morale improves
|
||||
|
||||
NVIDIA lying on their own presentations about the real performance of their cards is one thing. It's another thing entirely, when they start bribing and threatening reviewers, to steer the editorial direction in NVIDIA's favor.
|
||||
|
||||
In December 2020, hardware review channel *Hardware Unboxed* [received an email][hubnvidia] from NVIDIA Senior PR Manager Bryan Del Rizzo, after they reviewed NVIDIA cards on pure rasterization performance without DLSS or ray tracing, saying that performance did not live up to their expectations:
|
||||
|
||||
[hubnvidia]: https://www.youtube.com/watch?v=wdAMcQgR92k
|
||||
|
||||
> Hi Steve,
|
||||
>
|
||||
> We have reached a critical juncture in the adoption of ray tracing, and it has gained industry wide support from top titles, developers, game engines, APIs, consoles and GPUs.
|
||||
>
|
||||
> As you know, NVIDIA is all in for ray tracing. RT is important and core to the future of gaming. But it's also only one part of our focused R&D efforts on revolutionizing video games and creating a better experience for gamers. This philosophy is also reflected in developing technologies such as DLSS, Reflex and Broadcast that offer immense value to consumers who are purchasing a GPU. They don't get free GPUs—they work hard for their money and they keep their GPUs for multiple years.
|
||||
>
|
||||
> Despite all of this progress, your GPU reviews and recommendations continue to focus singularly on rasterization performance and you have largely discounted all of the other technologies we offer to gamers. It is very clear from your community commentary that you do not see things the same way that we, gamers, and the rest of the industry do.
|
||||
>
|
||||
> Our Founders Edition boards and other NVIDIA products are being allocated to media outlets that recognize the changing landscape of gaming and the features that are important to gamers and anyone buying a GPU today—be it for gaming, content creation or studio and streaming.
|
||||
>
|
||||
> Hardware Unboxed should continue to work with out add-in card partners to secure GPUs to review. Of course, you will still have access to obtain pre-release drivers and press materials. That won't change.
|
||||
>
|
||||
> We are open to revisiting this in the future should your editorial direction change.
|
||||
|
||||
Hardware Unboxed was thus banned from receiving review samples of NVIDIA's Founder Edition cards. It didn't take long for NVIDIA to back-paddle after the heavily publicized outcry blew up in their face.
|
||||
|
||||
Which makes it all the more surprising, that a couple years later, they're trying to pull this again. With *Gamers Nexus* of all outlets.
|
||||
|
||||
https://www.youtube.com/watch?v=AiekGcwaIho
|
||||
|
||||
As Steve Burke explains in the video, NVIDIA approached him from the angle, that in order to still be given access to NVIDIA engineers for interviews and specials for their channel, Gamers Nexus needs to include Multi-Frame Generation metrics into their benchmark charts during reviews. Steve rightfully claims that this tactic of intimidating media by taking away access until they review NVIDIA cards in a way that agrees with the narrative NVIDIA wants to uphold, tarnishes the legitimacy of ***every*** review of every NVIDIA card ever made, past and present. It creates an environment of distrust that is not at all conductive when you're trying to be a tech reviewer right now.
|
||||
|
||||
This also coincided with the launch of the RTX 5060, a supposedly more budget friendly offering. Interestingly, NVIDIA did not provide reviewers with the necessary drivers to test the GPU prior to launch. Instead, the card and the drivers launched at the same time all of these reviewers were off at Computex, a computer expo in Taipei, Taiwan. The only outlets that did get to talk about the card prior to release were cherry-picked by NVIDIA, and even then it was merely *previews* of details NVIDIA allowed them to talk about, **not** independent *reviews.* Because if they would've been properly reviewed, they'd all come to the same conclusions: that the 8 GB of VRAM would make this $299[^budgetnotreally] "budget card" age very poorly because that is not enough VRAM to last long in today's gaming landscape.
|
||||
|
||||
[^budgetnotreally]: That's the MSRP of course, but as we already established, MSRPs are a complete wash with graphics cards, and JayzTwoCents demonstrates this in his [review][jayz5060] of the RTX 5060, with 3rd party offerings of the card adding as much as an $80 premium on top for diminishing little extra performance. Because, again, this card's Achilles' heel is the low amount of VRAM, and charging $80 over MSRP for only double-digit increases in MHz and call it "overclocked" is honestly insulting.
|
||||
|
||||
[jayz5060]: https://www.youtube.com/watch?v=fGn-_qj76sk&t=669s
|
||||
|
||||
But it probably doesn't matter anyways, because NVIDIA is also busy tarnishing the reputation of their drivers, [releasing hotfix after hotfix][nvidiadriverhotfix] in an attempt to stop their cards, old and new, from crashing seemingly randomly, when encountering certain combinations of games, DLSS and Multi-Frame Generation settings. Users of older generation NVIDIA cards can simply roll back to a previous version of the driver to alleviate these issues, but RTX 50 series owners don't get this luxury, because older drivers won't make their shiny new cards go.
|
||||
|
||||
[nvidiadriverhotfix]: https://www.theverge.com/news/653115/nvidia-gpu-drivers-black-screen-crashes-issues
|
||||
|
||||
## NVIDIA won, we all lost
|
||||
|
||||
With over 90% of the PC market running on NVIDIA tech, they're the clear winner of the GPU race. The losers are every single one of us.
|
||||
|
||||
Ever since NVIDIA realized there is tons of more money to be made on everything that is *not* part of putting moving pixels on a screen, they've taken that opportunity head on. When the gold rush for crypto-mining started, they were among the first to sell heavily price-inflated, GPU-shaped shovels to anybody with more money than brains. Same now with the "AI" gold rush. PC gamers were hung out to dry.
|
||||
|
||||
NVIDIA knows we're stuck with them and it's infuriating. They keep pulling their shenanigans and they will keep doing it until someone cuts them down a couple notches. But the only ones who could step up to the task won't do it.
|
||||
|
||||
AMD didn't even attempt at facing NVIDIA at the high-end segment this generation, instead trying to compete on merely the value propositions for the mid-range. Intel is seemingly still on the fence if they really wanna sell dedicated GPUs while shuffling their C-suite and generally being in disarray. Both of them could be compelling options when you're on a budget, if it just wasn't for the fact that NVIDIA has a longstanding habit of producing proprietary tech that only runs well on their hardware. Now they've poisoned the well with convincing everybody that ray tracing is something every game needs now and games that incorporate it do so on an NVIDIA tech-stack which runs like shit on anything that is not NVIDIA. That is not a level playing field.
|
||||
|
||||
When "The way it's meant to be played" slowly turns into "The only way it doesn't run like ass" it creates a moat around NVIDIA that's obviously hard to compete with. And gamers aren't concerned about this because at the end of the day, all they care about is that the game runs well and looks pretty.
|
||||
|
||||
But I want you to consider this: Games imbued with such tech creates a vendor lock-in effect. It gives NVIDIA considerable leverage in terms of how games are made, which GPUs you consider buying to run these games and how well they will eventually, actually run on your system. If all games that include NVIDIA's tech are made in a way that make it so you _have_ to reach for the more expensive models, you can be sure that's a soft power move NVIDIA is gonna pull.
|
||||
|
||||
And as we established, it looks like they're already doing that. Tests show that the lower-end NVIDIA graphics cards cannot (and probably were never intended to) perform well enough, even with DLSS, because in order to get anything out of DLSS you need more VRAM, which these lower-end cards don't have enough of. So they're already upselling you on more expensive models by cutting corners in ways that make it a "no-brainer" to spend more money on more expensive cards, when you otherwise wouldn't have.
|
||||
|
||||
And they're using their market dominance to control the narrative in the media, to make sure you keep giving them money and keep you un- or at the very least misinformed. When you don't have to compete, but don't have any improvements to sell either (or have no incentive for actual, real R&D) you do what every monopolist does and wring out your consumer base until you've bled them dry.
|
||||
|
||||
A few years back I would've argued that that's their prerogative if they provide the better technical solutions to problems in graphics development. Today, I believe that they are marauding monopolists, who are too high on their own supply and they're ruining it for everybody. If NVIDIA had real generational improvements to sell, they wouldn't do it by selling us [outright lies][nvidia5050].
|
||||
|
||||
[nvidia5050]: https://www.youtube.com/watch?v=caU0RG0mNHg
|
||||
|
||||
And I hate that they're getting away with it, time and time again, for over seven years.
|
360
src/posts/2025-07-28_collective-shout-puritan-agenda.md
Normal file
360
src/posts/2025-07-28_collective-shout-puritan-agenda.md
Normal file
|
@ -0,0 +1,360 @@
|
|||
---
|
||||
title: Puritans Know No Mercy
|
||||
image:
|
||||
src: https://img.sebin-nyshkim.net/i/d2aa735d-9a71-49e0-9b74-a31dc90d8373
|
||||
alt: Close-up of a person holding several cleaning products
|
||||
credit: Photo by Kelly Sikkema on Unsplash
|
||||
tags: ["censorship", "gaming", "lgbtq+"]
|
||||
---
|
||||
|
||||
Gaming storefronts [Steam] and [itch.io] have come under fire recently for taking NSFW games and media off their platforms or preventing such contents from showing up in search results. Both Steam and itch.io have been known for their lenient policy regarding such games and media and nobody seemingly bat an eye over it—until an Australian activist group entered the picture.
|
||||
|
||||
[Steam]: https://store.steampowered.com
|
||||
[itch.io]: https://itch.io
|
||||
|
||||
<!-- more -->
|
||||
|
||||
That group's name is **Collective Shout**, founded and lead by Australian political activist and "pro-life feminist" [*Melinda Tankard Reist*][wikipedia-melinda-tankard-reist]. The movement describes itself on its [About page][collectiveshout-about] as *"a grassroots campaigns movement against the objectification of women and the sexualisation of girls."*
|
||||
|
||||
[wikipedia-melinda-tankard-reist]: https://en.wikipedia.org/wiki/Melinda_Tankard_Reist
|
||||
[collectiveshout-about]: https://www.collectiveshout.org/about
|
||||
|
||||
## Exploiting the already vulnerable for a puritanical agenda
|
||||
|
||||
At first glance, campaigning against the sexualization of women and girls reads like a laudable goal. However, the rest of the About page quickly drifts into the typical puritanical rhetoric and seeks to blame the problem on a "pornified" society that allegedly endorses such abuse against women and girls if it can get them off. They deliberately create this bogeyman, once again using children as a front—a pattern common among puritans and right-wing activists pushing for nothing less than strict gender role norms as they see them. Their radical anti-porn stance also classifies pretty much anything LGBTQ+ related as "sexually deviant", further proving that this has nothing to do with protecting women, children or anybody else for that matter. The Venn diagram between these two groups is very much a circle.
|
||||
|
||||
And indeed, *Collective Shout* has the backing of several prominent religious and anti-porn activist groups, as ([now former][valens-bsky]) reporter for VICE, Ana Valens, [writes][archive-vice-collective-shout]:
|
||||
|
||||
> On July 11th, Collective Shout [published an open letter][collective-shout-open-letter] to the CEOs behind PayPal, MasterCard, Visa, Paysafe, Discover, and JCB. On the post, Collective Shout includes signatures from executives at such censorship-prone organizations as National Center on Sexual Exploitation (NCOSE) and Exodus Cry. Other allies include the anti-porn groups Coalition Against Trafficking in Women and the U.K. org CEASE. Both NCOSE and Exodus Cry have previously encouraged the removal of certain online content they deem harmful, with NCOSE in particular taking a strong focus on Steam.
|
||||
>
|
||||
> In 2018, NCOSE previously targeted a series of visual novels on Steam, briefly threatening the removal of these titles on Valve's digital storefront. Steam ultimately reversed its decision to ban these games, instead opening the door to adult content on the platform. Since 2018, NCOSE has repeatedly mentioned Steam in its various articles, almost as if the anti-porn organization has been waiting for an opportunity to go viral with a censorship campaign against the platform. Did the group play a pivotal role in pressuring American payment processors to change their policies toward Steam? It's plausible. NCOSE, which originally began as the religious “Morality in Media” organization, is a conservative group based in the U.S.
|
||||
|
||||
[valens-bsky]: https://bsky.app/profile/acvalens.net/post/3lufjdqmhxs2v
|
||||
[archive-vice-collective-shout]: https://web.archive.org/web/20250719204151/https://www.vice.com/en/article/group-behind-steam-censorship-policies-have-powerful-allies-and-targeted-popular-games-with-outlandish-claims/
|
||||
[collective-shout-open-letter]: https://www.collectiveshout.org/open-letter-to-payment-processors
|
||||
|
||||
They also maintain close ties with Australia's eSafety Regulator, giving them a direct line to the government. The eSafety Regulator is also part of the *Global Online Safety Regulators Network* (GOSRN), [which lists the following members][gosrn-member-list]:
|
||||
|
||||
[gosrn-member-list]: https://www.esafety.gov.au/about-us/consultation-cooperation/international-engagement/the-global-online-safety-regulators-network#membership
|
||||
|
||||
* [eSafety Commissioner – Australia](https://www.esafety.gov.au/homepage)
|
||||
* [Arcom – France](https://www.arcom.fr/)
|
||||
* [Autoriteit online Terroristisch en Kinderpornografisch Materiaal (ATKM) – the Netherlands](https://www.atkm.nl/)
|
||||
* [Coimisiún na Meán – Ireland](https://www.cnam.ie/coimisiun-na-mean-sets-out-plans-to-enhance-online-safety-and-to-regulate-and-support-irish-media-sector/) (Vice Chair)
|
||||
* [Council for Media Services – Slovakia](https://rpms.sk/)
|
||||
* [Film and Publication Board – South Africa](https://www.fpb.org.za/)
|
||||
* [Korea Communications Standards Commission – Republic of Korea](https://www.kocsc.or.kr/eng/mainPage.do)
|
||||
* [Office of Communications (Ofcom) – United Kingdom](https://www.ofcom.org.uk/home) (Chair)
|
||||
* [Online Safety Commission – Fiji](https://onlinesafetycommission.com/)
|
||||
|
||||
*Collective Shout* also, unsurprisingly, doesn't care much to familiarize itself with the subject matter of the games or media it's trying to get banned, as Valens further outlines:
|
||||
|
||||
> In 2018, Collective Shout encouraged its supporters to sign a petition to ban [Quantic Dream's *Detroit: Become Human*][collective-shout-detroid-tweet] from sale in Australia, claiming the game features “child abuse and violence against women.” The petition focused on an abusive father's violent behavior toward his housekeeper and daughter in the game. This dynamic, core to the character Kara's story arc, is intended to encourage empathy for the abused woman and child. While it's unclear whether Collective Shout is actively targeting *Detroit: Become Human* in 2025, the removal of such a game would be akin to artistic censorship of material discussing misogynistic abuse against female family members. Targeting the game, in other words, could be considered anti-feminist in intent.
|
||||
|
||||
[collective-shout-detroid-tweet]: https://x.com/CollectiveShout/status/964663931439480834
|
||||
|
||||
A [previous article][archive-vice-collective-shout-2], also by Valens, also questioned the reasoning of *Collective Shout* and the games they target:
|
||||
|
||||
> Without further proof from Collective Shout on the supposed games in question, it's hard to say whether child or childlike characters existed in *any* of the games targeted by the organization. It's certainly plausible that Collective Shout is disingenuously describing adult anime characters in adult video games as underage.
|
||||
|
||||
Both of Valens' articles are only available in archived form, since VICE operator, Savage Ventures, ordered their removal shortly after they were published, seemingly because Valens was among the first to highlight a connection between Steam's updated rules, the sudden removal of adult games and the involvement of *Collective Shout*, who probably didn't appreciate being challenged the way Valens dared. Valens and other writers [subsequently quit VICE in protest][valens-bsky].
|
||||
|
||||
[archive-vice-collective-shout-2]: https://archive.ph/USxe6
|
||||
[valens-bsky]: https://aftermath.site/waypoint-quit-steam-vice
|
||||
|
||||
Valens' articles show a clear pattern: *Collective Shout* takes a very shallow and vibes-based approach to targeting games and media for their campaigns. The smallest hint or mention in games and media of the things they are outspoken against is enough to set them off. They don't shy away from ripping things completely out of context and giving it only a very surface-level look and treatment if it furthers their case. Even when the games and media in question aim to make the audience sympathize with the victimized groups *Collective Shout* claims to advocate for, they want that thing nuked from orbit.
|
||||
|
||||
In their [open letter][collective-shout-open-letter], they talk about "hundreds of other games featuring rape, incest and child sexual abuse on both Steam and Itch.io", yet conveniently omit any mention of what games they're talking about, so nobody can challenge them on it, citing that "most of the content found within the games, including the graphics and the developers descriptions, are too distressing for us to make public."
|
||||
|
||||
*Collective Shout* is going by the same playbook we've seen dimes-a-dozen by now: make a highly sensationalized point, inflate the numbers to make it sound especially egregious, but be suspiciously tight-lipped about details, other than repeating their mantra-like ramblings ad nauseam about wanting to protect children and women.
|
||||
|
||||
## Payment processors force platforms to bend the knee
|
||||
|
||||
Unfortunately, *Collective Shout* successfully ["jawboned"][jawboning] Steam and itch.io by sending the payment processors after them. Steam has amended their [developer onboarding documentation][steamworks-onboarding] with a new rule about what a developer should not publish on Steam (emphasis mine):
|
||||
|
||||
[jawboning]: https://knightcolumbia.org/blog/six-things-about-jawboning
|
||||
|
||||
> 15. Content that ***may*** violate the rules and standards set forth by Steam's payment processors and related card networks and banks, or internet network providers. In particular, certain kinds of adult only content.
|
||||
|
||||
[steamworks-onboarding]: https://partner.steamgames.com/doc/gettingstarted/onboarding?language=english#5
|
||||
|
||||
In amending their guidelines in this way it becomes clear that Valve had to cave to payment processors' demands to either "remove the stuff or have the relationship terminated."
|
||||
|
||||
Valve later [admitted][rps-valve-nsfw-ban] as much to the media to have added this rule after being approached by said payment processors about the adult-oriented games on their platform and the subsequent blanket [delisting of games][bsky-steamdb-removed-games].
|
||||
|
||||
[rps-valve-nsfw-ban]: https://www.rockpapershotgun.com/valve-are-now-removing-a-bunch-of-sex-games-from-steam-to-keep-banks-and-card-companies-happy
|
||||
[bsky-steamdb-removed-games]: https://bsky.app/profile/steamdb.info/post/3lu32vdlsmg27
|
||||
|
||||
What's particularly interesting in this context is, that Steam already had rules in place that specifically govern the availability of adult-oriented games on Steam:
|
||||
|
||||
> 3. Adult content that isn't appropriately labeled and age-gated
|
||||
>
|
||||
> […]
|
||||
>
|
||||
> 6. Content that violates the laws of any jurisdiction in which it will be available
|
||||
> 7. Content that is patently offensive or intended to shock or disgust viewers
|
||||
|
||||
By adding rule 15, Valve is handing over the control of their own platform to the whims of banks and credit card companies.
|
||||
|
||||
Similarly, developers on itch.io very abruptly found their games delisted from search and their accounts up for review. What initially drew a lot of anger from people is that it happened without much of any reasons given. Itch.io came forth with a [statement][itch-statement] some time later:
|
||||
|
||||
> Recently, we came under scrutiny from our payment processors regarding the nature of some content hosted on itch.io. Due to a game titled No Mercy, which was temporarily available on itch.io before being banned back in April, the organization [Collective Shout][collective-shout-open-letter] launched a campaign against Steam and itch.io, directing concerns to our payment processors about the nature of certain content found on both platforms.
|
||||
>
|
||||
> Our ability to process payments is critical for every creator on our platform. To ensure that we can continue to operate and provide a marketplace for all developers, we must prioritize our relationship with our payment partners and take immediate steps towards compliance.
|
||||
>
|
||||
> This is a time critical moment for itch.io. The situation developed rapidly, and we had to act urgently to protect the platform's core payment infrastructure. Unfortunately, this meant it was not realistic to provide creators with advance notice before making this change. We know this is not ideal, and we apologize for the abruptness of this change.
|
||||
|
||||
[itch-statement]: https://itch.io/updates/update-on-nsfw-content
|
||||
|
||||
> [!note] Update 2025-07-31
|
||||
> Removed mention of [*Consume Me*][itch-consume-me] and [*Mouthwashing*][itch-mouthwashing]. Neither Consume Me nor Mouthwashing have been listed on itch.io search results since [2018](https://bsky.app/profile/leafo.itch.io/post/3lv2pk2viz22q) and [2024](https://bsky.app/profile/itch.io/post/3lv2hlptfos2x), respectively, due to not meeting indexing criteria, i.e. not hosting their files on itch.io.
|
||||
|
||||
[itch-consume-me]: https://q_dork.itch.io/consume-me
|
||||
[itch-mouthwashing]: https://kasuraga.itch.io/mouthwashing
|
||||
|
||||
Among the casualties of games that no longer appear in the search results because of the fallout caused by *Collective Shout* is [*Radiator 2*][itch-radiator-2] by Robert Yang, former teacher at New York University's Game Center, which attempts to "expand eroticism in games beyond a cutscene", not by depicting intercourse, but more abstract and silly concepts, like:
|
||||
|
||||
* "spank the heck out of a dude and learn about how BDSM communities formalize consent / caring"
|
||||
* "a short interactive music video game thing where you help a hunk enjoy a delicious shapely treat"
|
||||
* "an autoerotic night-driving game about pleasuring a gay car"
|
||||
|
||||
[itch-radiator-2]: https://radiatoryang.itch.io/radiator2
|
||||
|
||||
This has absolutely nothing in common with the extreme contents that *Collective Shout* and similar bigoted advocacy groups claim to campaign against "to protect women and girls from harm." This protects no-one and robs everyone of unique experiences that equally don't hurt anybody! They went for the payment processors specifically to suffocate gaming platforms, because there's a disproportionate amount of LGBTQ+ and minority media on Steam and itch.io, the latter of which has an even higher percentage of games and media by LGBTQ+ folk for a primarily queer audience. This also includes furries, which regularly have to defend themselves against accusations of bestiality and various interpretations of "sexual deviancy."
|
||||
|
||||
If *Collective Shout* were being honest, they would also take a stance against something like Game of Thrones, which had a ***far bigger*** audience and was full of things that these hypocrites could have taken offense to, i.e. all of the incest. So, imagine my surprise when I searched for the show on their website and came up with… [nothing][collective-shout-game-of-thrones]! Nice double standard you've got there! This implies to me that they lie through their teeth what this is actually about—namely hurting the previously mentioned minority groups and calling it "protecting children."
|
||||
|
||||
[collective-shout-game-of-thrones]: https://www.collectiveshout.org/search_results?q=Game+of+Thrones
|
||||
|
||||
## US payment processors hold too much power
|
||||
|
||||
This is just the latest in a series of events that shows how brittle payment systems online are. Visa and MasterCard hold a [precarious duopoly][visa-mastercard-marketshare] over the global cash-free payment market, with 1.5 billion (37%) and 1.1 billion (32%) cards, respectively, in circulation globally.
|
||||
|
||||
This naturally translates to online payment, where this market dominance will force anybody's hand if they're hoping to offer their services to a global audience. This gives them tremendous power not only over Steam and itch.io, but also sites like Patreon, which back in [2014][patreon-payment-processor-2014] and [2017][patreon-payment-processor-2017] faced similar pressure from payment processors, threatening to cut ties with Patreon altogether over hosting adult-oriented content, which would've basically killed it overnight.
|
||||
|
||||
[visa-mastercard-marketshare]: https://www.fool.com/money/research/credit-debit-card-market-share-network-issuer/
|
||||
[patreon-payment-processor-2014]: https://www.engadget.com/2015-12-02-paypal-square-and-big-bankings-war-on-the-sex-industry.html
|
||||
[patreon-payment-processor-2017]: https://www.bbc.com/news/technology-41749885
|
||||
|
||||
Other sites that faced similar issues include:
|
||||
|
||||
* fansly ([June 28, 2025][fansly-policy-update]): Updates policies to ban content such as anthropomorphic characters (furry), or calling yourself "puppy", "cat girl" or VTubing as such, because payment providers classify it as bestiality, hypnosis/mind control regardless of context, amateur or party wrestling as non-con and any sort of public space & recording locations.
|
||||
* Suruga-ya ([May 20, 2025][surugaya-policy-update]): The Japanese store removed adult works after pressure from payment processors. In similar fashion, other sites like DLSite, Fantia, Melonbooks and DMM stopped offering Visa and/or MasterCard as payment options, even after some of them tried to appease to these payment processors' policies.
|
||||
* Gumroad ([March 15, 2024][gumroad-policy-update]): In order to keep PayPal on board as a payment processor, Gumroad banned any and all adult content on the site, followed by a mass-exodus of artists.
|
||||
* pixiv ([Nov 15, 2022][pixiv-policy-update]): The Japanese art sharing site tightened its rules about how adult contents can be monetized on their paid offerings BOOTH and pixivFANBOX, after pressure from payment processors. Later, they [announced](https://nichegamer.com/pixiv-will-block-nsfw-content-for-users-in-us-and-uk/) that they were going to block US and UK viewers from accessing adult-oriented materials, starting on 25th April 2024.
|
||||
* OnlyFans ([Aug 19, 2021][onlyfans-policy-update]): The site infamous for being a smash hit for adult-oriented performers announces that sexually explicit contents are no longer permitted. After public outcry, the decision was [rolled back](https://www.theverge.com/2021/8/25/22640988/onlyfans-no-ban-porn-sexually-explicit-content-creators) a week later.
|
||||
|
||||
[fansly-policy-update]: https://x.com/battiechan_/status/1937262078374654183
|
||||
[surugaya-policy-update]: https://crazyforanimetrivia.com/surugaya-store-removes-adult-games/
|
||||
[gumroad-policy-update]: https://techcrunch.com/2024/03/15/gumroad-no-longer-allows-most-nsfw-art-leaving-its-adult-creators-panicked/
|
||||
[pixiv-policy-update]: https://nichegamer.com/pixiv-new-content-restriction/
|
||||
[onlyfans-policy-update]: https://www.theverge.com/2021/8/19/22632797/onlyfans-prohibit-sexually-explicit-content-porn-creators
|
||||
|
||||
PayPal is of particular mention here, as their controversial business decisions to their customers' detriment are [well-documented][paypal-criticism]. PayPal has since garnered a reputation of stealing NSFW artists' money, by freezing their account when anything of an NSFW nature is mentioned in e.g. a payment note, thereby disallowing them to move funds out of their PayPal account.
|
||||
|
||||
[paypal-criticism]: https://en.wikipedia.org/wiki/PayPal#Criticism_and_controversies
|
||||
|
||||
> [!important] A word to artists
|
||||
> If you're an artist using PayPal for your services, I urge you to take funds out of your PayPal ***immediately*** as soon as you get paid. Also, use PayPal's [invoice system][paypal-invoices] so you have full control over what appears on the transaction history what you were paid for.
|
||||
|
||||
[paypal-invoices]: https://securepayments.paypal.com/c2/cshelp/article/how-do-i-create-and-send-an-invoice-help319?locale.x=en_C2
|
||||
|
||||
*Collective Shout* correctly identified the weak point of every platform, company or individual wanting to do business online: threaten their ability to get paid.
|
||||
|
||||
> [!note] Update 2025-07-29
|
||||
> A friend sent me an article in which NieR: Automata director Yoko Taro rightly notes that the dominance US payment processors have gives them leverage over another country's autonomy.
|
||||
|
||||
Visa's and MasterCard's duopoly also has other implications: since they're corporations based in the United States, operating within the United States' legal framework, it gives the United States itself an uncomfortable leverage over the rest of the world. Suddenly, what is [perfectly legal][yoko-taro-payment-processors] in one country is effectively overruled if the same thing is illegal in the US or what intermediaries there think is acceptable use and what is not. It has the potential to greatly impact other countries' legal, political and economical autonomy. And the United States is [not shy][eu-us-tariff-talks] about using its dominance in certain areas to force other countries to cater to the interests of the United States and those of US lobby groups and corporations.
|
||||
|
||||
[yoko-taro-payment-processors]: https://automaton-media.com/en/news/nier-creator-speaks-out-against-payment-processors-pressuring-japanese-adult-content-platforms/
|
||||
[eu-us-tariff-talks]: https://www.bbc.com/news/articles/c14g8gk8vdlo
|
||||
|
||||
You might be inclined to think "Well, why won't they just process payments themselves?"
|
||||
|
||||
Steam is big, but not *that* big. And certainly neither is itch.io. Rolling your own payment processing entangles you into a lot of regulatory obligations even Steam is not prepared to deal with. Their forte is games, not banking infrastructure and the regulatory compliance therein.
|
||||
|
||||
There are, however, [alternatives][payment-alternatives] on the rise. One such alternative is [WERO], a payment system in which banks process payments directly among each other, currently available in Germany, France and Belgium, with plans to roll out to all of Europe in 2026. On the US side there's [FedNOW] by the federal reserve bank which aims to do the same, with a small number of banks involved. Check if your bank supports these and if not ask them to do so.
|
||||
|
||||
[payment-alternatives]: https://tech.lgbt/@pq1r/114907824163916258
|
||||
[WERO]: https://wero-wallet.eu/
|
||||
[FedNOW]: https://www.frbservices.org/financial-services/fednow
|
||||
|
||||
There's also been calls to support [S. 401] or the *Fair Access to Banking Act,* which aims to *"amend the Federal Reserve Act to prohibit certain financial service providers who deny fair access to financial services."* However, the bill is primarily backed by Republicans and as outlined on Republican senator Kevin Cramer's [own website](https://www.cramer.senate.gov/news/press-releases/cramer-reintroduces-fair-access-to-banking-act-to-protect-legal-industries-from-debanking), it is aimed at banks not wanting to fund primarily Republican idealized, obviously idiotic and actually harmful things:
|
||||
|
||||
> Cramer's legislation is a response to United States banks and financial institutions increasingly using their economic standing to categorically discriminate against legal industries and conservatives. For example, Citigroup instituted a [policy](https://www.citigroup.com/rcs/citigpa/akpublic/storage/public/Environmental-and-Social-Policy-Framework.pdf) in 2018 to withhold project-related financing for coal plants, and in 2020, five of the country's largest banks [announced](https://thehill.com/policy/finance/498307-trump-energy-secretary-accuses-banks-of-redlining-oil-and-gas-indusry) they would not provide loans or credit to support oil and gas drilling in the Arctic National Wildlife Refuge, despite explicit congressional authorization. Such exclusionary practices also extend to industries protected by the Second Amendment, with Capital One, among other banks, previously including “ammunitions, firearms, or firearm parts” in the [prohibited payments](https://www.capitalone.com/legal/corporate/terms) section of its corporate policy manual, and payment services like Apple Pay and PayPal denying their services for transactions involving firearms or ammunition.
|
||||
|
||||
[S. 401]: https://www.congress.gov/bill/119th-congress/senate-bill/401/text
|
||||
|
||||
I urge you to think twice before calling for support for this bill, lest we'll have a [Monkey's Paw][wikipedia-monkeys-paw] situation on our hands.
|
||||
|
||||
[wikipedia-monkeys-paw]: https://en.wikipedia.org/wiki/The_Monkey%27s_Paw
|
||||
|
||||
And before anyone even thinks about bringing up crypto currency as the alternative: Kindly seek your nearest bus for some much needed face time to remove yourself from this gene pool! I can't think of anything less suitable than this bait-and-switch nothing burger tech bro eyewash bigger fool pyramid scheme, dressed up as "currency" that's even more vibe-based and less regulated than the stock market already is. Ultimately, it's nothing more than imposed recreational therapy for computers, involving fun number guessing games that contribute to setting our planet on fire even more than it already is by so-called "AI."
|
||||
|
||||
## What you can do
|
||||
|
||||
> [!warning] Don't waste your energy yelling at the agitators
|
||||
>
|
||||
> I know it's tempting to give Collective Shout a piece of your mind, but I need you to understand that ***you're only giving them more ammunition for their case!***
|
||||
>
|
||||
> They've already [posted to their website][collective-shout-abuse] the most vile shit they've received after their involvement got public. They'll use this to further prove their puritan talking points!
|
||||
>
|
||||
> ***TAKE THAT ENERGY AND FILE COMPLAINTS WITH THE PAYMENT PROCESSORS INSTEAD!!!***
|
||||
|
||||
[collective-shout-abuse]: https://www.collectiveshout.org/gamers-threats-and-abuse
|
||||
|
||||
Resistance against *Collective Shout* has been ramping up over the past couple of days online.
|
||||
|
||||
People have compiled the phone numbers of various banks and credit card companies to file complaints with them and let them know that it is none of their goddamn business how people spend their own money.
|
||||
|
||||
One such compilation can be found at the aptly named website [YellAt.Money](https://yellat.money/) and another one at [stop-paypros.neocities.org](https://stop-paypros.neocities.org/). These go beyond phone numbers, email addresses and online forms, they also provide templates, phone scripts, "points to hit", links to petitions and explain how Australian citizens can [contact the Australian government][acnc-complaints] agency ACNC to file complaints against *Collective Shout* (their [register entry][acnc-collective-shout-entry] can be seen here) for lobbying political action, which could threaten their status as a charity.
|
||||
|
||||
[acnc-complaints]: https://www.acnc.gov.au/raise-concern
|
||||
[acnc-collective-shout-entry]: https://www.acnc.gov.au/charity/charities/8f14b7df-39af-e811-a963-000d3ad24077/profile
|
||||
|
||||
Staff of the hotlines of the payment processors have reportedly started to ask people to pivot to email. This is probably a desperate attempt by the call centers to cope with the sheer volume of incoming calls, because calls take priority over emails.
|
||||
|
||||
***Don't let them shut you down!***
|
||||
|
||||
**KEEP CALLING!!!**
|
||||
|
||||
If they hang up on you, **immediately call again and ask for a supervisor,** because you were just hung up on!
|
||||
|
||||
As soon as service levels drop considerably for an extended period of time, **they will take note!**
|
||||
|
||||
Tips from [Bluesky][bsky-callcenter-tips]:
|
||||
|
||||
[bsky-callcenter-tips]: https://bsky.app/profile/islandwarlock.bsky.social/post/3lusk6dkpek2z
|
||||
|
||||
> * Forecast accuracy needs to be off for at least a week before I notice / bring it to management's attention. What is off? Up by at least 10%. I don't know what normal volume is at MC or Visa, but that's one goal to get attention.
|
||||
> * Increased handle times reeeealllly stand out. If every call is taking, on average, even 3 seconds longer than normal, that is typically worth one additional body in a seat. 30-60 seconds and suddenly we're talking of adding an unexpected new hire class, which costs soooo much money
|
||||
> * Service level needs to drop for at least a month. This is how most call centers decided if they had a good month or not. Most times this means answering 80% of all calls in under 30 seconds. More and longer calls makes that harder.
|
||||
> * Another monthly metric to keep in mind is first call resolution (FCR). A lot of places these days don't want customers calling back, and they measure this, typically against a goal of 90%+. Tanking this number will typically get an equal amount of attention as service level.
|
||||
|
||||
Having said that, if you do decide to call them, I want to be very clear about one thing:
|
||||
|
||||
***Be assertive, but don't be an asshole!***
|
||||
|
||||
The people on the other end have the least amount of control over this whole fiasco. They don't deserve your ire, the management at the payment processors and *Collective Shout* does. If it only took them a little over 1,000 calls to get what they want, I think we can make our voices heard orders of magnitude louder!
|
||||
|
||||
[Another tip][bsky-visa-automated-trick] on Bluesky talks about how to more easily get through to a call agent at MasterCard:
|
||||
|
||||
[bsky-visa-automated-trick]: https://bsky.app/profile/tendermiasma.com/post/3lusrnv5b3c2r
|
||||
|
||||
> Visa is easy but you have to trick the phone tree with MasterCard:
|
||||
>
|
||||
> 1. select option that you have a card
|
||||
> 2. select option for account info
|
||||
> 3. press a few random buttons when asked to give your card, wait until it transfers you to an agent
|
||||
> 4. Request to file a complaint about discriminatory practices
|
||||
|
||||
[Yet another approach][bsky-insufferable-oblivious] you can take to gum up the works is being the insufferable oblivious customer that doesn't want to file a complaint but just ask why you can't pay for stuff online and to be put on hold instead of being called back (slightly edited for form):
|
||||
|
||||
[bsky-insufferable-oblivious]: https://bsky.app/profile/ghoulpus.bsky.social/post/3luvh25cytk2y
|
||||
|
||||
> Currently on the phone with Visa.
|
||||
>
|
||||
> Instead of starting with a complaint, I approached this as a Confused American Consumer who doesn't understand why they can't buy things off a website they use.
|
||||
>
|
||||
> The call center rep immediately asked if it was about Steam/itch. I said yes and asked for information, saying I don't understand what's happening, just trying to understand. Something about Australia? Why is Australia impacting me in the US?
|
||||
>
|
||||
> They said to email complaints to an email address.
|
||||
>
|
||||
> I don't have a complaint though! I don't even know what I'm complaining out.
|
||||
>
|
||||
> Email offered again.
|
||||
>
|
||||
> I repeated myself. Slower.
|
||||
>
|
||||
> Email again.
|
||||
>
|
||||
> After a few rounds of this I asked for a manager. I'm now on hold. My demeanor has been polite but confused. Talking slower than usual, to eat up time.
|
||||
>
|
||||
> I've worked call centers before, and I'm not here to lash out at the rep. I'm playing the part of Polite But Annoying Customer. I'm causing friction but not giving them any reason to dismiss me.
|
||||
>
|
||||
> They just offered me a callback from the supervisor but I said I'd stay on hold.
|
||||
>
|
||||
> Got a supervisor.
|
||||
>
|
||||
> Wasted time with greetings / asking how their day is, etc. Repeated everything I had told the rep. Said I don't want to waste anyone's time emailing a complaint if I don't know what the complaint is about.
|
||||
>
|
||||
> Supervisor finally said they've been instructed to only hand out the email and give no further information. They can't tell me anything else on the phone. I say hey, no problem, I know this isn't your fault. I ask to file an internal ticket. Supervisor said all calls are logged with case numbers.
|
||||
>
|
||||
> I ask for the case number.
|
||||
>
|
||||
> I now have something I can call back about and waste more time (since I don't know how email works and I need people to explain things to me slowly on the phone).
|
||||
>
|
||||
> Total call length: 25 minutes.
|
||||
>
|
||||
> I think anyone who has done customer service before wants to be as efficient as possible when calling. But here, you need to play the part of someone who is incapable of doing any self-service while also embodying the kind neighbor who eats up 15 minutes of your life whenever you pass their porch.
|
||||
|
||||
## A slippery slope into authoritarian censorship
|
||||
|
||||
Steam and itch.io are only a stepping stone. If we let these puritanical zealots have their way, we'll have much bigger problems on our hands than missing out on a bit of wank material.
|
||||
|
||||
Restricting access to or outright banning porn is just to get people on board with something allegedly laudable sounding and get laws enacted. Once it's law, however, the stage is set for progressively broadening the scope of what falls under its definitions.
|
||||
|
||||
Today, it's "porn." Tomorrow, it's banning anything that allows people to explore their sexuality on their own terms. Next week, it's censoring and banning anything they deem "harmful" in their very loose definitions or stuff they simply don't like. It's always a slippery slope, especially in the political climate we're currently living in. Make no mistake: the end goal is always to force their world views onto everyone else and demand conformity. Ask LGBTQ+ people how it feels to have your existence constantly politicized, when you're just trying to live your life. Puritans will not stop and target ***anyone*** that does not conform to their perfect image of how people are supposed to behave and maintain relationships.
|
||||
|
||||
If you need an example of what happens when puritanical fervor is passed as law, look no further than the UK's deceitfully named *Online Safety Act.*
|
||||
|
||||
Originally intended to prevent minors from accessing porn online, it is now used as a cudgel to classify Wikipedia as a "Category 1" platform, which would require by law that Wikipedia verify visitors' age, verify the identity of contributors and censor "harmful" topics (i.e. information about sexuality and LGBTQ+, but also important mental and physical health topics). The Wikimedia Foundation is [currently challenging][wikimedia-osa-court] its classification in court, arguing that it "would undermine the privacy and safety of Wikipedia's volunteer contributors, expose the encyclopedia to manipulation and vandalism, and divert essential resources from protecting people and improving Wikipedia."
|
||||
|
||||
[wikimedia-osa-court]: https://wikimediafoundation.org/news/2025/07/17/wikimedia-foundation-challenges-uk-online-safety-act-regulations/
|
||||
|
||||
If you live in the UK and want to help kill this [dysfunctional mistake][online-safety-act-vs-death-stranding] of a law, [sign the petition][kill-uk-online-safety-act] on the UK government's website!
|
||||
|
||||
[online-safety-act-vs-death-stranding]: https://www.pcgamer.com/hardware/brits-can-get-around-discords-age-verification-thanks-to-death-strandings-photo-mode-bypassing-the-measure-introduced-with-the-uks-online-safety-act-we-tried-it-and-it-works-thanks-kojima/
|
||||
[kill-uk-online-safety-act]: https://petition.parliament.uk/petitions/722903
|
||||
|
||||
> [!important] Circumvention is no solution
|
||||
> As noted by Swiss privacy-focused service provider [Proton][proton-vpn-spike], they saw a 1,400% increase in sign-ups to their VPN service. While it's one way to circumvent the restrictions imposed by the UK's Online Safety Act, you must not think this to be a solution to the issue at hand! The Online Safety Act is still a massive breach of privacy for everyone in the UK and it's only a matter of time before VPNs are outlawed or subject to steep fines for circumventing the restrictions.
|
||||
|
||||
[proton-vpn-spike]: https://x.com/ProtonVPN/status/1948773319148245334
|
||||
|
||||
Or look at [FOSTA-SESTA], from April 2018 during Trump's first term, which got heavy pushes from bigoted hypocrite coalitions like *Exodus Cry* to "make it illegal to knowingly assist, facilitate, or support sex trafficking," and amend [Section 230] to exclude providers from its protections if they're found to host such content. Nobody in their right mind is against preventing sex trafficking, but what this bill and its proponents call "sex trafficking" is a disingenuous twisting of words that anti-porn groups like *Collective Shout*, *Exodus Cry* and *NCOSE* use to describe **all** of the legal sex industry. They believe everyone who is on there is so against their will and needs saving, even when it's professional adult performers making a living of it and do so on their own volition. After they got their law passed, they immediately started a campaign against PornHub to be shut down entirely on the grounds it's a sex trafficking website and garnered over 2.5 million signatures on a petition for that purpose.
|
||||
|
||||
[FOSTA-SESTA]: https://archive.ph/OJGp9
|
||||
[Section 230]: https://en.wikipedia.org/wiki/Section_230
|
||||
[exodus-cry-abortion]: https://www.vanityfair.com/hollywood/2020/11/melissa-mccarthy-exodus-cry-apology
|
||||
|
||||
FOSTA-SESTA is the nexus point to which you can almost always trace back all of this puritanical scrubbing anything sex-related off the internet. It's why payment processors immediately cut ties with anyone and everyone when these bigots come knocking. There's [legal precedent][visa-pornhub], that when Visa tried to have their inclusion in a case against PornHub's parent company dismissed, a judge denied that motion, on the grounds that cutting ties and later reinstating them, Visa must've known about the contents, thus they're on the hook, regardless. However, in [April 2024](https://www.courthousenews.com/judge-tentatively-dismisses-visa-from-pornhub-sex-trafficking-lawsuits/) another judge tentatively dismissed Visa's involvement in the case, arguing they merely acted as a utility service in this case. That didn't stop *Collective Shout* from claiming they're on the hook for what happens on the platforms they do business with in their [open letter][collective-shout-open-letter] to pressure these companies, threatening the *stability and predictability of their business* instead. The ***only*** reason FOSTA-SESTA was enacted was to weaken Section 230 in order to make it easier for these groups to legally prosecute sites on the internet they simply don't like. In all of its existence, it was only used **ONCE** for its stated purpose, according to a [GOA report][fosta-sesta-goa-report] from June 2021:
|
||||
|
||||
[visa-pornhub]: https://www.bbc.com/news/technology-62372964
|
||||
[fosta-sesta-goa-report]: https://www.gao.gov/assets/gao-21-385-highlights.pdf
|
||||
|
||||
> In June 2020, DOJ brought one case under the criminal provision established by section 3 of FOSTA for aggravated violations involving the promotion of prostitution of five or more people or acting in reckless disregard of sex trafficking. As of March 2021, restitution had not been sought or awarded. According to DOJ officials, prosecutors have not brought more cases with charges under section 3 of FOSTA because the law is relatively new and prosecutors have had success using other criminal statutes.
|
||||
|
||||
The implication here very much is that these puritanical advocacy groups draft their own laws, get them passed when the opportunity presents itself (i.e. when a very litigious executive that's very agreeable to their views is in office) and then go to town with that state sanctioned battering ram.
|
||||
|
||||
As for the payment processors, they will say this is merely an issue of "risk management." The risk being: getting dragged into court room bouts over fabricated facilitation of sex-trafficing and overblown claims of CSAM material being present on a platform they provide their services to. The risk in this case is very real as the case with Visa shows, but it also implies a very real and powerful lever payment processors can pull. No prior involvements of the courts, no evidence needed, just a bunch of religious nut jobs and they pull it in the name of "brand safety." This is the equivalent of micro-transactions using in-game currency to obfuscate how much you're really spending. Groups like *Collective Shout* can make payment processors say it's about their "brand safety" and "risk compliance"—but the reality is they let themselves get weaponized into making other people's judgement calls over content that doesn't comply with their subjective values and not even a surface-level understanding of the media they seek to have censored or banned. Not courts or elected officials. A private company without any transparency just decided, no, this thing doesn't get to exist because someone ***might*** feel icky about it, hurt their bottom line or make shareholders nervous. Suddenly, the question about morality becomes a matter of *brand management.*
|
||||
|
||||
The other uncomfortable truth of this is that one country gets to push their narrative of acceptable forms of expression onto the rest of the world. The fact that Japanese manga sites and indie developers on Steam had their [revenues frozen][japan-investigate-payment-processors] over content that is perfectly legal in their homeland and has been approved of by domestic regulative bodies is proof of that. Once they've scrubbed the media landscape clean of "porn," they'll turn their attention to something else.
|
||||
|
||||
[japan-investigate-payment-processors]: https://nichegamer.com/japanese-government-to-investigate-payment-processors-after-withholding-revenue-from-adult-games-on-steam/
|
||||
|
||||
Optional story paths in your favorite RPG series with a gay romance option? Banned for pushing "the gay agenda."
|
||||
|
||||
Black super hero as the main protagonist? Banned for pushing "the woke mind virus."
|
||||
|
||||
Tactical espionage action game that challenges a country's narrative and ethos? Banned for "propaganda."
|
||||
|
||||
Hack-and-slash game that is themed around the occult? Banned for "promoting satanism."
|
||||
|
||||
Game that makes you come to terms with your own mortality? Banned for "stop making people sad."
|
||||
|
||||
The looming threat of losing business by being financially suffocated will inevitably have a chilling effect on game developers going forward if this is allowed to continue. Stories that never get told not because players or middle and upper management rejected them, but because nobody dared to even pitch them. Do you ever wonder why games have felt so lifeless and without teeth in recent years? It's because of this focus on maximum profitability and guaranteed profits, and it's going to get a lot worse if groups like *Collective Shout* get to have a say about what art and fiction are allowed to do.
|
||||
|
||||
Censorship will not come with sirens and handcuffs. It will come with a deafening silence of mediocrity that never dares to question, never dares to challenge, never dares to provoke, simply out of fear of being demonetized. If a few financial gatekeepers and religious lobby groups get to decide what's too controversial, across borders, without your input, without transparency and without any sort of accountability, it's no longer in the hands of developers anymore to decide what games get made and we are all going to be poorer for it.
|
||||
|
||||
The puritans are well-connected and use pretty euphemisms to mask as charities and organizations advocating for "safety" or "for the children" or "against sexual exploitation" as a thinly veiled cover-up to fool the larger population they're well-intentioned (and shut down anyone challenging them, because "oh, so you're actually *for* sexual exploitation of children?!") but push their puritanical agenda in the shadows until their world views dominate and are enshrined by law. They will not stop at porn, they will not stop at LGBTQ+ people and they will most certainly try to dictate how heterosexual relationships are allowed to exist!
|
||||
|
||||
Be ***very*** critical about these kinds of deceitful plays on words and scrutinize their talking points at every step of the way. Don't give them even the slightest benefit of a doubt! Your freedom of expression and everyone's privacy depends on it! If it's not *Collective Shout* celebrating victories over this, then it's going to be another group claiming to "protect children."
|
||||
|
||||
**This is first and foremost a cultural and political problem,** not a technical one. If you're pissed off that Australian puritans feel emboldened to dictate what you can do with your own money, I need the technologists, gamers and queer activists in my audience to rise up! An attack on one of us is an attack on all of us!
|
||||
|
||||
Finally, I want to leave you with this evergreen quote by Tumblr user [genderkoolaid]:
|
||||
|
||||
> In order to not succumb to sex negative conservatism you have to accept that people will get off to things that are upsetting to you. And you cannot assume anything about what they have or have not experienced, what they do or do not believe, and how they act based solely on what gets them off. Even if it's extremely confusing and disturbing to you. There are people who have only ever had heterosexual vanilla sex in missionary with the lights off, who actively contribute to more real world harm than your average fetish artist. Kink is not a reliable source of information on someone's moral standing. It just feels good to think that way.
|
||||
|
||||
[genderkoolaid]: https://genderkoolaid.tumblr.com/post/759167209816899584/in-order-to-not-succumb-to-sex-negative
|
|
@ -1,16 +1,5 @@
|
|||
{
|
||||
"layout": "blogpost.njk",
|
||||
"permalink": "/posts/{{ title | slugify }}/",
|
||||
"url": "https://blog.sebin-nyshkim.net",
|
||||
"author": {
|
||||
"name": "Sebin Nyshkim",
|
||||
"href": "https://blog.sebin-nyshkim.net"
|
||||
},
|
||||
"twitter": {
|
||||
"cardType": "summary_large_image",
|
||||
"account": "SebinNyshkim"
|
||||
},
|
||||
"mastodon": {
|
||||
"fediverseCreator": "@SebinNyshkim@meow.social"
|
||||
}
|
||||
"date": "git Created"
|
||||
}
|
||||
|
|
46
src/privacy.md
Normal file
46
src/privacy.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
layout: page.njk
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
I use a self-hosted [Ackee](https://ackee.electerious.com) instance to gain insights about the types of devices that visit this site.
|
||||
|
||||
Ackee is open-source analytics software. The data it collects is anonymized in a way that does not allow me to identify individual visitors. It also does not save any cookies and does not follow you around the web. I merely store whether you agree to analytics collection or not in your browser's local storage.
|
||||
|
||||
## Data that Ackee collects
|
||||
|
||||
Ackee gives me insight into the following data points:
|
||||
|
||||
* Number of visits per day
|
||||
* Number of visitors that currently view the site
|
||||
* Approximate duration of stay
|
||||
* Number of times a given page was visited
|
||||
* Sites a visit originated from (referrer)
|
||||
* Name and manufacturer of the device
|
||||
* Name and version of the operating system
|
||||
* Name and version of the browser
|
||||
* Screen size of the device
|
||||
* Primary language of the browser or operating system
|
||||
|
||||
Ackee uses the IP, user-agent and domainId to identify a user. All information will be hashed together with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) that changes daily. The final hash is called `clientId`.
|
||||
|
||||
The daily salt is never stored anywhere. It avoids that database backups can be used to stick data together to reconstruct the browsing history of a user.
|
||||
|
||||
## Purpose of collecting data
|
||||
|
||||
I collect this data to better understand my audience, improve the site's user experience, and to inform future development and editorial decisions. The data will not be shared with anyone, ever.
|
||||
|
||||
## Data retention
|
||||
|
||||
Ackee removes data from previous records when a new record with an existing identification gets added. This way the user identifier and other identifiable data is only stored once in the database.
|
||||
|
||||
In other words: Ackee forgets who you are as soon as it sees you again. It's not possible to reconstruct a browsing history, even on a daily basis.
|
||||
|
||||
## User consent
|
||||
|
||||
Data collection is opt-in. I will not collect any analytics data until you explicitly allow me to do so. If you previously opted-in but changed your mind, click the button below:
|
||||
|
||||
<button onclick="localStorage.setItem('ackeeDetailed', false)" class="items-center bg-sky-600 *:stroke-[2.5] 2xl:px-6 2xl:py-3 2xl:rounded-2xl button duration-300 font-bold gap-2 hover:bg-sky-700 inline-flex no-underline px-5 py-2 rounded-xl text-white transition-colors">Opt-out</button>
|
||||
|
||||
By using this site, you acknowledge that you have read and understood this privacy policy. If you have any questions or concerns about how your data is collected or used, please feel free to [contact me](/contact).
|
|
@ -1,12 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,md,njk,ejs,pug}'],
|
||||
theme: {
|
||||
extend: {
|
||||
gridTemplateColumns: {
|
||||
content: '20rem minmax(0, 1fr)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('tailwindcss-safe-area')]
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue