How to use tailwind with nix build?

Hey guys,

I am working on a golang project and I am trying to play around with nix. My api is built with golang, templ (HTML templating language) and tailwindcss (css library).

I can build my golang api + templ sor far with nix. But I am stuck at trying to compile tailwindcss with it. For whatever reason I don’t get any output and my styles.css isn’t being compiled. What’s weird is that templ is being compiled correctly…

When I run the app with ./result/bin/api the app works fine. I just don’t get any style as the styles.css doesn’t exist.

I would love some help if anyone know why it isn’t working. Thanks :slight_smile:

{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    {

      packages.${system} = {
        default = pkgs.buildGoModule {
          name = "api";
          version = "0.0.1";
          vendorHash = "sha256-uMWmWV9Fzvsp12I7BLIJEGsQlnCFUunnUCwGshnzvzI=";
          src = ./.;

          nativeBuildInputs = with pkgs;[
            tailwindcss_4
            templ
          ];

          preBuild = ''
            tailwindcss -i ./web/styles/styles.css -o ./public/styles.css
            templ generate
          '';
        };
      };

      devShells.${system} = {
        default =
          let
            server-watch = pkgs.writeShellScriptBin "server_watch" ''
              templ generate --watch --proxy="http://localhost:8080" --cmd="go run ./cmd/api/main.go"
            '';

            styles-watch = pkgs.writeShellScriptBin "styles_watch" ''
              tailwindcss -i ./web/styles/styles.css -o ./public/styles.css --watch
            '';

            db-cli = pkgs.writeShellScriptBin "db_cli" ''
              docker exec -it shopping_db bash -c "psql -U postgres -d shopping"
            '';
          in
          pkgs.mkShell
            {
              buildInputs = with pkgs;[
                go
                gopls
                golangci-lint
                golangci-lint-langserver
                gotools
                templ
                tailwindcss_4
                watchman
                goose

                server-watch
                styles-watch
                db-cli
              ];

              shellHook = ''
                echo "🚀 Development shell ready."
                echo "Use 'server_watch' to reload the server."                
                echo "Use 'styles_watch' to reload the css."
                echo "Use 'db_cli' to enter into the db."
              '';
            };
      };
    };
}```

Greetings, welcome to Nix! :snowflake:

  1. Does it happen in devShell, in the resulting package or both?
  2. How do you verify that styles.css is missing? It it the Go application that can’t find it or the call to templ generate?

Thanks for getting back

  1. No devShell works just fine. I have one pane where I run server_watch and antoher pane where I run styles_watch. And the tailwind compiler works just fine and go will serve the public directory correctly as well.
  2. I verified this by printing the /result directory content. This directory only has /bin/api. So I am guessing it’s missing the public directory (which should have been built by tailwind during the preBuild hook) ? templ generate works with no problem and the final binary runs perfectly fine. Except that it doesn’t have any css

In that case you might need to use the $out directory – explanation.

preBuild = ''
-  tailwindcss -i ./web/styles/styles.css -o ./public/styles.css
+  tailwindcss -i ./web/styles/styles.css -o $out/share/styles.css
  templ generate
'';

This would make the file visible in the ./result/ folder, but you need to make sure that your application knows where to find it. Also, if templ generate need this file, I think it will no longer find it. From what I understand, this situation has a very easy solution, but I don’t have much experience in packaging software with Nix, so I’m not sure I would be able to help any more than this.

Btw, have you seen templ’s documentation about Nix? link

I did try to play around with the $out variable in the preBuild hook but I never got it to work either.

In theory this should work as it build the same directories as in dev

preBuild = ''
-  tailwindcss -i ./web/styles/styles.css -o ./public/styles.css
+  tailwindcss -i ./web/styles/styles.css -o $out/bin/public/styles.css
  templ generate
'';

and the result output looks correct. But no when I go to my browser, I see a 404 styles.css not found.

[thibault@nixos:~/Documents/shopping-plo-plo]$ tree ./result
./result
└── bin
    ├── api
    └── public
        └── styles.css

3 directories, 2 files

Here are some debug logs with it helps. I am very confused why it can’t find the styles.cssas it’s right there

[thibault@nixos:~/Documents/shopping-plo-plo]$ ./result/bin/api
Current working directory: /home/thibault/Documents/shopping
✓ public directory found
✗ public/styles.css not found: stat public/styles.css: no such file or directory

That stat error looks like it might be resolving a relative path public/style.css relative to the working directory, could that be the issue?

Anyway, how I like to handle this type of static file in my Go projects is to use the embed module to embed the file directly into my binary:

package resources

import _ "embed"

//go:embed public/syle.css
var Stylesheet []byte

Then you just don’t have to deal with this type of issue.

But of course it may not always be feasible, e.g. if you have GBs of assets. In that case you could try to figure out the path to the current executable from the working directory and the command line, or check an environment variable that you set in a wrapper script:

packages.${system}.default = {
  # ...
  nativeBuildInputs = with pkgs; [
    makeWrapper
    tailwindcss_4
    templ
  ];

  postInstall = ''
    wrapProgram "$out/bin/api" --set PUBLIC_ROOT "$out/bin/public"
  '';
};
2 Likes

Thank you so much for the help @synalice @synalice guys :pray:

go it working by using the “embed” module.

2 Likes

Glad to hear you got it working! Don’t hesitate if you have more questions :slightly_smiling_face: