This past weekend, I sat down to research solutions to improve the SEO performance of 2coffee. I realized that there was one method I hadn't applied yet, which is CLS. In simple terms, CLS measures the amount of layout change that occurs on a web page. For example, imagine a user is reading your website and suddenly the text jumps down to make room for an image that is still loading. This action causes some discomfort for the user, as the sudden layout change while reading is frustrating. Now, what if it's not just the text but a button the user was about to click? It would feel like being "pranked" in a way.
My blog usually has one or more images in each post, with one main image representing the entire post. Therefore, the situation where the text is loaded first and then the image suddenly jumps in can happen. This is especially noticeable for mobile users with slow internet speeds or weak devices.
There is a way to solve this issue, which is to specify the height for the images. Initially, I thought that setting width: 100%; height: auto
would be enough, but I was wrong. Without explicitly specifying the height, the browser cannot reserve space for the image. In other words, if you want to reserve space, you need to specify the height
. This is when the issues started to occur.
height
for all images because they have different dimensions. Doing so could distort the appearance of other images. height
for each image in the markdown editor. Then, by combining it with the CSS property aspect-ratio: attr(width) / attr(height)
, I could resize the images comfortably. It seemed like everything was solved until another issue arose.My posts are stored in markdown format, which allows me to write articles without dealing with complex HTML code. The benefit of markdown is that it helps keep the articles "clean" and organized. However, the markdown-to-HTML library I'm using does not support adding attributes to the <img>
tag.
I tried to find another library, but it seems that none of them could fulfill my requirements. I attempted to read the source code of the current library for a solution, but it seemed beyond my reach. Maybe it's because I didn't understand it well enough, or maybe I didn't want to spend too much time on it. That's when I thought, if I were using Node.js, there would surely be numerous libraries that could handle this problem (For those who don't know, I'm using Golang to write the API).
Indeed, after a few searches, I found showdown, which supports adding attributes to the <img>
tag. At this point, I have to say that the Golang library has no chance against Javascript. Partly because the JS community is very large, so almost every library exists.
This led to the decision to create a separate service dedicated to parsing markdown using Javascript and showdown. But no matter what, I had to ensure that the service's calling speed was "somewhat" comparable to a native function in Go. Therefore, I decided to implement gRPC and use internal method calls.
For those who don't know, gRPC is a framework that allows you to call functions from another service with high speed and reliability. Similar to REST, gRPC uses HTTP to transport information, but it uses HTTP/2.
There are several reasons why I chose gRPC for this task. Firstly, I wanted the service to be only called internally (with no intention of publishing it). Secondly, I needed the speed benefits that gRPC provides. And thirdly, I wanted a clear distinction between internal service calls and published service calls, something that gRPC excels at.
Implementing gRPC tends to be more complex than REST. You have to write more code and the implementation of gRPC calls can be more intricate. However, the reliability and speed trade-offs make it worthwhile.
The name of this service is markdown
, and it is written in Node.js. In markdown
, there is only one very simple .proto file with one function:
syntax = "proto3";
package hicoffee.markdown;
service Markdown {
rpc ConvertMarkdownToHTML (ConvertMarkdownToHTMLRequest) returns (ConvertMarkdownToHTMLReply) {}
}
message ConvertMarkdownToHTMLRequest {
string markdown = 1;
}
message ConvertMarkdownToHTMLReply {
string html = 1;
}
The implementation in Node.js is also very straightforward:
const PROTO_PATH = __dirname + '/protos/markdown.proto';
require('dotenv').config();
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { ConvertMarkdownToHTML } = require('./handlers/markdown');
const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const markdown_proto = grpc.loadPackageDefinition(packageDefinition).hicoffee.markdown;
function convertMarkdownToHTML(call, callback) {
callback(null, { html: ConvertMarkdownToHTML(call.request.markdown) });
}
function main() {
const server = new grpc.Server();
server.addService(markdown_proto.Markdown.service, { ConvertMarkdownToHTML: convertMarkdownToHTML });
server.bindAsync(process.env.ADDRESS, grpc.ServerCredentials.createInsecure(), () => {
server.start();
console.log(`Server running at ${process.env.ADDRESS}`);
});
}
main();
There is only one main function, convertMarkdownToHTML
, which simply uses the showdown
library to convert markdown to HTML and returns the result.
In Go, implementing the client to make calls to markdown took a bit more time. Basically, it still relies on the .proto file to call the function, with the added step of building the .proto file into two *.pb.go
files before being able to use it.
$ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative protos/markdown.proto
For more detailed instructions on how to install and build the Protocol Buffer Compiler, please refer to Protocol Buffer Compiler Installation.
Finally, to check if markdown
is working, I used bloomrpc. Bloomrpc is a GUI client that allows for quick and easy gRPC calls.
Since the calls are internal, I didn't implement additional client authentication. If you plan to implement public gRPC services, consider looking into security.
Finally, I deployed everything to the server, and they work as expected.
There's no problem that can't be solved. The solution depends on your experience, starting from simple to complex. If I only knew Go and wasn't aware of gRPC... things might have gone in a different direction. I hope this article provides you with another approach to problem solving. There's no one-size-fits-all solution, only the most appropriate one based on the circumstances you choose.
Me & the desire to "play with words"
Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!
Subscribe to receive new article notifications
Comments (0)