TL;DR
I built this website using Blazor and Next.js then deployed both parts to Vercel. If you're interested in the why or the how then keep reading!
Table of Contents
Overview
When planning my 2024 goals, I knew that I wanted to get hands-on with some new technology. Suddenly, I found the following LinkedIn post from Lars Klint.
He mentioned Blazor, which was something I hadn't heard about before. I am traditionally a Java developer, yet I am never afraid to try something new. I quickly prepared myself with a couple amazing courses on Pluralsight.
While training, I was thinking of a practical project that I could use to solidify my knowledge. It was then that I recalled a challenge that Jeremy Morgan had created during his time at Pluralsight that I intended on completing but never started. The first of an incomplete series, "Code Portfolio Challenge" tasked with building a resume website dynamically from a JSON file. Although I missed the deadline back in June 2022 by a tad, I thought this would be the perfect way to flex my Blazor knowledge.
Blazor
I found the coding experience for Blazor to be similar to that of Angular and React. The difference is that my templates, aka razor files, were mixed with C# instead of JavaScript or TypeScript. Shared components could be created and nested in other pages, just like the other frameworks allow.
I decided to run with the default Blazor template that comes from the dotnet new blazorwasm -o TodoList
command. The site was aesthetically pleasing from the start. Besides, my goal wasn't to learn CSS; I already knew that.
Http.GetFromJsonAsync
If you're already familiar with C#, then I'm sure you know that you can load a static file using Http.GetFromJsonAsync. That is exactly what I did in order to load the resume.json data that I had created.
// Home.razor
@if (_resume == null)
{
<h1>Loading...</h1>
}
else
{
<PageTitle>@Resume.Name</PageTitle>
}
@code {
private Resume? _resume;
protected override async Task OnInitializedAsync()
{
_resume = await Http.GetFromJsonAsync<Resume>("data/wheeler-resume.json");
}
}
CascadingValue and CascadingParameter
After hacking away for ten minutes, I realized that this app was going to be terrible if I had to fetch the static resume over and over again on every page I had. CascadingParameter to the rescue!
Cascading values and parameters provide a convenient way to flow data down a component hierarchy from an ancestor component to any number of descendent components.
// MainLayout.razor
<CascadingValue Value="@_resume">
@Body
</CascadingValue>
// Home.razor
@code {
[CascadingParameter]
public MainLayout.Resume Resume { get; set; } = null!;
}
Now any component that needed the resume data could simply wire it up for dependency injection to save the day!
Oh My Mobile! 😱
With the rise of web activity on cell phones, I knew that it wasn't negotiable that even this side project had to work on mobile and look good too. For most of the styles, I could use traditional media queries. Although, in some cases, I wanted to manipulate the HTML itself for mobile. For that to work, I had to fetch the screen size myself.
First, I added the following script to my index.html
to create a JavaScript function that I could find with the IJSRuntime. Then I created a service that would allow me to isolate that logic from any specific component, as I knew I would need it in multiple places.
// index.html
<script type="text/javascript">
window.BrowserServiceGetDimensions = function () {
return {
width: window.innerWidth,
height: window.innerHeight
};
};
</script>
// BrowserService.cs
using Microsoft.JSInterop;
public class BrowserService
{
private readonly IJSRuntime _js;
public BrowserService(IJSRuntime js)
{
_js = js;
}
public async Task<BrowserScreenSize> GetScreenSize()
{
var dimensions = await GetDimensions();
return dimensions.Width switch
{
>= 1024 => BrowserScreenSize.Desktop,
>= 768 => BrowserScreenSize.Tablet,
_ => BrowserScreenSize.Mobile
};
}
private async Task<BrowserDimension> GetDimensions()
{
return await _js.InvokeAsync<BrowserDimension>("BrowserServiceGetDimensions");
}
}
public class BrowserDimension
{
public int Width { get; set; }
public int Height { get; set; }
}
public enum BrowserScreenSize
{
Desktop,
Mobile,
Tablet,
}
One usage of this logic was to reduce the number of social icons I displayed on the footer.
// SocialFooter.razor
@code {
[Parameter]
public MainLayout.ResumeUrl[] Urls { get; set; } = null!;
private MainLayout.ResumeUrl[]? _urls;
protected override async Task OnParametersSetAsync()
{
_urls = await GetUrlsForScreen();
}
private async Task<MainLayout.ResumeUrl[]> GetUrlsForScreen()
{
var screen = await Browser.GetScreenSize();
return screen switch
{
BrowserScreenSize.Tablet => Urls.Take(7).ToArray(),
BrowserScreenSize.Mobile => Urls.Take(5).ToArray(),
_ => Urls
};
}
}
Anchor Navigation
Frustratingly, anchor navigation didn't work out of the box for Blazor. Luckily, I found Meziantou's blog which had a great workaround that I could use.
private async Task ScrollToFragment()
{
var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
var fragment = uri.Fragment;
if (fragment.StartsWith('#'))
{
// Handle text fragment (https://example.org/#test:~:text=foo)
// https://github.com/WICG/scroll-to-text-fragment/
var elementId = fragment[1..];
var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
if (index > 0)
{
elementId = elementId.Substring(0, index);
}
if (!string.IsNullOrEmpty(elementId))
{
await JsRuntime.InvokeVoidAsync("NavAnchorScrollToId", elementId);
}
}
}
Vercel
I had no prior experience with Vercel before attempting this. I only had the idea because it was one of the few platforms Jeremy mentioned in the challenge posting that I hadn't already experimented with.
I would be lying if I said that I didn't struggle to deploy Blazor to Vercel.
- Build Command
dotnet publish --configuration Release --output Publish --self-contained
- Output Directory
Publish/wwwroot
- Install Command
rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm && yum install dotnet-sdk-7.0 -y
Storage
Maybe that would've been sufficient. Except I accidentally learned about Vercel Storage. Did you know that Vercel has free storage offerings like Redis, Postgres, and Blob? I sure didn't know that. Once I did, I had the crazy idea to enhance this application with a share feature.
I thought it would be interesting for other people to be able to upload their own resume (based on my format) and re-generate the page dynamically. It was easy enough to change the source of the resume in the code. It was much harder to persist that data permanently so that I could recall it with a unique URL.
Because the data uploaded could omit literally any field, I had to cover my bases in the code with lots of null checks. Now the user can choose which fields to retain, and the sections on the page will disappear like magic.
Resume = {}
When importing a JSON, I am saving it in Vercel Blob storage with a unique ID. Then provide a share URL that can be used to reload that same resume. For example, https://cgc-resume-from-json.vercel.app/?id=f87791d3-c359-42e8-8be2-067ce54a2e20.
With this feature, even non-technical people could leverage this site for their own virtual resume without writing any code!
JSON is not code. 😶
Security
Uploading to the Vercel Blob service required some critical thinking. Originally, I didn't want to build a backend for this project. I was pretty fixed on the frontend only.
Unfortunately, it had to be done. You may not know this, but there is absolutely no secure way to protect sensitive keys like the BLOB_READ_WRITE_TOKEN
, in a client side app. A sophisticated hacker can find that data.
Next.js
Did you know that Vercel created Next.js? Well, I didn't. That is, until I tried to navigate the Vercel documentation. I have never felt pressured to use a specific technology like the way Vercel pressured me to use Next.js. All of their documentation pointed towards it. Trying to use any other technology seemed like I was on my own.
It was at this pivotal point that I had a decision to make.
- Do I jump into Next.js, a framework I've never used, for the ease of documentation and integration?
- Do I struggle through the documentation and implementation with a backend framework or language that I already know?
Since I would be struggling in either scenario, I chose to dive into Next.js. I've heard the splash it's made in the community and thought it would be a great learning opportunity.
One more quick course on Pluralsight to ramp up.
API
I got started with npx create-next-app@latest
to scaffold the project. Then I deleted all the files that I didn't need and created a pages/api
directory to hold the API code.
Any file inside the folder
pages/api
is mapped to/api/*
and will be treated as an API endpoint instead of a page. They are server-side only bundles and won't increase your client-side bundle size. Docs
For this simple app, I only required a single TypeScript file, blob.ts, with two methods to GET
and PUT
.
The putBlob
function accepts the resume data as the request body in JSON format and uploads the file to Vercel Blob storage using the SDK.
import { put, PutCommandOptions } from '@vercel/blob';
import type { NextApiRequest, NextApiResponse } from 'next';
async function putBlob(
req: NextApiRequest,
res: NextApiResponse<BlobResponse>
): Promise<void> {
if (!req.body) {
sendError(res, 400);
return;
}
const id = crypto.randomUUID();
const options: PutCommandOptions = {
access: 'public',
addRandomSuffix: false,
contentType: 'application/json',
};
try {
const result = await put(`${id}.json`, JSON.stringify(req.body), options);
const { downloadUrl, pathname, url } = result;
res.status(200).json({ id, downloadUrl, pathname, url });
} catch (error) {
console.log(error);
sendError(res, 500);
}
}
The getBlob
function accepts the unique id as a query parameter and uses that to pull a single blob from the list
method.
import { list } from '@vercel/blob';
import type { NextApiRequest, NextApiResponse } from 'next';
async function getBlob(
req: NextApiRequest,
res: NextApiResponse<BlobResponse>
): Promise<void> {
const id = req.query?.id as string;
if (!id) {
sendError(res, 400);
return;
}
const result = await list({
limit: 1,
prefix: `${id}.json`,
});
if (!result?.blobs?.length) {
sendError(res, 404);
return;
}
const { downloadUrl, pathname, url } = result.blobs[0];
res.status(200).send({ id, downloadUrl, pathname, url });
}
Deployment
With the end in sight, one last battle. I created a monorepo to improve organization. I didn't want to clutter my GitHub with several disjointed repositories for the same small project.
I couldn't see any clear way to manipulate the "Build & Development Settings" on a per-directory basis. I did see that they had "Root Directory" support at the project level, though. I created another Vercel project for the same GitHub repository. Then I adjusted the "Root Directory" for both projects.
At least since I opted to use Next.js, the presets worked to get the app up.
Retrospective
Looking back, I actually learned a lot. Now I've used Blazor, Next.js, and Vercel. Before I started this journey, I hadn't touched any of them before.
With that said, I don't think that I would go this route again. I was able to stitch these components together like a baby Frankenstein monster, but I don't necessarily recommend it.
To save yourself the headaches I suffered,
- Deploy Blazor to Azure
- If you want to use Vercel, then use Next.js
I would encourage you to check out the website for yourself. Maybe share it with your friends and colleagues. I would love any brutally honest feedback. https://cgc-resume-from-json.vercel.app/
As always if you liked this content, maybe you would like to Buy Me a Coffee or connect with me on LinkedIn.
Top comments (0)