Skip to content

Template structure

This section is about the file structure. You will learn how the different files are organized and the role each folder plays in the project.

Global structure

bash
my-project/
├── public/                 # static files (images, robots.txt, favicon.ico, etc.)
├── src/
   ├── layouts/            # layout HTML chunks
   ├── partials/           # HTML partial chunks
   ├── root/               # Project root
|   ├── postcss/        # Postcss files
|   ├── scripts/        # Alpine JS components and main entry
|   ├── *.html          # HTML pages
├── package.json            # project dependencies
├── postcss.config.js       # PostCSS configuration
├── tailwind.config.js      # tailwind css configuration
└── vite.config.js          # vite plugins and configuration

The directory structure can seem unusual for someone who is not used to build tools. We will break each part of it, so you understand how everything works.

Public folder

bash
my-project/
├── public/                 # static files (images, robots.txt, favicon.ico, etc.)

The public folder holds all the static assets that a website or an app needs to serve. It can be robot files, favicons, and any images used in the project. When working inside the template, the images folder is accessible from the root: src="/img/path-to-your-image.jpg".

Layouts

bash
my-project/
├── src/
   ├── layouts/            # layout HTML chunks
|   ├── default.html

All the HTML layouts resides in this folder. For a more concise code, this project uses vite-plugin-handlebars. The implementation we made of it inside this project makes it easy to break your layouts into several reusable files, preventing you to go through every page to make your changes. You can find more information about vite-plugin-handlebars by looking at the plugin documentation.

The layouts folder holds all the template layouts. It is mandatory to have at least one layout, and it should always be named default.html. Additional layouts can have the names you want, but don't forget the .html extension. A layout acts as container providing the same base for a set of similar pages. Finally notice the {{> @partial-block }} call that is present in each layout file. This is a reference to append the rest of the page content, that comes from the HTML files located in src/root/*.html.

Layout files

The layouts/ folder generally holds one layout file, but there are cases, like in Mistral, where there can be more than one:

  • default.html : The default layout generally used by all the project pages.
  • minimal.html : A minimal layout to do different things.

Pages

bash
my-project/
├── src/
   ├── root/               # Project root
|   ├── *.html          # HTML pages

The src/root/ folder holds all the template pages. Pages are focused on content. They are related to a layout and automatically injected to it by vite when the page is served. Inside a page, a layout is always represented by opening and closing handlebars tags :

