Depending on your server location or even SEO purpose you’ll see benefits of CDN. If you run the Lighthouse report you’ll most likely get a warning about assets not having a good caching policy. You can work this around with your Nginx or Apache. But still, someone visiting your website from the other side of the globe would feel the load time. This is where global caching would become handy. You can do a lot of stuff with global caching.
Vite is a modern front-end build tool designed for lightning-fast development and efficient code bundling for production. In the context of Laravel applications, Vite plays a crucial role in bundling CSS and JavaScript files into production-ready assets.
Laravel seamlessly integrates with Vite through an official plugin and a Blade directive, simplifying the process of loading assets for both development and production.
Prerequisites
- Basic familiarity with Laravel and Vite.
- An account with a CDN provider such as Cloudflare or AWS (CloudFront and S3).
Building assets with Vite
Laravel 9 and above versions come with Vite configured by default. However, if you want to configure the Vite manually refer to Asset Building Docs.
Building assets for production in Laravel is straightforward. Execute the npm run build command, which compiles and builds assets into the public/build folder.
One of Vite’s advantages lies in its ability to generate new files with different names whenever changes occur in assets. This ensures that the latest files are loaded in the browser, addressing potential caching issues at various levels.
Using Vite directive
With the Vite Blade directive, you can rely on Vite to handle asset loading on your pages. Simply add the Vite Blade directive to the <head> of your application’s root template:
<!doctype html>
<head>
{{-- ... --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
Configuring CDN
There are many CDN providers out there, for this post I will be covering AWS CloudFront and CloudFlare.
AWS CloudFront
Amazon CloudFront is a content delivery network (CDN) service built for high performance, security, and developer convenience. CloudFront can be configured with an S3 bucket that would distribute and cache the objects for better delivery.
Laravel comes with a default S3 disk preconfigured. Depending on your application you may want to duplicate the s3 disk configuration in config/filesystems.php with a new disk called cdn or you can use the same S3 bucket for both but you need to configure ACL policies for private objects. To keep things simple let’s duplicate the disk and create a separate public S3 bucket.
Update the config/filesystems.php and add a new disk cdn:
<?php
return [
// Other configurations
'disks' => [
// Other disk configurations
'cdn' => [
'driver' => 's3',
'key' => env('AWS_CF_ACCESS_KEY_ID'),
'secret' => env('AWS_CF_SECRET_ACCESS_KEY'),
'region' => env('AWS_CF_DEFAULT_REGION'),
'bucket' => env('AWS_CF_BUCKET'),
'url' => env('AWS_CF_URL'),
'endpoint' => env('AWS_CF_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_CF_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
// Other configurations
]
This will allow you to communicate with S3 using the storage facade, For example Storage::disk('cdn'). Now update the .env with the following environment variables.
AWS_CF_ACCESS_KEY_ID=
AWS_CF_SECRET_ACCESS_KEY=
AWS_CF_DEFAULT_REGION=
AWS_CF_BUCKET=
AWS_CF_URL=
To arrange these credentials we’ll need to configure an S3 bucket and then set up CloudFront distribution.
- First, from the AWS console create a S3 bucket with default settings. (ACL disabled and Block All Public Access). Once created update the
AWS_CF_BUCKETandAWS_CF_DEFAULT_REGIONbased on your configurations - Then create a CloudFront distribution (For a more detailed guide refer to this article):
- While creating the distribution select the S3 bucket for the Origin access that we created in the previous step. This will auto-populate some settings.
- From there under Origin access, Select Origin access control settings. Then it’ll show the Create control setting button, click on it to create the default Create control setting with Origin type S3.
- Then you can Enable security protections from under the Web Application Firewall but for CDN I’ll be leaving it Disabled.
- Optionally, from the settings card you can configure the custom domain which would require additional configuration including adding a CNAME record and configuring an SSL certificate. To keep things simple I’ll be skipping the setup for the custom domain.
- Finally, click on Create Distribution. This will create a distribution with an alert on the console showing to update the bucket policy. Click on Copy Policy from the alert then go to bucket permissions. Then edit the bucket policy paste the copied policy settings and save the changes.
- After this setup, your distribution should have the domain name under the General Details card. For example xxxxxx.cloudfront.net, This will be the
AWS_CF_URL.
- Create IAM access keys for the Laravel Application:
- Create a new user
cdn_user(leave everything default). - Select the user
cdn_userand under the permissions click Add Permissions and select Create Inline Policy. - Now switch from Visual Editor to JSON and paste the following:
- Create a new user
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::ENTER_YOUR_BUCKET_NAME",
"arn:aws:s3:::ENTER_YOUR_BUCKET_NAME/*"
]
}
]
}
Make sure to replace the ENTER_YOUR_BUCKET_NAME with your bucket name. After making the changes save the policy. Then from under the Security Credentials inside cdn_user create a new Access Key. Based on your scenario select either option. I will continue with Application running outside AWS.
You should now have the Access key and Secret access key which will be your AWS_CF_ACCESS_KEY_ID and AWS_CF_SECRET_ACCESS_KEY respectively.
Cloudflare R2
If your site is behind Cloudflare, it will automatically cache assets over the CDN. However, in certain situations, such as when you enable caching and set a policy for a specific duration, there’s a possibility that a request might not find the cached content and might have already done more deployments. When you run the Vite build, the old files get replaced by the new ones, which can occasionally lead to the loading of outdated HTML and attempts to load old assets.
Cloudflare R2 is S3-compatible object storage offered by Cloudflare. This means we can use the same S3 driver to configure the R2 disk. To configure R2 with Laravel add the cdn disk into config/filesystems.php :
<?php
return [
// Other configurations
'disks' => [
// Other disk configurations
'cdn' => [
'driver' => 's3',
'key' => env('CLOUDFLARE_R2_ACCESS_KEY_ID'),
'secret' => env('CLOUDFLARE_R2_SECRET_ACCESS_KEY'),
'region' => 'us-east-1', // ignore region
'bucket' => env('CLOUDFLARE_R2_BUCKET'),
'url' => env('CLOUDFLARE_R2_URL'),
'endpoint' => env('CLOUDFLARE_R2_ENDPOINT'),
'use_path_style_endpoint' => env('CLOUDFLARE_R2_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
// Other configurations
]
This will allow you to communicate with R2 using the storage facade, For example Storage::disk('cdn'). Now update the .env with the following environment variables.
CLOUDFLARE_R2_ACCESS_KEY_ID=
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
CLOUDFLARE_R2_ENDPOINT=
CLOUDFLARE_R2_BUCKET=
CLOUDFLARE_R2_URL=
To arrange these credentials:
- First, create an R2 bucket by following this R2 getting started guide. After creating the bucket you can set the name of the bucket into for
CLOUDFLARE_R2_BUCKET. - Inside the bucket settings, under bucker details, you will find the
S3 APIendpoint which is yourCLOUDFLARE_R2_ENDPOINT. - Set up a custom domain to enable CDN: Under the bucket settings enable public access by connecting the domain. This will allow Cloudflare to cache the assets to accelerate the object delivery. More details about custom domains are here. You’ll need to configure CNAME if your domain is outside of Cloudflare. I suggest using a subdomain like
cdn.yourdomain.com. This will be theCLOUDFLARE_R2_URL. - For the Access ID and Access Key follow this post to create a new API Token for Bucket.
Creating asset publish scripts
Depending on your CI/CD setup, you might choose to release assets either from the production server or directly from the deployment pipeline environment. Deploying directly from the CI/CD pipeline is a more advanced approach, but I’ve included instructions for both methods. The first artisan command is for publishing from the server, while the second is for publishing directly from the CI/CD pipeline using the node environment.
Create artisan vite:publish command
Since Laravel already has credentials for the bucket, the simplest way to upload assets to the CDN is through an artisan command. Create a new command by running the following:
php artisan make:command VitePublish
This will create a new command at app/Console/Commands/VitePublish.php. Now, update the command as follows:
<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
use IlluminateSupportFacadesFile;
use IlluminateSupportFacadesStorage;
class VitePublish extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'vite:publish';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Publishes the Vite build to configured CDN';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Publishing assets to CDN');
$buildFiles = File::allFiles(public_path('/build'));
foreach ($buildFiles as $asset) {
$this->info('Uploading asset to: build/' . $asset->getRelativePathname());
Storage::disk('cdn')->put('build/' . $asset->getRelativePathname(), $asset->getContents());
}
$this->info('Vite assets published successfully');
}
}
This code iterates through all files in the /build directory within the public folder of the application. For each file, it logs a message indicating the file is being uploaded to a specified location (build/ followed by the file’s relative path). Then, it uploads each file to a storage disk named cdn at a similar path (build/ followed by the file’s relative path) using the file’s contents.
Create npm run vite:publish
This method would be useful for publishing assets directly from the CI/CD pipeline. For example, you can publish the assets from GitHub Actions or Gitlab/Bitbucket Pipeline. Do note for this to work you’ll need to configure the correct environment variables in your pipeline environment.
Since Cloudflare R2 is S3 compatible we can use the AWS S3 client to communicate with both CDN. Install the AWS S3 client :
npm i --save-dev @aws-sdk/client-s3 mime-types
Then create a new file vite-publish.js in the project root. Paste the following content:
import fs from "fs";
import path from "path";
import mime from "mime-types";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
// Read credentials and region from environment variables.
const bucketAccessKeyId = process.env.CDN_BUCKET_ACCESS_KEY_ID;
const bucketSecretAccessKey = process.env.CDN_BUCKET_SECRET_ACCESS_KEY;
const bucketEndpoint = process.env.CDN_BUCKET_ENDPOINT;
const bucketName = process.env.CDN_BUCKET_NAME;
// Configure S3 client with your access and secret keys.
const s3Client = new S3Client({
region: "us-east-1", // ignore this for R2 but replace with correct one for S3.
endpoint: bucketEndpoint, // you can skip this for S3.
credentials: {
accessKeyId: bucketAccessKeyId,
secretAccessKey: bucketSecretAccessKey,
},
});
// Upload files to CDN bucket.
const directoryPath = "./public/build";
const assetsPaths = fs.readdirSync(directoryPath, { recursive: true });
assetsPaths.forEach((assetPath) => {
const filePath = path.join(directoryPath, assetPath);
if (fs.lstatSync(filePath).isFile()) {
console.log(`Uploading file: ${assetPath}`);
// Determine the MIME type based on the file extension
const contentType = mime.lookup(filePath) || 'application/octet-stream';
s3Client.send(new PutObjectCommand({
Bucket: bucketName,
Key: `build/${assetPath}`,
Body: fs.readFileSync(filePath),
ContentType: contentType,
})).then((data) => {
console.log(`File "${assetPath}" uploaded successfully.`);
}).catch((error) => {
console.error(`Error uploading file "${assetPath}":`, error);
});
}
});
In the above script, you need to set and update the environment variables and s3Client configuration based on your needs. This script reads all the file paths from public/build and then uploads all of them to the bucket based on variables passed into the s3Client configuration.
Finally, update the package.json with to register the command:
{
// ...
"scripts": {
"dev": "vite",
"build": "vite build",
"vite:publish": "node ./vite-publish.js"
},
// ...
}
Deploying assets to CDN
Finally, update the .env and add the following to configure the CDN endpoint for Vite. Make sure to replace the URL as per your configured domain on CDN. This will be the same as AWS_CF_URL or CLOUDFLARE_R2_URL.
ASSET_URL=https://cdn.yourdomain.com
To ensure updated assets are deployed, run the following command sequence during deployment:
npm run build
Then publish them to the CDN, depending on your configuration you can run:
php artisan vite:publish
or using the npm command:
npm run vite:publish
Resolving CORS Errors
If you encounter CORS errors in the console for some JavaScript files, this is likely due to the CDN being hosted separately. To fix these CORS issues, set up the correct CORS policy on your Bucket and CDN service, such as adding your domain to the allowed host list.
Conclusion
Integrating Vite with Laravel and a CDN enhances performance and simplifies asset management, offering a more efficient and user-friendly web development experience. There are many settings you can fine-tune, refer to the asset-building docs.
Keep the Conversation Going
I hope you found this post helpful! If you have any questions or feedback, feel free to reach out. You can also find me on X (Twitter) @priyashpatil for additional insights and updates on my latest content.