Setting up a Hugo static site inside Laravel on Forge

Making a hybrid static + dynamic site can be quite a useful combination. This might sound like an oxymoron, but it simply means serving static pages from a site that also runs a dynamic web application. You can get some of the best of both worlds with this approach.

My Chinese-learning site ChineseBoost uses this combination – a lot of the content pages, e.g. for Chinese grammar, are statically generated with Hugo, while dynamic parts of the site are served by a Laravel application. Similarly on our pop out cards shop, we have a static blog and an ecommerce application on the same server.

This is easy to achieve when you’re using a webserver like nginx. Your nginx installation is probably set up by default to serve static files that it finds in the public directory with directives like this:

index index.html index.htm index.php;

location / {
    try_files $uri $uri/ /index.html /index.php?$query_string;

The config file is usually at e.g. /etc/nginx/sites-available/foo.conf.

This means that if you have a file at public/foobar-static-slug/index.html, nginx will serve that at Nginx is fast at everything, and especially fast at serving static files, so this will perform nicely with minimal load.

Read more about serving static content with nginx.

Set up Hugo inside Laravel

You can have Hugo output your static content straight to the public directory where nginx will then serve it.

I would suggest having a separate (non-public) directory for your static content, e.g. blog at the top-level of your repo, which your static source files live in. You could have a separate repo for this and pull it in to do the static generation, but I prefer keeping related things in the same repo as much as possible.

You have a standard Hugo set-up inside your blog directory, with the important config.yaml file which controls how the site gets generated. There is nothing different or special about how you set up this Hugo site, despite it being inside a repo that is primarily for a Laravel application.

The approach I take is to use a symbolic link to the static files that Hugo generates, at the path I want the static content to appear in the public directory. For example, if you want your static content on /blog/, you’d do something like:

cd /home/whoever/

yarn install # etc for blog setup...

if [ ! -f ./hugo ]; then
    tar -vxzf hugo_0.62.2_Linux-64bit.tar.gz


ln -sf /home/whoever/ /home/whoever/

That can be run on a server during your deployment process.

Now the blog content is served on /blog/.

Because of how nginx checks for the existence of static files, you can still have Laravel routes that start with /blog/, so long as there isn’t a real index.html at that location on disk.

Automatically regenerating on deploy with Forge

This works quite nicely with Laravel Forge, as you can put the above script in the deploy script for a site, and Forge will then regenerate your static content each time you merge to master as part of its deployment process.

Your homepage can be static

Just a note that it can be beneficial to have a static HTML file as your homepage by putting an index.html file inside public. The homepage often gets a large proportion of traffic, and serving it statically reduces the load to minimal levels. It also means your homepage is likely to stay up even if something goes wrong with your Laravel application, which looks better than the whole site going down.

If you’re regenerating the static content on each deploy (or more regularly, see below), you can still have regularly updating “live” content on the homepage even when it’s a static file.

If you don’t want to generate that file with Hugo, it’s quite easy to get Laravel to write the content of your homepage Blade view file to public/index.html, so you can control that page from Laravel using your full application, but have the benefits of it being static.

Scheduled Hugo regeneration with Laravel console command

Laravel makes it easy to run application commands and bash scripts on a schedule (one of my favourite features of Laravel), and you can utilise this to automatically regenerate your static content regularly, e.g. once an hour or whatever cadence suits your content:


$schedule->exec('(cd /home/whoever/ && ./hugo)')->hourly();

Note that that assumes the Hugo binary is present there and that the output files are linked from the public directory as described above.

Now the static content is kept up-to-date at all times. If your Laravel application or scheduler goes down, you will at least have the most recently generated static content being served, so the whole site won’t be down.

Laravel + Hugo data files is fun

One final thing that is quite useful is combining this with Hugo’s data templates.

Hugo can read from JSON or CSV files while it is generating the site, and it’s easy to have Laravel write these files in a scheduled command using your site’s dynamic data.

For example, Pop Robin Cards has product catalogue data from Laravel written into JSON files for Hugo to use when generating the static content. Because both get refreshed regularly, this stays up to date but keeps the benefits of managing static content.

Tech mentioned