Assets and Static Files

Templates render the HTML. Assets — CSS, JavaScript, fonts, favicons, downloads — provide the styling, interactivity, and supporting files that templates pull in. SleekCMS organizes assets into a few well-known folders and gives templates two helpers, link() and script(), for including them on a page.

This page covers where assets live, how to include them, the special handling for Tailwind CSS, and how the build pipeline handles static files at deploy time.


Folder Layout

Assets are organized by type:

css/                Stylesheets — included via link()
  tailwind.css      Special: auto-compiled and auto-injected (see below)
  styles.css        Your own CSS file(s)

js/                 Client-side scripts — included via script()
  app.js
  carousel.js

public/             Other static files served verbatim at the site root
  favicon.ico
  apple-touch-icon.png
  fonts/inter-var.woff2
  downloads/brochure.pdf
  robots.txt

The css/ and js/ folders are conventional locations for stylesheets and scripts that templates include. The public/ folder is for any other static file that needs to ship with the site — favicons, manifest files, fonts, downloadables, robots.txt, sitemap.xml, anything that's served as-is at a known URL.

At deploy time, every file under these folders ends up in the deployed output. The directory structure is preserved.


Including CSS

Use the link() helper to add a stylesheet. Pass the path; the helper figures out the right <link> tag attributes:

<% link('/css/styles.css') %>
<!-- Output: <link rel="stylesheet" type="text/css" href="/css/styles.css"> -->

For non-CSS link types (canonical, preconnect, RSS feed autodiscovery), pass an object with explicit attributes:

<% link({ rel: 'canonical', href: url(item) }) %>
<% link({ rel: 'preconnect', href: 'https://fonts.googleapis.com' }) %>
<% link({ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: '/rss.xml' }) %>

Don't write raw <link> tags. The injection helpers deduplicate (so calling the same stylesheet from many block templates is safe) and place tags in the right <head> order.


Including JavaScript

Use the script() helper. A path ending in .js becomes an external script; anything else is treated as inline code:

<!-- External -->
<% script('/js/app.js') %>
<!-- Output: <script src="/js/app.js"></script> -->

<!-- Inline -->
<% script('console.log("loaded")') %>
<!-- Output: <script>console.log("loaded")</script> -->

<!-- With explicit attributes -->
<% script({ src: '/js/app.js', defer: true }) %>
<% script({ src: '/js/module.js', type: 'module' }) %>

Scripts are appended at the end of <body> rather than in <head>, which is the right place for most page scripts (no render-blocking).


Tailwind Is Special

/css/tailwind.css is the one stylesheet that does not need to be included via link(). If that file exists, the build pipeline compiles Tailwind utility classes used in your templates and auto-injects the resulting stylesheet on every page. You don't call link('/css/tailwind.css') — doing so would add the file twice.

For everything you'd customize in a Tailwind project — theme tokens, custom utilities, plugin imports — edit /css/tailwind.css directly. The compile pipeline picks up the changes on save.

Tailwind and Styling


Files in public/

Anything you drop under public/ is served at the corresponding URL. A file at public/favicon.ico is available at /favicon.ico. A file at public/fonts/inter.woff2 is at /fonts/inter.woff2. A file at public/downloads/menu.pdf is at /downloads/menu.pdf.

You don't need a helper to reference these — write the URL directly in your templates:

<% link({ rel: 'icon', href: '/favicon.ico' }) %>

<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var.woff2') format('woff2-variations');
  }
</style>

<a href="/downloads/menu.pdf">Download menu (PDF)</a>

Use public/ for:

  • Favicons (favicon.ico, apple-touch-icon.png)
  • PWA manifests (manifest.json)
  • Self-hosted fonts (.woff2)
  • Static downloadables (PDFs, sample files)
  • Hand-written robots.txt or sitemap.xml (though the latter is usually better as a page template)
  • Anything else that ships as-is

Asset URLs and Caching

The deployed asset URLs are stable — /css/styles.css stays /css/styles.css across builds. The build pipeline does not append content hashes by default, so CDN caching is up to the deployment target.

Most deployment targets (Netlify, Vercel, surge.sh) handle cache headers automatically based on file type. For more aggressive cache control, configure the deploy target's cache settings rather than touching the file output.


Common Patterns

Layout includes site-wide assets. The layout is the right place for stylesheets and scripts that every page needs:

<% link('/css/styles.css') %>
<% script('/js/app.js') %>

Pages opt into page-specific assets. A page that needs a special library or stylesheet declares it in the page template:

<% link('/css/blog.css') %>
<% script('/js/comments.js') %>

Blocks declare their own dependencies. A block that needs a syntax highlighter or a charting library declares it where the block is defined:

<!-- blocks/code-sample.ejs -->
<% link('/css/syntax.css') %>
<% script('/js/highlight.js', 'defer') %>
<pre class="code"><code><%= item.source %></code></pre>

Even if the page contains five code-sample blocks, the link and script are emitted once per page — the injection helpers deduplicate automatically.


What's Next