I just had to take a markdown service out of my API

I just had to take a markdown service out of my API

Daily short news for you
  • Hic, I've been really busy lately, and there have been so many changes that I haven't had time to write a longer post. Instead, I'm trying to maintain this short daily update for readers. Since it's short, it's much quicker to write.

    I'm currently tweaking the interface a bit while also adding a new feature. Can you guess what it is? Here's a hint: it has something to do with AI and Tiktok 😁

    » Read more
  • Currently on the 14-day trial of Windsurf, today is day seven, and I have some quick impressions as follows:

    First, the interface is a bit more customizable, giving a flatter and friendlier feel compared to the traditional VSCode.

    Second, the suggestions are super fast but a bit hasty. They’re not always accurate, yet they confidently offer several lines at once. So, not every tab tab is correct. However, it reads context well, better than Copilot.

    Third, the Chat/Edit feature is top-notch, very good, almost a perfect understanding, probably on par with Cursor, but I’m not entirely sure; that's just how it feels.

    Additionally, one annoyance is that sometimes it suggests but the tab doesn’t match, which makes it a hassle to delete.

    I wonder how it will be after the 14 days, so I will continue to update. But overall, it’s way better than Copilot.

    Oh! One more thing, the Vietnamese in this one is terrible. I have no idea why!?

    » Read more
  • smee.io is a simple way to create a webhook address and map it to the localhost address on your computer.

    $ npm install --global smee-client $ smee -u https://smee.io/eu4UoW8vrKSZtTB

    » Read more

The Issue

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.

Development

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.

low CLS

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.

  • First, it's not possible to set a fixed height for all images because they have different dimensions. Doing so could distort the appearance of other images.
  • The solution I thought of was to explicitly specify the 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.

Approach

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.

Testing the markdown function

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.

Conclusion

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.

Premium
Hello

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!

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!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...
Scroll or click to go to the next page