Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Assets, CSS, Images, etc

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

If you download the course code from the page where you're watching this video, after unzipping, you'll find a start/ directory that contains the same brand new Symfony 6 app that we created earlier. You don't actually need that code, but it does contain one extra directory called tutorial/, like I have here. This holds some files that we're about to use.

So let's talk about our next goal: to make this site look like a real site... instead of looking like something I designed myself. And that means we need a true HTML layout that brings in some CSS.

Adding a Layout & CSS Files

We know that our layout file is base.html.twig... and there's also a base.html.twig file in the new tutorial/ directory. Copy that... paste it into templates, and override the original.

Before we look at that, also copy the three .png files and put those into the public/ directory... so that our users can access them.

Beautiful. Open up the new base.html.twig file. There's nothing special here. We bring in some external CSS files from some CDNs for Bootstrap and FontAwesome. By the end of this tutorial, we'll refactor this into a fancier way of handling CSS... but for right now, this will work great.

But otherwise, everything is still hardcoded. We have some hardcoded navigation, the same block body... and a hardcoded footer. Let's go see what it looks like. Refresh and woo! Well, not perfect, but better!

Adding a Custom CSS File

The tutorial/ directory also holds an app.css file with custom CSS. To make this publicly available so that our user's browser can download it, it needs to live somewhere in the public/ directory. But it doesn't matter where or how you organize things inside.

Let's create a styles/ directory... and then copy app.css... and paste it there.

Back in base.html.twig, head to the top. After all the external CSS files, let's add a link tag for our app.css. So <link rel="stylesheet" and href="". Because the public/ directory is our document root, to refer to a CSS or image file there, the path should be with respect to that directory. So this will be /styles/app.css.

<!DOCTYPE html>
... lines 4 - 15
<link rel="stylesheet" href="/styles/app.css">
... lines 17 - 25
... lines 27 - 85

Let's check it. Refresh now and... even better!

The asset() Function

I want you to notice something. So far, Symfony is not involved at all in how we organize or use images or CSS files. Nope. Our setup is dead simple: we put things in the public/ directory... then refer to them with their paths.

But does Symfony have any cool features to help work with CSS and JavaScript? Absolutely. It's called Webpack Encore and Stimulus. And we'll talk about both of those towards the end of the tutorial.

But even in this simple setup - where we just put files in public/ and point to them - Symfony does have one minor feature: the asset() function.

It works like this: instead of using /styles/app.css, say {{ asset() }} and then, inside quotes, move our path there... but without the opening "/".

<!DOCTYPE html>
... lines 4 - 15
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
... lines 17 - 25
... lines 27 - 85

So the path is still relative to the public/ directory... you just don't need to include the first "/".

Before we talk about what this does... let's go see if it works. Refresh and... it doesn't! Error:

Unknown function: did you forget to run composer require symfony/asset.

I keep saying that Symfony starts small... and then you install things as you need them. Apparently, this asset() function comes from a part of Symfony that we don't have yet! But getting it is easy. Copy this composer require command, spin over to your terminal and run it:

composer require symfony/asset

This is a pretty simple install: it downloads just this one package... and there are no recipes.

But when we try the page now... it works! Check out the HTML source. Interesting: the link tag's href is still, literally, /styles/app.css. That's exactly what we had before! So what the heck is this asset() function doing?

The answer is... not much. But it's still a good idea to use. The asset() function gives you two features. First, imagine you deploy to a sub-directory of a domain. Like, the homepage lives at https://example.com/mixed-vinyl.

If that were the case, then in order for our CSS to work, the href would need to be /mixed-vinyl/styles/app.css. In this situation, the asset() function would detect the sub-directory automatically and add that prefix for you.

The second - and more important thing that the asset() function does - is allow you to easily switch to a CDN later. Because this path is now going through the asset() function, we could, via a configuration file, say:

Hey Symfony! When you output this path, please prefix it with the URL to my CDN.

This means that, when we load the the page, instead of href="/styles/app.css, it would be something like https://mycdn.com/styles/app.css.

So the asset() function might not be doing anything you need today, but anytime you reference a static file - whether it's a CSS file, JavaScript file, image, whatever, use this function.

