feat: update entire css pipeline

The goal of this change was to speed up my CSS pipeline. Previously
builds would take up to 30s because it had to rebuild the CSS for every
page (I inlined the CSS).

This updates the pipeline to remain somewhat the same, but offloads the
busywork of computing my theme CSS (my design system in other words),
and is replaced by a mostly static file (`css/global/lilypad.css`),
which I can update as my design system changes.

I also offloaded several of my utility classes as I know I will want to
use them in other sites I design, so it made sense to move them as well.

The new pipeline makes use of 11ty's bundle feature (newly included in
11ty v3), which allows me to bundle all my CSS together into one file.
Since I know my core styles like `lilypad.css` will always remain the
same, I decided to keep that inline. I use the `transforms` feature for
bundling to run a PostCSS transform on the bundled output, which
resolves any imports and minifies the final output.

In order to cut down my build times even further, I removed my XML
minifier - it was taking upwards of 3 seconds to minify XML to probably
a small benefit to end users.
This commit is contained in:
Devin Haska 2025-05-28 17:00:37 -07:00
parent 0bd05de72f
commit 788ef1dc8d
Signed by: wonderfulfrog
SSH key fingerprint: SHA256:ejOGyH9064rJiikox4ykOHLeuUg1f9l8wmJxs+MzNw0
24 changed files with 57 additions and 436 deletions

13
config/bundles/css.js Normal file
View file

@ -0,0 +1,13 @@
import postcss from "postcss";
import postcssImport from "postcss-import";
export default {
transforms: [
async function (content) {
const css = await postcss([postcssImport])
.process(content, { from: "src/includes/css/styles.css" })
.then((result) => result.css);
return css;
},
],
};

5
config/bundles/index.js Normal file
View file

@ -0,0 +1,5 @@
import css from "./css.js";
export default {
css,
};

View file

@ -1,22 +0,0 @@
export default {
light: {
primary: "oklch(0.6 0.1025 212.16)",
secondary: "oklch(0.61 0.2232 31.48)",
background: "oklch(0.98 0 0)",
surface: "oklch(0.96 0.0078 207.9)",
border: "oklch(0.87 0.045077 207.8465)",
text: "oklch(0.15 0 0)",
fadeText: "oklch(0.45 0.0214 207.84)",
shadow: "oklch(0.39 0.0688 212.35)",
},
dark: {
primary: "oklch(0.57 0.0991 213.4)",
secondary: "oklch(0.55 0.1982 31.52)",
background: "oklch(0.2 0 0)",
surface: "oklch(0.26 0.0106 233.21)",
border: "oklch(0.27 0.0238 245.26)",
text: "oklch(0.98 0 0)",
fadeText: "oklch(0.78 0.018 207.85)",
shadow: "oklch(0.39 0.0688 212.35)",
},
};

View file

@ -1,11 +0,0 @@
export default {
0: 0,
0.25: 4,
0.5: 8,
1: 16,
1.5: 24,
2: 32,
3: 48,
4: 64,
5: 80,
};

View file

