Background
Recently, I've been dipping my toes into the world of Search Engine Optimization (SEO) and "marketing" (I use that term loosely because, at the precise time of writing this, I have nothing to actually market).
I'm glad I'm starting now.
My interest is in gaining the marketable skill, but more than that, I am always interested in learning about systems of which interfaces are an integral part. And wow, SEO is all about interfacing with disparate systems and expectations. The internet as a social platform is very organic—it grows and changes in response to how humans and robots use it—and the sheer scale of it necessitates some very interesting workflows.
The actions I have taken recently are meant to establish a firm foundation upon which future endeavors will be built; and to own as much of the SEO as I can (I'm already saving at least $50 per month). To that end, I've made progress in five major areas:
- Detailed Open Graph metadata and custom preview images.
- Free campaign tracking.
- Metadata and structured data.
- Vercel analytics.
- Using Google Search Console.
Open Graph and Custom Images
The first topic is the shiniest one. If you got here through a link posted on a platform such as LinkedIn or Discord, you may have noticed a preview image tailored to this post (or you may not have, if the protocol used to collect it doesn't like my implementation). That is thanks to a dynamic image composition function in my site!
Open Graph (OG) is a metadata framework originally designed by Facebook that serves to create a rich representation of a webpage for use in their "Social Graph" mapping tool. This framework is also consumed by platforms other than just Facebook; for example, Google uses it along with several other frameworks (more to come in the Structured Data section). OG is a great way to get custom images to appear in search results and shared links, as many platforms rely on this framework to collect them. X (formerly Twitter) has their own set of metadata in which images are referenced, so most metadata should be replicated on the proprietary Twitter metadata in order to appear as intended on X.
Propagating information across multiple metadata frameworks is a good example of the technical challenges in SEO. There are so many different protocols—some competing, and some complementing each other—that maximizing your value means intentionally covering as many bases as possible. Using a system that unifies each of these frameworks in a reliable way is key to a robust and scalable SEO operation.
Custom Images
On to the cool part. Generating the images themselves was a relatively simple process, but even that required a certain amount of tinkering with the actual implementation in the wild.
I used a combination of sharp
and a good old-fashioned HTML canvas
to achieve the custom composition. Sharp
is an excellent image composition module, but it has some shortfalls with rendering text. The text rendering is where I brought canvas
in—it has much stronger and more featureful options for text.
The majority of the image is created by stitching together static images. I start with a random snapshot chosen from a collection of pictures I took from my home state of Alaska, then I blend a gradient over the top of it. Rather than spending the compute on generating a gradient on the fly, I have a simple black-to-white gradient saved statically on the server, and I can perform color alterations and/or transformations on it using sharp
to achieve interesting results.
After the attractive background is composed, the foreground is composed on top of that. The icons that are found around this site are saved as SVGs, which make for snappy, infinitely scalable images (that's what it stands for, "Scalable Vector Graphics"). For posts, I collect the skills associated with it (as an example, TypeScript is a skill related to this post) and impose their icons upon the background. Using metrics from the canvas
implementation of the text, I intelligently place these images so as to avoid clutter and collisions.
The text I keep referencing is drawn onto an ephemeral canvas
which only exists in memory. I then save it as an image buffer which can be composed as any other image by sharp
. The only real "native" text rendering that sharp
can offer is doing so via SVG, which unfortunately has little support in many environments. For this reason, I opted to use the slightly-more-resource-intensive option, which gave me much more control over the text.
import sharp from "sharp";
import { createCanvas } from "canvas";
// Step 1: Create text with `canvas`
const ctx = createCanvas(1200, 630).getContext("2d");
ctx.font = "bold 48px sans-serif";
ctx.fillText("Custom Blog Title", 600, 315);
const textBuffer = canvas.toBuffer("image/png");
// Step 2: Compose background and text with `sharp`
async function generateImage() {
return sharp("static/background.jpg") // Static background image
.composite([{ input: textBuffer, top: 0, left: 0 }]) // Add text overlay
.jpeg({ quality: 90 }) // Output as high-quality JPEG
.toBuffer(); // Return the resulting image as a buffer
}
All in all, the actual implementation of the image generation is pretty simple. It's a little resource intensive, so I do some caching to ensure this functionality isn't being called too often.
The part that took the most time was ensuring that the images were even useful for OG. I'll sum up what I learned here:
- The systems that fetch the OG image do not like parameters, so make sure that the URI for each image is canonical.
- As with all things SEO, patience is key for live production, so test locally and exhaustively.
- Know your environment; make sure all resources are available.
Serverless Function Considerations
On that last point, I have a bit of explaining to do. I'll be writing more about testing soon, but one thing that'll come up a lot is maintaining a test environment which very closely resembles your production environment.
In this project, I admittedly do not adhere to that wisdom incredibly well. My local development environment is based on a server, whereas the production environment is serverless. This means a few things, but the most salient for this feature is that my production environment has an ephemeral filesystem. The functions I used to fetch the resources when testing locally were suddenly failing when I was previewing the feature on the serverless environment.
This being the first time I'd had to make that consideration, a broken feature nearly made it to production! Luckily, I had the preview environment to work against and the knowledge of filesystem limitations.
Know your environment!
Campaign Tracking
Have you ever seen a URL with parameters like utm_source=facebook&utm_media=share
? These parameters are part of a framework called the "Urchin Tracking Module." Originally developed by a company called Urchin (hence the name), which eventually became Google Analytics, this is a set of five URL parameters used to track the source of traffic on a website. It turns a simple hyperlink into a source of simple analytics! Unlike some more dubious options (looking at you, cookies), UTM is concerned only with the source of the traffic, not the user. It's infeasible to collect detailed user information through this system because the information collected is dictated only by the URL that they click on. In addition, since the data is available in the URL, the user can actually see what will be collected before even taking the action.
There are many platforms that support this tracker, including Google Analytics (its original proprietor, of course) and Vercel Analytics' premium "Vercel Analytics Plus" platform. In January, I started tracking UTM traffic on my database by inserting an effect at the root component of my Next.js app.
import { useEffect } from "react";
// Collect, persist, and optionally remove UTM parameters
export function useUtmTracker(cleanUrl: boolean = true) {
useEffect(() => {
// Capture UTM parameters from the URL
const params = new URLSearchParams(window.location.search);
const utmParams = {
utm_source: params.get("utm_source"),
utm_medium: params.get("utm_medium"),
utm_campaign: params.get("utm_campaign"),
};
// Save them to localStorage (or backend if applicable)
Object.entries(utmParams).forEach(([key, value]) => {
if (value) localStorage.setItem(key, value);
});
// Optionally clean UTM parameters from the URL
if (cleanUrl) {
params.forEach((_, key) => {
if (key.startsWith("utm_")) params.delete(key);
});
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, document.title, newUrl);
}
}, [cleanUrl]);
}
The data is then stored in the database for future processing.
Tracking Traffic
The purpose of tracking is to make informed decisions. A well-devised UTM program will help glean insights as to which platforms and methods are the most fruitful in your marketing efforts. Consistency is important when it comes to tracking this data, especially since it can be infiltrated with garbage very easily if it is not filtered out. Any value can be provided to the parameters by malicious users.
In order to make the most of your UTM program, decide on a scalable nomenclature and stick to it. Remember that these values are case-sensitive.
The five UTM parameters are utm_source
, utm_medium
, utm_campaign
, utm_term
, and utm_content
. In this writing, we will only concern ourselves with source
, medium
, and campaign
, as they are the most commonly used.
utm_source
This parameter describes the source of the traffic and is the highest level of tracking. Common values here might be 'facebook' or 'email' and refer to the "place" where the link that led this traffic to the site is located. It's important that this parameter be concerned only with the platform/site that the link is on and not be coupled to any other details about the link. That's what the other parameters are for!
utm_medium
This parameter is a slightly more detailed (but still ideally somewhat reusable) piece of information regarding the nature of the link. Depending on your marketing campaign, this parameter could have many more (or far fewer) options than the utm_source
might. For example, since I don't pay for any ads, my utm_medium
s are things like:
- 'post' for posts on platforms like LinkedIn and X.
- 'message' for direct marketing, e.g., sending an email to a recruiter.
- 'profile' for static links, such as the one on my GitHub README.
A more involved marketing campaign might make use of different kinds of ad campaigns (such as cost-per-click or cost-per-mille) in conjunction with different utm_source
s like 'facebook' or 'google.' Notice these examples do not create duplicate sources like having 'facebook' and 'facebook_ads.' Be sure that your UTM program is intelligently designed to avoid clutter or challenges in aggregation.
utm_campaign
This is the narrowest parameter of the three most common, and also the one where one can take the most liberty. As the name suggests, this is a signifier for individual marketing efforts. Some examples might explain it best: 'summer_sale' and 'grand_opening' are good examples of campaigns you might see. This is also a good parameter to use for testing different strategies by discerning links with different content or approaches that are presented on the same platform.
Metadata and Structured Data
Metadata is vital to any SEO effort—it is the rich data that allows crawlers to easily discern the meaning and value of a webpage. The Next.js app router offers two easy-to-use methods for setting metadata on a page: the static export metadata
object and the dynamic generateMetadata()
function.
Another useful metadata framework is "Structured Data" schemas, served as a subset of JSON and found in <script>
tags on pages. These structured data schemas are one of the sources of very rich results on search engine result pages (like the author information of a research paper or the director of a film).
Recalling that I mentioned covering many different frameworks, you may see where I'm going with this.
I wrapped generateMetadata()
in another function that replicates the metadata across the different frameworks. This includes utilizing the vanilla meta
tags, meta:twitter
tags, and returning JSON-LD scripts for the Structured Data schema. All of this metadata is dynamically collected from the individually rendered pages for each of my skills, posts, projects, etc., in order to make them richer and more relevant webpages.
Vercel Analytics
The free tier of Vercel is very powerful. The continuous deployment alone is a seamless, enterprise-level tool. Next.js+Vercel also offers an incredibly powerful analytics engine that can integrate into an existing application by adding a single component to the root. These analytics are visible and well-organized out of the box on the Vercel dashboard, though some features are behind a paywall (recall from the previous section regarding UTM parameters).
Nevertheless, the free tier analytics provide very valuable insights. These analytics provide information about traffic, bounce rate, referrals, and even broad location data served by the Vercel edge network. In fact, that location data can be highly granular (down to the city) if one were to collect the header data themselves rather than just relying on the provided analytics dashboard.
Regarding detailed user data from Vercel headers, always ensure adherence to laws and best practices.
This dashboard is another source of information that can inform decisions. Combining the knowledge from these analytics as well as my UTM tracker, I can discern that the majority of my traffic comes from LinkedIn and that my posts drive nearly twice as many visitors as my profile does. I can also see that the vast majority of visitors to my site are looking at the homepage and the skills page.
Knowing these things allows me to focus my efforts in those areas. Rather than spending all of my time on blog posts, which make up around 20% of my visitors, I can focus on sprucing up my skills and homepage, which comprise a combined 44% of my visitors.
This kind of guided effort helps marketing teams make the best use of their audience's time and attention as well as their own work.
Google Search Console
The last piece of my recent journey I'd like to touch on is Google Search Console, a central hub that allows domain owners to monitor and interface with the Google Search Engine. This tool provides feedback about problems with webpages that prevent the crawler from indexing them, which can seriously harm search engine performance.
Early in development, I did not notice that Vercel had defaulted to deploying production at www.trentonyo.com
, whereas I had been focusing on the sleeker trentonyo.com
. This was a barely noticeable difference, but there was technically a 301
redirect when the crawler tried to access the URLs at trentonyo.com
as I had requested. Most users with modern internet speeds never would have noticed, but universal design is not the practice of concerning oneself with "most users." This meant I had to root out all the previously crawled pages prepended with www.
and start the crawling process over again with the sleeker base URL.
This lesson, learned early, can and will save a lot of effort and money in the future when I do more SEO for other brands. Having a consistent and canonical URL scheme is vital and vastly improves the performance of a website on search engines.
Another important early lesson is that these things take time. The crawler can take days, even weeks, to index a website properly depending on the number of fixes and changes that need to be made. It is of vital importance that any SEO efforts start well before they will be called upon for action.
The Search Console also provides more analytics about metrics such as the average search position, impressions, clicks, and search terms, as well as the location where your site appeared. This will become much more important to me in the future when I have more products to market, but as of now, it is mostly a novelty.
Conclusion
The most salient takeaway from this initial push into SEO is that it takes time. There was almost nothing quick about these improvements; while the implementation of code may have been a pretty quick turnaround, there were lengthy debugging processes involved in interfacing with black-box ecosystems and waiting for organic processes like web crawlers to conclude.
Aside from all of the technical chops and hands-on development I earned myself, I also gained insight into how the design and planning of these efforts look. I learned about many free and detailed tools for inspecting the performance of SEO efforts.
I continued to stretch my CI/CD muscles as well. For instance, I wrote a couple of new GitHub workflows: one for generating UTM links via issues in my GitHub repo for on-the-go networking, and another for internally crawling the site for broken links. The link generator is just an issue-triggered action which uses templated issues to build up a string I can use as a UTM-rich link. This is particularly handy if all I have is my phone! The internal link crawler is a simple script which fetch
es a URL and collects all the 'href' links from the returned HTML, following all the links it finds and tracking their responses (with steps to keep it to the same hostname and prevent testing duplicate links). The link checker also generates issues automatically for me to handle.
If you've been thinking about improving your site's SEO or learning hands-on tools like campaign tracking or Google Search Console, start small and get moving! Every step, even small optimizations, will lead to progress over time. If you're curious about any of the workflows or tools I shared, drop me a message or leave a comment—I’d be happy to dive deeper.