In fact, up here, I'm referencing three images. Let's use asset: {{ asset()... and then it auto-completes the path! Thanks Symfony plugin! Repeat this for the second image... and the third.

<!DOCTYPE html>
... lines 4 - 6
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16x16.png') }}">
... lines 10 - 15
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
... lines 17 - 25
... lines 27 - 85

We know this won't make any difference today... we can refresh the HTML source to see the same paths... but we're ready for a CDN in the future.

Homepage And Browse Page HTML

So the layout now looks great! But the content for our homepage is... just kind of hanging out... looking weird... like me in middle school. Back in the tutorial/ directory, copy the homepage template... and overwrite our original file.

Open that up. This still extends base.html.twig... and it still overrides the body block. And then, it has a bunch of completely hard coded HTML. Let's go see what it looks like. Refresh and... it looks awesome!

Except that... it's 100% hard coded. Let's fix that. All the way on top, here's the name of our record, print the title variable.

And then, below for the songs.. we have a long list of hardcoded HTML. Let's turn this into a loop. Add {% for track in tracks %} like we had before. And... at the bottom, endfor.

For the song details, use track.song... and track.artist. And now we can remove all the hardcoded songs.

{% extends 'base.html.twig' %}
... lines 2 - 4
{% block body %}
<div class="container">
<h1 class="d-inline me-3">{{ title }}</h1> <i class="fas fa-edit"></i>
<div class="row mt-5">
... lines 9 - 34
<div class="col-12 col-md-8 ps-5">
<h2 class="mb-4">10 songs (30 minutes of 60 still available)</h2>
{% for track in tracks %}
<div class="song-list">
<div class="d-flex mb-3">
<a href="#">
<i class="fas fa-play me-3"></i>
<span class="song-details">{{ track.song }} - {{ track.artist }}</span>
<a href="#">
<i class="fas fa-bars mx-3"></i>
<a href="#">
<i class="fas fa-times"></i>
{% endfor %}
<button type="button" class="btn btn-success"><i class="fas fa-plus"></i> Add a song</button>
{% endblock %}

Sweet! Let's try that. Hey! It's coming to life people!

One more page to go! The /browse page. You know the drill: copy browse.html.twig, and paste into our directory. This looks a lot like the homepage: it extends base.html.twig and overrides block body.

Over in VinylController, we weren't rendering a template before... so let's do that now: return $this->render('vinyl/browse.html.twig') and let's pass in the genre. Add a variable for that: $genre = and if we have a slug... use our fancy title-case code, else set this to null. Then delete the $title stuff... and pass genre into Twig.

... lines 3 - 9
class VinylController extends AbstractController
... lines 12 - 29
public function browse(string $slug = null): Response
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null;
return $this->render('vinyl/browse.html.twig', [
'genre' => $genre

Back in the template, use this in the h1. In Twig, we can also use fancy syntax. So if we have a genre, print genre, else print All Genres.

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<h1>Browse {{ genre ? genre : 'All Genres' }}</h1>
... lines 6 - 45
{% endblock %}

Testing time. Head over to /browse: "Browse all genres"! And then /browse/death-metal: Browse Death Metal. Friends, this is starting to feel like a real site!

Except that these links up in the nav... go nowhere! Let's fix that next by learning how to generate URLs. We're also going to meet the mega-powerful bin/console command line tool.

Leave a comment!

Login or Register to join the conversation
Benanamen Avatar
Benanamen Avatar Benanamen | posted 9 months ago | edited

After following the instructions exactly and copying the files from the tutorial folder I refreshed the page and nothing changed. After some digging I discovered the page was cached by Symphony and kept reading the old page.

FIX: Clear the cache by running command: php bin/console cache:clear


C:\laragon\www\mixed_vinyl>php bin/console cache:clear

 // Clearing the cache for the dev environment with debug true

 [OK] Cache for the "dev" environment (debug=true) was successfully cleared.
11 Reply
Ngawang-choeden-S Avatar
Ngawang-choeden-S Avatar Ngawang-choeden-S | Benanamen | posted 3 months ago | edited

Thanks for that, also had same issue.


Hey Benanamen,

Yeah, clearing the cache should be the first option to try, in most cases it helps :) Thanks for this tip!



So the layout now looks great! But the content for our homepage is... just kind of hanging out... looking weird... like me in middle school

Golden 😆

1 Reply

For a quick revision on how to use filters you can add

<h2 class="mb-4">{{ tracks|length }} songs ({{ tracks|length * 10 / 2 }} minutes of {{ tracks|length * 10 }} still available)</h2>

in the homepage.html.twig file, which reads out the length of the tracklist


Hey @Brentspine ,

Thanks for sharing this tip :) Ideally, it would be great to know the exact length of each song so you could compute it with seconds approximations.


Lotfi Avatar

i just cloned the repo switched to the styling branch
i'm not seeing any tutorial folder


Hey Lotfi,

The repo is not the right way to get the correct course code as the course code is shared across many tutorials and also coded and separated with our internal tools. To get the course code you need to open any video page and in the right-up corner find the "Download" -> "Course Code" button. That's the only correct way to get the related course code for the specific tutorial with all the related materials.


1 Reply
Otacon Avatar

Hello can't get the asset path autocompletion , any idea please?


Hey Otacon,

do you have enabled the Symfony support plugin in your PhpStorm installation?

Otacon Avatar

Hi MolloKhan, yes its enabled...


Interesting... Try upgrading PhpStorm and the plugin. Then open the settings configuration and check all the options below the "code folding" label, and then click on the "Clear index" button. It should do the trick.


Peter Avatar

Hey Ryan,

Thanks for this excellent tutorial about assets.

One thing that I'm wondering how to use the asset helper (like you use in twig) in a controller, so that I could use it to generate a url to lets say $this->asset('assets/imgs/profilepic.jpg');

Any help would be much appreciated I've googled and googled but nothing really came up for Symfony 6.

Best wishes,


Hey Peter,

Excellent question! Well, if you use a Symfony plugin in PhpStorm - you can hold Cmd and click on the asset() Twig function - it should open the method responsible where you can see what service it uses behind the scene :) With this little trick you would figure out that asset() function uses Symfony\Component\Asset\Packages::getVersion() behind the scene, try to inject that service into your controller and call the getVersion() method on it for your path.

I hope this helps!


2 Reply
Benoit-L Avatar
Benoit-L Avatar Benoit-L | posted 1 year ago

Hi there, when I try to run the final solution just to see how it runs I have the following error : [Application] Apr 18 19:12:57 |CRITICA| REQUES Uncaught PHP Exception Twig\Error\RuntimeError: "An exception has been thrown during the rendering of a template ("Asset manifest file "C:\wamp64\www\code-symfony\
finish/public/build/manifest.json" does not exist.")." at C:\wamp64\www\code-symfony\finish\templates\base.html.twig line 7.
Any idea of what it could be ?

in the start version of the application, when I use the asset function it works (=it displays correctly) but I have the following error in the terminal : [Application] Apr 24 12:18:12 |ERROR | REQUES Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: "No route found for "GET" (from "https://12")" at C:\wamp64\www\code-symfony\start\vendor\symfony\http-kernel\EventListener\RouterListener.php line 130


Hey Benoit L.!

I can answer both of these :).

Uncaught PHP Exception Twig\Error\RuntimeError: "An exception has been thrown during the rendering of a template ("Asset manifest file
"C:\wamp64\www\code-symfony\finish/public/build/manifest.json" does not exist.")."

This is my fault! It looks like I need to update the README instructions for the "finish" code. What you need to do is install and run Encore. The commands are:

yarn install
# or you can run
npm install

yarn watch
# of you can run
npm run watch

That will compile the Encore assets, which includes building this missing build/manifest.json file.

in the start version of the application, when I use the asset function it works (=it displays correctly) but I have the following error in the terminal :
[Application] Apr 24 12:18:12 |ERROR | REQUES Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException:
"No route found for "GET;

This is ok, you can ignore this :). Our site, at the start, doesn't have a "favicon". But, your browser still looks for it: the first time you come to the site, it makes a request in the background to "/favicon.ico" to see if it's there. It's not, so the request is handed by Symfony, which returns a 404. That's all you're seeing, and it's nothing to be worried about. In a real app, you would create a favicon, because, of course, you want a nice looking icon in your browser when someone visits your site.


1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3