@ -2,7 +2,6 @@ import collection from "./collection.js";
import date from "./date.js";
import feed from "./feed.js";
import general from "./general.js";
import postcss from "./postcss/index.js";
import tag from "./tag.js";
export default {
@ -10,6 +9,5 @@ export default {
...date,
...feed,
...general,
...postcss,
...tag,
};

View file

@ -1,5 +0,0 @@
import postcss from "./postcss.js";
export default {
...postcss,
};

View file

@ -1,32 +0,0 @@
/*
* Implementation sourced from eleventyone starter kit
* https://github.com/philhawksworth/eleventyone
* ---
* https://github.com/philhawksworth/eleventyone/blob/master/src/site/css/styles.11ty.js
*/
import { default as postcssBase } from "postcss";
import postcssImport from "postcss-import";
import postcssImportExtGlob from "postcss-import-ext-glob";
import autoprefixer from "autoprefixer";
import cssnano from "cssnano";
import colors from "./utils/colors.js";
import fontFamily from "./utils/font-family.js";
import fontVariables from "./utils/font-variables.js";
import spacing from "./utils/spacing.js";
const postcss = async (rawCss) => {
const css = `${rawCss}${fontFamily}${fontVariables}${colors}${spacing}`;
return await postcssBase([
postcssImportExtGlob,
postcssImport,
autoprefixer,
cssnano,
])
.process(css, { from: "src/includes/css/styles.css" })
.then((result) => result.css);
};
export default {
postcss,
};

View file

@ -1,34 +0,0 @@
import colorSchemes from "../../../design-tokens/colors.js";
import { helperClassesToCss } from "./helper-classes.js";
const lightScheme = colorSchemes.light;
const darkScheme = colorSchemes.dark;
const colorToCss = (key, value) => `--color-${key}: ${value};`;
const colorSchemeToCss = (scheme) =>
Object.entries(scheme).reduce(
(css, [key, value]) => css + colorToCss(key, value),
``,
);
const lightCss = colorSchemeToCss(lightScheme);
const darkCss = colorSchemeToCss(darkScheme);
const colorSchemeToHelperClassesCss = (scheme, helperClasses) => {
return Object.entries(scheme).reduce((css, [key]) => {
return css + helperClassesToCss(helperClasses, key, `var(--color-${key})`);
}, ``);
};
const helperClasses = [
["text", ["color"]],
["bg", ["background-color"]],
];
const helperClassesCss = colorSchemeToHelperClassesCss(
lightScheme,
helperClasses,
);
export default `:root{${lightCss}}${helperClassesCss}@media (prefers-color-scheme: dark) {:root{${darkCss}}}`;

View file

@ -1,58 +0,0 @@
import path from "path";
import fonts from "../../../design-tokens/fonts.js";
const getFontUrl = (src) => path.join("/assets/fonts", src);
const fontsToCss = (fonts) => {
return Object.entries(fonts).reduce((css, [, fontProperties]) => {
const family = fontProperties.family;
const format = fontProperties.format;
const styles = fontProperties.styles;
return (
css +
Object.entries(styles).reduce((css, [variant, fontFamily]) => {
const url = getFontUrl(fontFamily.path);
const style = fontFamily.fontStyle;
const weight = fontFamily.fontWeight;
const stretch = fontFamily.fontStretch;
const postScriptName = [family, variant].join("-").replaceAll(" ", "");
return (
css +
fontFamilyToCss(
family,
style,
weight,
stretch,
url,
family,
postScriptName,
format,
)
);
}, ``)
);
}, ``);
};
const fontFamilyToCss = (
family,
style,
weight,
stretch,
url,
localName,
postScriptName,
format,
) => `@font-face {
font-family: ${family};
font-style: ${style};
font-weight: ${weight};
font-stretch: ${stretch};
font-display: swap;
src: local("${localName}"), local("${postScriptName}"), url("${url}") format("${format}")
}\n`;
export default fontsToCss(fonts);

View file

@ -1,57 +0,0 @@
/*
* Fallbacks from Modern Font Stacks.
* https://modernfontstacks.com/
*/
import fonts from "../../../design-tokens/fonts.js";
const displayFallbacks = [
"ui-monospace",
"Cascadia Code",
"Source Code Pro",
"Menlo",
"Consolas",
"DejaVu Sans Mono",
"monospace",
];
const bodyFallbacks = [
"ui-monospace",
"Cascadia Code",
"Source Code Pro",
"Menlo",
"Consolas",
"DejaVu Sans Mono",
"monospace",
];
const monoFallbacks = [
"ui-monospace",
"Cascadia Code",
"Source Code Pro",
"Menlo",
"Consolas",
"DejaVu Sans Mono",
"monospace",
];
const fallbacks = {
display: displayFallbacks,
body: bodyFallbacks,
monospace: monoFallbacks,
};
const fontsToCss = (fonts) => {
return Object.entries(fonts).reduce((css, [fontType, fontProperties]) => {
const family = fontProperties.family;
const fontTypeCss = fontFamilyToCss(fontType, family);
return css + fontTypeCss;
}, ``);
};
const fontFamilyToCss = (type, value) =>
`--font-family-${type}: ${value},${fallbacks[type].join(",")};`;
export default `:root{${fontsToCss(fonts)}}`;

View file

@ -1,43 +0,0 @@
/**
* Given an array of CSS properties, output css properties
* with each property equal to `value`
*/
export const cssPropertiesToCss = (cssProperties, value) => {
return cssProperties.reduce((css, cssProp) => {
return css + `${cssProp}:${value};`;
}, ``);
};
/**
* Given a helperClass (string) and array of cssProperties,
* will generate a css class named helperClass that has
* all cssProperties mapped to value.
*/
export const helperClassToCss = (helperClass, cssProperties, value) => {
const cssProps = cssPropertiesToCss(cssProperties, value);
return `.${helperClass}{${cssProps}}`;
};
/**
* Given an array of helperClasses that map to cssProperties,
* output a string of CSS that maps the helperClass (with variant modifier)
* to the array of css properties with each css property equal to
* value
*
* e.g.
* helperClasses = [["text", ["color"]]],
* variant = "primary",
* value = "#000"
*
* Will output the following:
* .text-primary {
* color: #000;
* }
*/
export const helperClassesToCss = (helperClasses, variant, value) => {
return helperClasses.reduce((css, [helperClass, cssProperties]) => {
return (
css + helperClassToCss(`${helperClass}-${variant}`, cssProperties, value)
);
}, ``);
};

View file

@ -1,51 +0,0 @@
import spacing from "../../../design-tokens/spacing.js";
import { helperClassesToCss } from "./helper-classes.js";
const spacingToCss = (variant, value) =>
`--spacing-${variant.replace(".", "\\.")}: ${value}px;`;
const spacingToHelperClassesCss = (spacingValues, helperClasses) => {
return Object.entries(spacingValues).reduce((css, [spacingVariant]) => {
const variant = spacingVariant.replace(".", "\\.");
return (
css +
helperClassesToCss(helperClasses, variant, `var(--spacing-${variant})`)
);
}, ``);
};
const helperClasses = [
["m", ["margin"]],
["my", ["margin-block-start", "margin-block-end"]],
["mx", ["margin-inline-start", "margin-inline-end"]],
["ml", ["margin-inline-start"]],
["mr", ["margin-inline-start"]],
["mt", ["margin-block-start"]],
["mb", ["margin-block-end"]],
["p", ["padding"]],
["py", ["padding-block-start", "padding-block-end"]],
["px", ["padding-inline-start", "padding-inline-end"]],
["pl", ["padding-inline-start"]],
["pr", ["padding-inline-start"]],
["pt", ["padding-block-start"]],
["pb", ["padding-block-end"]],
["w", ["width"]],
["h", ["height"]],
["size", ["width", "height"]],
["radius", ["border-radius"]],
["gap", ["gap"]],
["row-gap", ["row-gap"]],
["column-gap", ["column-gap"]],
["flow-space", ["--flow-space"]],
];
const spacingVariablesCss = Object.entries(spacing).reduce(
(css, [variant, value]) => css + spacingToCss(variant, value),
``,
);
const helperCss = spacingToHelperClassesCss(spacing, helperClasses);
const css = `:root{${spacingVariablesCss}}${helperCss}`;
export default css;

View file

@ -1,4 +1,3 @@
import htmlConfigTransform from "./html-config.js";
import xmlConfigTransform from "./xml-config.js";
export default { htmlConfigTransform, xmlConfigTransform };
export default { htmlConfigTransform };

View file

@ -1,12 +0,0 @@
import minifyXML from "minify-xml";
export default function (eleventyConfig) {
eleventyConfig.addTransform("xml-minify", (content, path) => {
if (path && path.endsWith(".xml")) {
return minifyXML(content, {
shortenNamespaces: false,
});
}
return content;
});
}

View file

@ -1,15 +1,13 @@
import { eleventyImageTransformPlugin } from "@11ty/eleventy-img";
import { collectionByTag } from "./config/collections/index.js";
import bundles from "./config/bundles/index.js";
import filters from "./config/filters/index.js";
import markdown from "./config/plugins/markdown.js";
import shortcodes from "./config/shortcodes/index.js";
import transforms from "./config/transforms/index.js";
export default function (eleventyConfig) {
// --------------------- Watch Targets -----------------------
eleventyConfig.addWatchTarget("./src/includes/css");
// --------------------- Passthrough File Copy -----------------------
["src/assets/fonts/", "src/assets/images"].forEach((path) =>
eleventyConfig.addPassthroughCopy(path),
@ -18,6 +16,11 @@ export default function (eleventyConfig) {
// --------------------- Markdown -----------------------
eleventyConfig.setLibrary("md", markdown);
// --------------------- Bundles -----------------------
Object.keys(bundles).forEach((bundleName) => {
eleventyConfig.addBundle(bundleName, bundles[bundleName]);
});
// --------------------- Collections -----------------------
eleventyConfig.addCollection("postsByTag", (collection) =>
collectionByTag(collection, "post"),

View file

@ -31,11 +31,9 @@
"markdown-it-footnote": "^4.0.0",
"markdown-it-image-figures": "^2.1.1",
"markdown-it-prism": "^2.3.0",
"minify-xml": "^4.5.2",
"pluralize": "^8.0.0",
"postcss": "^8.5.3",
"postcss-import": "^16.1.0",
"postcss-import-ext-glob": "^2.1.1",
"upload-to-bunny": "^1.4.0"
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,22 @@
@import "global/reset.css";
@import "global/variables.css";
@import "global/global-styles.css";
@import-glob "blocks/*.css";
@import-glob "compositions/*.css";
@import-glob "utilities/*.css";
@import "blocks/button.css";
@import "blocks/card.css";
@import "blocks/media-display.css";
@import "blocks/media-grid.css";
@import "blocks/pill.css";
@import "blocks/posts.css";
@import "blocks/prism.css";
@import "blocks/site-header.css";
@import "blocks/site-logo.css";
@import "blocks/stars.css";
@import "compositions/cluster.css";
@import "compositions/repel.css";
@import "utilities/fonts.css";
@import "utilities/image.css";
@import "utilities/table.css";
@import "utilities/wrapper.css";

View file

@ -1,32 +0,0 @@
.flex {
display: flex;
}
.flex-col {
display: flex;
flex-direction: column;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-1 {
flex: 1;
}

View file

@ -1,8 +0,0 @@
/*
* FLOW UTILITY
* Like the Every Layout stack: https://every-layout.dev/layouts/stack/
* Info about this implementation: https://piccalil.li/quick-tip/flow-utility/
*/
.flow > * + * {
margin-block-start: var(--flow-space, 1em);
}

View file

@ -2,22 +2,6 @@
font-size: 1rem;
}
.font-size-m {
font-size: 1.125rem;
}
.font-size-l {
font-size: 1.5rem;
}
.font-size-xl {
font-size: 2rem;
}
.font-size-2xl {
font-size: 3rem;
}
.line-height-s {
line-height: 0.8rem;
}
@ -29,7 +13,3 @@
.line-height-l {
line-height: 1.5rem;
}
.line-height-xl {
line-height: 2rem;
}

View file

@ -1,11 +0,0 @@
.list-none {
list-style-type: none;
}
.list-disc {
list-style-type: disc;
}
.list-decimal {
list-style-type: decimal;
}

View file

@ -1,15 +0,0 @@
/*
* VISUALLY HIDDEN UTILITY
* Info: https://piccalil.li/quick-tip/visually-hidden/
*/
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 0;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

View file

@ -20,12 +20,13 @@
<script type="module"
src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.4.0/lite-youtube.min.js"></script>
{% endif %}
{% set css %}
<style>{% include "css/global/lilypad.css" %}</style>
{% css %}
{% include "css/styles.css" %}
{% endset %}
<style>{{ css | postcss | safe }}</style>
{% endcss %}
<link rel="stylesheet" href="{% getBundleFileUrl "css" %}">
</head>
<body class="flex-col">
<body class="flex flex-col">
{% include "partials/header.html" %}
<main id="main" class="flow flex-1 wrapper" tabindex="-1">
{{ content | safe }}