Appearance
Front-end Asset Management
Front-end Asset Organization
By default, assets sources are stored under PROJECT_ROOT/resources and then "compiled" into src/public/ directory. These assets will be served by CDN to ensure optimal performance. The following is a summary of assets locations in our system:
CSS: resources/pcss/ => public/css/
JavaScript: resources/js/ => public/js/
Images: resources/img/ => public/img/
Fonts: resources/fonts/ => public/build/assets/fonts/ (self-hosted, processed by Vite)
Videos: CDN (Amazon S3 + CloudFront)
There are some npm tasks/commands to transform our asset sources into production-ready assets:
.js files should be transpiled, versioned and minified for production.
PostCSS files should be compiled into CSS, optimized/minified and versioned to avoid caching problems.
Cacheable Assets
Assets managing using PHP
We use Laravel mix-compatible API and versioning system.
We also combine this with the asset() helper to generate CDN URLs.
This line will automatically add the CDN URL (based on the .env variable ASSET_URL) and a cache-busting query string to prevent cache problems on client side:
html
<!--src-->
@vite('resource/js/app.js')
<!--output (example)-->
<script type="module" src="https://assets.interaction-design.org/js/app-rAndOm.js" defer></script>Note that random part of the filename depends on file content (checksum) and thus changed only when then content is changed.
Fonts
The project self-hosts Arimo font files instead of loading them from Google's CDN. This eliminates the external domain dependency, avoids extra DNS lookups, and removes the privacy concern of Google tracking font requests.
Font files are stored in resources/fonts/arimo/ and processed by Vite like any other asset; they get content-hashed, copied to public/build/assets/fonts/, and served via CloudFront in production.
How it works
- Source files:
resources/fonts/arimo/*.woff2— the raw font files - CSS declarations:
resources/css/web-fonts.css—@font-facerules withsrc: url("../fonts/arimo/...")relative paths - Vite processing: During build, Vite resolves the CSS
url()references, copies the.woff2files topublic/build/assets/fonts/with content hashes, and rewrites the URLs in the compiled CSS - CDN delivery: In production,
ASSET_URLpoints to CloudFront, so fonts are served globally with no extra setup
Updating fonts
To download the latest font files from Google Fonts:
bash
npm run fonts:updateThis runs the fontsUpdate gulp task (scripts/fonts/update-google-fonts.mjs), which iterates the FONTS array, fetches each font's CSS from the Google Fonts API, and downloads only the whitelisted subsets.
Adding a new font
- Add an entry to the
FONTSarray inscripts/fonts/update-google-fonts.mjs:
js
{
name: 'open-sans',
url: 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap',
subsets: ['latin', 'latin-ext'],
}- Run
npm run fonts:update: the script downloads the.woff2files toresources/fonts/{name}/ - Add
@font-facedeclarations toresources/css/web-fonts.css.
Font preloading
The normal-weight latin subset (arimo-normal-latin.woff2) is preloaded via <link rel="preload"> in the document <head>. This eliminates the CSS -> font waterfall: without preloading, the browser discovers the font only after downloading and parsing the CSS that contains the @font-face rule (HTML → CSS → font). With the preload hint the browser starts fetching the font in parallel with CSS, reducing FOUT and improving LCP when text is the largest contentful element.
Only this single variant is preloaded because:
- Normal weight renders all body text (the most common variant by far)
- The latin subset covers U+0000–00FF, handling virtually all English content
- It is the smallest file (~20 KB), so the bandwidth cost is minimal
- Preloading multiple fonts would compete for bandwidth and negate the benefit
The crossorigin attribute is required even for same-origin fonts — @font-face fetches always use anonymous CORS mode, and without crossorigin the preloaded response won't match, causing a double download.