html
<!--Start Layout-->
{{#> default }}

  <!--Page content goes here-->

<!--End Layout-->
{{/default}}

Those special tags tell vite-plugin-handlebars which layout to use to serve for the page. Now that we have a basic understanding of what vite-plugin-handlebars does, let's look closer at the HTML files.

Page files

All HTML pages live in the src/root/ folder. Each one of this pages is assigned to one of the existing layouts and makes use of partials living in the src/partials/ folder.

Partials

bash
my-project/
├── src/
   └── partials/
|   ├── *.html

The partials folder holds all your HTML partials. Partials are chunks of HTML code that you want to reuse and that are handled by the vite-plugin-handlebars plugin. A partial can be a button, a navbar, a content section or whatever you want. Note that you can create as many sub-folders as you want to organize your partials. Partials are named like HTML files : navbar.html. When you want to call a partial in one of your layouts or pages use the following expression : {{> partial-name}}. You also have to provide the path to the partial, starting from the partials folder, and omitting it. for example, a partial that would reside in src/partials/navbar/widgets/dropdown.html should be called this way: {{> navbar/widgets/dropdown}}. Partials can also be called inside other partials, there is no limit to that. Please also note that you don't have to add the .html extension in your partial call.

Scripts

├── src/
│   ├── root/               # Project root
|   │   ├── scripts/        # Alpine JS components and main JS entry
|   |   │   ├── main.js

All javascript files live in the src/root/scripts folder. We dropped jQuery support quite some time ago and are now using the ES6 syntax to handle javascript functions. You should be familiar with the bare bones of ES6 when working with this template. Dropping jQuery doesn't mean we didn't replace it with something else. In fact we did, we are now using Alpine JS, a powerful dependency free javascript declarative framework, similar to Vue, but with less complexity. You can learn the basics of Alpine JS by reading the documentation. Alpine works very well in a simple setup where you declare your scripts in the same HTML page. However, since we are not importing the library from a CDN, but rather from node_modules, we use a little more complex but significantly more solid setup for alpine inside an ES 6 compatible environment. In the following example, we instantiate Alpine and load some additional plugins.

javascript
import '@purge-icons/generated'
import '@vidstack/player/define/vds-media.js'
import '@vidstack/player/define/vds-video.js'
import '@vidstack/player/define/vds-aspect-ratio.js'
import '@vidstack/player/define/vds-poster.js'
import 'swiper/css/bundle'

//Alpine and plugins import
import Alpine from 'alpinejs'
import intersect from '@alpinejs/intersect'
import collapse from '@alpinejs/collapse'
import persist from '@alpinejs/persist'

import './demo'
import './components'

window.Alpine = Alpine
//Init intersect plugin
Alpine.plugin(intersect)
//Init collapse plugin
Alpine.plugin(collapse)
//Init persist plugin
Alpine.plugin(persist)
//Init Alpine store
Alpine.store('app', {
  init() {
    this.on = window.matchMedia('(prefers-color-scheme: dark)').matches
  },
  isDark: Alpine.$persist(false),
})
//Start Alpine
Alpine.start()

document.onreadystatechange = function () {
  if (document.readyState == 'complete') {
    // Do something here
  }
}

Like we said earlier, It's a little bit more complex in this project since we are importing functions from separate JS files. The only thing you need to now when importing an Alpine JS function in your main.js file is that you need to add it to the window object for it to work properly. Otherwise, you'll get undefined. Here is a practical example:

javascript
//Import the function you need from the target file
import { navbar } from './navbar'

//Bind it to the window object to access it from anywhere
window.navbar = navbar

This can quickly become very verbose in your main file. Therefore, we decided to make those imports in nested index.js barrel files. An example is src/root/scripts/components/index.js. In this file, every component JS file is imported and bound to the global window object. Here is the sample code from that file:

javascript
// Imports
import { layout } from './layout'
import { navbar } from './navbar'
import { search } from './search'
import { backtotop } from './backtotop'
import { boxCarousel, cardCarousel } from './carousel'
import { video } from './video'
import { collapse } from './collapse'
import { gallery } from './gallery'
import { pricing } from './pricing'
import { comparison } from './comparison'
import { blog } from './blog'

// Binding to window object
window.layout = layout
window.navbar = navbar
window.search = search
window.backtotop = backtotop
window.boxCarousel = boxCarousel
window.cardCarousel = cardCarousel
window.video = video
window.collapse = collapse
window.gallery = gallery
window.pricing = pricing
window.comparison = comparison
window.blog = blog

Then, inside the main.js you can easily re-import barrel files to have all your functions loaded in:

javascript
import './components'

You now know everything about how to structure your Javascript files in this project.

PostCSS

my-project/
├── src/
│   ├── root/               # Project root
|   │   ├── postcss/        # Postcss files
├── postcss.config.js       # PostCSS configuration

This template is built with Tailwind CSS v3.x, and therefore uses PostCSS for compilation, instead of Sass. The postcss.config.js file provides minimal configuration to compile with Tailwind and uses cssnano to minify the bundle in production. It should be enough but you can extend it to your needs if you want. Here is the sample configuration:

javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
  },
}

There are also other PostCSS files related to Tailwind and template styles living in src/root/postcss:

my-project/
├── src/
│   ├── root/                  # Project root
|   │   ├── postcss/           # Postcss files
|   |   │   ├── modules/       # Postcss modules
|   |   │   ├── main.postcss   # Main file

The main file contains some basic Tailwind CSS setup as well as some additional styles and imports:

css
@import './modules/swiper.postcss';
@import './modules/vds.postcss';

/* Tailwind */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  [x-cloak] {
    @apply !hidden;
  }

  .tw-accessibility {
    @apply outline-none focus:outline-none focus:outline-dashed focus:outline-muted-300 dark:focus:outline-muted-600 focus:outline-offset-2;
  }

  .tw-accessibility-static {
    @apply outline-dashed outline-muted-300 dark:outline-muted-600 outline-offset-2;
  }
}

Let's break up the example above. The first 2 lines work exactly like Sass imports, meaning you can as well import other PostCSS files inside your main file. In this specific example, we are importing additional styles for some external plugins we have in our template, swiperjs and vidstack player.

The second chunk imports Tailwind base layers, so it can properly work. We're then using one of those provided layers (@layer utilities) to add some additional CSS utilities in our project. Note how the @apply directive is used to output the styles instead of using traditional CSS syntax.

Root files

There also a few files sitting at the root of the project that we need to discuss before getting into serious business:

  • vite.config.js : Vite uses this configuration file to set the dev and build behaviors, as well as initializing Vite / Rollup plugins.
  • package.json : Lists all your project's dependencies and gives useful metadata.
  • postcss.config.js : We already talked about this file and what it does above.
  • tailwind.config.js : Tailwind uses this file to initialize and load any additional configuration elements you've set in there. This is the next file we're diving in.

All Rights Reserved