Robert Valencia
Robert Valencia

Reputation: 1752

Thymeleaf unable to load static resources

I have a spring web application that loads the templates correctly, but does not load static assets like CSS and images. I have added my static resources within the src/main/resources/static directory, added thymeleaf annotations, added configurations and checked them against similar questions here on StackOverflow, but none of my CSS/image files are getting loaded:

Console Error

Refused to apply style from 'http://localhost:8080/login' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.

Directory Structure

Directory Structure

TemplateConfig.java

package com.valencra.recipes.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring4.view.ThymeleafViewResolver;

@Configuration
public class TemplateConfig {
  @Bean
  public SpringResourceTemplateResolver templateResolver() {
    final SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
    templateResolver.setPrefix("classpath:/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setTemplateMode("LEGACYHTML5");
    return templateResolver;
  }

  @Bean
  public SpringTemplateEngine templateEngine() {
    final SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
    springTemplateEngine.addTemplateResolver(templateResolver());
    springTemplateEngine.addDialect(new SpringSecurityDialect());
    return springTemplateEngine;
  }

  @Bean
  public ThymeleafViewResolver viewResolver() {
    final ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
    viewResolver.setTemplateEngine(templateEngine());
    viewResolver.setOrder(1);
    return viewResolver;
  }
}

layout.html

<!DOCTYPE html>
<html lang="en">

<head th:fragment="head">

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">

    <title>My Recipes</title>

    <link href='https://fonts.googleapis.com/css?family=Varela+Round' rel='stylesheet' type='text/css'>
    <link rel="stylesheet" th:href="@{/css/unsemantic-grid-responsive.css}">
    <link rel="stylesheet" th:href="@{/css/styles.css}">

</head>

<body>

    <nav th:fragment="nav">
        <a th:href="@{|/profile|}" th:text="${currentUser.name}">
            Chandra S.
        </a>
        &bull;
        <form th:action="@{/logout}" method="post" style="display: inline">
            logout
        </form>
    </nav>

    <div th:fragment="home">
        <div class="grid-100">
            <a th:href="@{|/|}">
                <h1>
                    <img th:src="@{/images/chefbot.svg}" height="60px">
                    <br>
                    My Recipes
                </h1>
            </a>
        </div>
    </div>

    <div th:fragment="favorite">
        <form th:action="@{|/recipes/${recipe.id}/favorite|}" method="post" style="display:inline">
            <button type="submit">
                <img th:src="${recipe.isFavorite(currentUser)} ? @{/images/favorited.svg} : @{/images/favorite.svg}" style="height: 15px;">
            </button>
        </form>
    </div>

</body>

</html>

login.html

<!doctype html>
<html lang="en">

<head th:replace="layout :: head"></head>

<body>

    <nav>
        <a th:href="@{/signup}">
            sign-up
        </a>
    </nav>

  <div class="grid-container">

    <div th:replace="layout :: home"></div>

    <div class="grid-100">
      <div class="recipes">

        <form th:action="@{|/login|}" method="post" th:object="${user}">
          <div class="prefix-20 grid-60 suffix-20">
            <p>
              <input placeholder="Username" th:field="*{username}"> </input>
            </p>
          </div> <div class="clear"></div>
          <div class="prefix-20 grid-60 suffix-20">
            <p>
              <input placeholder="Password" type="password" th:field="*{password}"> </input>
            </p>
          </div> <div class="clear"></div>
          <div class="prefix-20 grid-60 suffix-20">
            <p>
              <button>Login</button>
            </p>
          </div> <div class="clear"></div>
        </form>

      </div> <!-- recipes -->
    </div> <!-- grid-100 -->

  </div> <!-- grid-container -->

</body>

</html>

Upvotes: 2

Views: 4624

Answers (3)

Rafa J
Rafa J

Reputation: 61

In case it helps someone:

Intellij Idea 2023.2, Spring Boot 3.1.0., Thymeleaf 3.1.0., with Spring Security and Keycloak.

SecurityFilterChain

http.authorizeHttpRequests( registry -> registry
                .requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
                //.requestMatchers("/nice.html").hasAuthority("NICE")
                .requestMatchers("/resources/**","/static/**","/js/**","/css/**","/icons/**","/webjars/**").permitAll()
                .requestMatchers("/index*", "/index/**").permitAll()//.hasRole("operator")
                .anyRequest().authenticated());
HTML: <script type="module" th:src="@{/js/timerangeslider.js}"></script>

I could not load all static resources. The reason was Thymeleaf or Spring, or maybe Intellij, were using two different paths for my static content, don't know why. A .js file was not loaded and with Firefox inspector I checked that the src path was wrong. The solution is a bit silly, just moved the file with the mouse to another static folder and said yes to refactor when asked. Then moved back the file to the correct folder, said yes to refactor and that's all. It's kinda stupid, but maybe this will save time for someone.

Upvotes: 0

Vishnoo Rath
Vishnoo Rath

Reputation: 580

What helped in my case was answer from @phisch and the following code

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(
            "/webjars/**",
            "/img/**",
            "/css/**",
            "/js/**")
            .addResourceLocations(
                    "classpath:/META-INF/resources/webjars/",
                    "classpath:/static/img/",
                    "classpath:/static/css/",
                    "classpath:/static/js/");
     }

}

Upvotes: 0

phisch
phisch

Reputation: 4731

It looks like your CSS is served as HTML.

Since you put the Stylesheets in src/main/resources/static Spring should set up most things based on sensible defaults, including setting the correct content-type header.

I assume you are using Spring Security, since you showed a login form.

It could be that your resources are protected by Spring Security. Thus instead of your stylesheet you get a 30x redirect to the login page. This would explain why your browser complains about the text/html content type, which is wrong for css but the correct type for a login page.

Do you configure HttpSecurity somewhere in your project (most likely annotated with @EnableWebSecurity)?

If so, you need to allow anonymous access to your resources:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/css/**", "/images/**").permitAll()
            // rest of your code 

If you switch to the network tab in your browser console, you should be able to inspect the redirect to the login page.

Upvotes: 5

Related Questions