Retro Never Dies — Building your CV site using Scala.js

Hussachai Puripunpinyo
12 min readOct 8, 2020

I remember the good old days when we geocities together.

Are you using Linkedin? As you know, Linkedin is a professional network and their platform is great for connecting people (professionally). I have more than 500+ connections and more than a half are the people that I don’t know or never talked with or both. Having Linkedin profile is great, but you will be like millions of people out there and that makes it less cool. If you want to be different, you have to do something different. Being different makes you standout. Whether it’s better or worse, you’re surely more memorable. Today I will share my journey (and my code) of spinning up my CV site that cost me almost nothing besides my custom domain that I also use it for many other things.

As the title of the article says, I’m going to build my CV site using Scala.js. Scala is known as a hard-to-learn language, but it’s not actually true. Scala is not hard, but Scala.js is :). The hard part is that you have to use very specific versions of a lot of things or it won’t compile or you will get O̶u̶t̶O̶f̶M̶e̶m̶o̶r̶y̶E̶r̶r̶o̶r̶:̶ ̶G̶C̶ ̶o̶v̶e̶r̶h̶e̶a̶d̶ ̶l̶i̶m̶i̶t̶ ̶e̶x̶c̶e̶e̶d̶e̶d̶”̶ ̶t̶h̶r̶o̶w̶i̶n̶g̶ ̶f̶r̶o̶m̶ ̶S̶B̶T̶(Updated: Oct 14, 2020 — Figured out that this error was caused by the Scala compiler. The issue’s been reported). The transpiler works well (if it works). It just needs more love, and of course more testing. The hard part is to get everything working together. Most of the time, you cannot rely on Google or Stackoverflow because not many people are actually using it or they didn’t have any problem at all but I doubt that 🤔. Once you get all of those things sorted out, the work is pretty rewarding.

The Functional Requirements

I will list what we need for a retro-style website.

  1. The flashy background and flashing GIFs. The GIF images having these words “Welcome”, “Hot”, “New” are required. The perfect one will be a spinning 3D word.
  2. Tables. We gotta design the layout using Tables. Tables are great! Divs are bad!
  3. Marquee. We need a moving text. My first achievement in programming was that I could program the text to animate across the screen and it’s simple as surrounding that text with <marquee>. The tag is sadly obsolete and it’s not available in the DOM library I’m using.
  4. Counter. You want to brag people how popular your site is, right? This is how we do. Sometimes, we start the counter at 5,000 even before it’s seen by anyone. Haha!
  5. Guestbook. This is absolutely necessary. It’s super cool when strangers compliment you for the beautiful design or trying to send you money or selling lottery.

I know I miss a ton of cool stuffs like flashy scrollbar, mouse trail, javascript effects, MIDI music, … etc, but I have to come up with the MVP first.
I looked for a retro theme and I found this gem geo-bootstrap. Unfortunately, the development seems to be discontinued. So it’s stuck at Bootstrap 2. Anyway, I think it’s a good starting point.

🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
Let’s see it in action here => https://hussachai.tailrec.io
🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

The Non-Functional Requirements

  1. Scala will be used in place of HTML and Javascript.
  2. We will build the whole page using DOM API. Doing it with Javascript is not cool. We have to use Scala for this to make sure the types are correct at compile time (Not sure that we really need this but for the coolness sake it’s absolutely needed.). We use html.scala for that.
  3. Data will be fetched from an external JSON source. The JSON file must be encrypted using AES. Thanks to the forge.
  4. The page must be reactive. The DOM should reflect to the model change. We use Binding for state management and change propagation. There are alternative stacks for this and the similar ones are Scala.rx and Scalatags that are made by Li Haoyi. I used those libraries 6 years ago and Li Haoyi has earned my respect for still maintaining those libraries. Kodos to him. Anyway, this time I just wanted to try something new.

This project is more like a hobby and I have to relearn every time I get back to Scala.js. I used Scala.js 6 or 7 years ago for a short while, I got back to it 3 years later for a side gig which took like 3 days to complete (mostly due to relearning things lol). And now the due date has come, I have to relearn this every 3 years I guess. The sad part is that things are still pretty the same. The community is still very small and it’s quite concerning. Everything seems to be running by enthusiastic talent people.

I also realized that I’ve never written anything about Scala.js. So, it’s a good time to write up something devoting to Scala.js. It’s a great technology and it needs more love ❤️.

Prerequisites

You need to know Scala for sure, but Scala.js not so much because it’s basically Scala. Note that Scala.js transpiler does not support all of the classes in Java SDK. They have to rewrite some of those classes in Scala and there are some classes that are not supported by Scala.js. To give you a concrete example, take a look at this scala-js-java-time. Keep in mind that the output is not bytecode but JavaScript code. JVM and JavaScript are different lands with different rules. The good side of running Scala in the JavaScript environment is that there are a ton of libraries out there that you can use. Thanks to the big success of the NodeJS community.

You need:

  1. sbt for building the project and you need sbt version 1.+
  2. I believe you don’t need to install Scala compiler separately since sbt can handle this for you. I’m using Scala 2.13.3 in this project since some libraries are not available in other version.
  3. You need Java ≥ 8. I’m using Java 15 for this but I believe any version that is equal or greater than 8 is fine.
  4. NodeJS for executing JavaScript code.
  5. The following sbt script that has been proven to be working
name := "hussachai"

version := "0.1"

scalaVersion := "2.13.3"

enablePlugins(ScalaJSPlugin)

mainClass := Some("io.tailrec.hussachai.Hussachai")

// This is an application with a main method
scalaJSUseMainModuleInitializer := true


libraryDependencies ++= Seq(
"org.lrng.binding" %%% "html" % "latest.release",
"com.lihaoyi" %%% "upickle" % "1.2.1"
)

// Enable macro annotations by setting scalac flags for Scala 2.13
scalacOptions ++= {
import Ordering.Implicits._
if (VersionNumber(scalaVersion.value).numbers >= Seq(2L, 13L)) {
Seq("-Ymacro-annotations")
} else {
Nil
}
}

As you can see, the version matrix is somewhat complex. Let alone the %%% in there. %%% is a special syntax for Scala.js for locating the right version of the library. Let me explain to you a bit about the version situation in Scala. Sbt supports %% by default so that it can pull the right Scala library that works with your Scala version. For example, you’re using Scala version 2.12 and you want to use library A version 1.2. The library A’s author, if he/she wants to, can publish the library to support Scala version 2.11, 2.12, and 2.13 which means there will be 3 artifacts of library A version 1.2. There will be a_2.11:1.2, a_2.12:1.2, and a_2.13:1.2 This is annoying because not all library authors are nice like library A’s author. Many time, you will get into the situation that there is no lib published for your Scala version.

OK, are you ready? Things can get worse! %%% is another syntax for Scala.js and Scala.js 0.6 and Scala.js 1.+ are not compatible. You can imagine how this situation’s gonna look like now. Yes , as a library author, this is a nightmare. You have to publish 3 x 2 = 6 artifacts to support Scala 2.11, 2.12, 2.13 and Scala.js 0.6 and 1.+ 😨. I’m not attacking Scala or Scala.js by any means. I love the language but this version situation is a bit ridiculous for library authors and that will reflect the experience of developers. Hopefully, TASTy can mitigate this issue before it’s getting out of hand.

Oh almost forgot, you need to define the plugin in project/plugins.sbt

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0")

There are 2 ways of calling JavaScript code.

Calling Javascript Code Through Facade Types.

It’s more like creating a Scala interface for JavaScript. The transpiler knows your intention but it cannot help you check the types here unfortunately. You’re responsible for your own action. However, you have to do this only once. Once the interfaces are correctly defined with proper signatures and types, the transpiler can help you after that.

@js.native @JSGlobalScope
object window extends js.Object {
def alert(message: String): Unit = js.native
}
// After defining that, you can do window.alert("Hello") as if it's Scala API.

This approach is good when you have to reuse the code or you want to provide the JavaScript API as Scala.js library. Check this out for more details.

Calling JavaScript Code Through js.Dynamic

This is a one off solution. You can quickly call any JavaScript API using Scala Dynamic. The transpiler will convert the code to JavaScript as is.

From the above code, you can do it through js.Dynamic

js.Dynamic.global.window.alert("Hello")

And that code will be transpiled to

window.alert("Hello")

Well.. Why Scala.js doesn’t support window.alert(“Hello”out of the box?That’s totally a valid question. It’s obvious that window object is not available in the scope and Scala compiler cannot figure out that for you. But it’s something that the transpiler can help users by auto import Scala DOM facade into the scope. Currently you have to import that library and use it as the following.

import org.scalajs.domdom.window.alert("Hello")

The Build Process

Scala.js Sbt plugin provides you 2 tasks — fullOptJS and fastOptJS.

The above diagram is for fullOptJS which you want the transpiler to transpile your Scala code into the compact JavaScript code that is fully optimized. It takes longer time than fastOptJS. You can prefix the task with ~ (tile), e.g, ~fastOptJS and SBT will transpile the code automatically when the changes are detected.

  1. We want to modify the SBT fullOptJS task to execute Gen.js (2) using NodeJS. Once the Gen.js is complete, sbt starts the build process (3).
  2. Gen.js reads the JSON file which is a resume data in JSON format and encrypts it using AES (You can select the mode you want). forge is a good library that works in both NodeJS (server-side) and Javascript (browser-side). The output will be Base64 of the encrypted data. Since AES is a symmetric key, we have to share a key with Scala.js code as well. Gen.js also generates a Scala code that has a secret key and IV (initialization vector).
  3. The Scala has to compile the Scala code as well as the generated Scala code containing a secret key into JavaScript code.

You can do step 2, using this sbt code.

lazy val genKey = taskKey[Unit]("Generate and use a new secret key")
genKey := {
import scala.sys.process._
Seq("node", "gen.js").!
}
fullOptJS in Compile := (fullOptJS in Compile).dependsOn(genKey).value

We can define the task using the taskKey and execute the NodeJS using the function ! from scala.sys.process. You can tell SBT to execute the genKey task after fullOptJS task using dependsOn function.

You don’t need to do this with fastOptJS because you can use the ones (encrypted JSON data and generated code containing a secret key) that are generated from fullOptJS.

Tip

If you don’t want to generate a sourcemap for fullOptJS, you can do so using

// disable source map generation for fullOptJS
scalaJSLinkerConfig in (Compile, fullOptJS) ~= { _.withSourceMap(false) }

How It Works

I can show you how it works using a simple sequence diagram. The web browser fetches the resources from the server (CDN). When the web browser receives the (Scala.js) JavaScript, it executes the script. The script will fetch the encrypted data from the server. The data is encoded using BASE64 encoding and it must be decoded before decrypting. Once the data is decrypted, it must be a valid JSON and we can use upickle to parse that JSON into case classes. At this step, we bind our case classes to the DOM elements and render the DOM elements along with the data. Voilà!

Here’s the snippet for decrypting the data. We use js.Dynamic here because it’s just a one off solution. We’re not going to use the forge function elsewhere.

def decrypt(base64Cipher: String): String = {
val forge = js.Dynamic.global.forge
val cipher = forge.util.createBuffer(js.Dynamic.global.utf8Btoa(base64Cipher))
val decipher = forge.cipher.createDecipher("AES-CBC", js.Dynamic.global.utf8Btoa(GenSecure.SecretKey))
decipher.start(js.Dictionary{"iv" → js.Dynamic.global.utf8Btoa(GenSecure.EncodedIV)})
decipher.update(cipher)
decipher.finish();
val hexData = decipher.output.toHex()
return js.Dynamic.global.htoa(hexData.toString).toString
}

How Secure It Is?

If we’re talking about the data encryption part, it’s definitely NOT secure at all LOL. How can I deliver the content to you publicly without password protected and expect it to be secure? It’s impossible! I can make it secure though, but I need you to sign in and verify that you’re not a bot (can go extra length for this). In that case, I can just rely on TLS and don’t bother about encryption at all because the file can be protected by a session ID or some sort. My CV is a plain static resources that can be served straight out of a CDN. A secret key is actually embedded in the JavaScript code produced by Scala.js. Hmm… So, what the heck I’m doing this for? It’s for preventing the bot from scraping my data. That’s actually my excuse. The real reason is that it’s cool and it’s very fun. Also, note that my key rotation policy is that it changes every time I update my CV as depicted in the above diagram.

The Quirks

The NodeJS function for encoding data into BASE64 and vice versa is terrific Buffer.from(data, ‘base64’).toString(‘utf8’) but that API doesn’t exist in the vanilla JavaScript and we have to use the g̶o̶o̶d̶ old atob and btoa function. The problem is that these functions do not work with UTF-8 and I spent a good amount of time just to find that out.

This Stackoverflow post saved my day, and here’s the code

function utf8Btoa(str) {
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
}

Find The Hosting Provider

I want a custom domain for my CV site and I already have a domain that I want to use. At first, I uploaded all my static files to Amazon S3 because it’s something I’m familiar with. It worked great. As you already know that the S3 resource URL is super ugly and there’s no way I’m gonna share my CV site with that URL.

https://${my-bucket-name}.s3-${aws-zone-id}.amazonaws.com/index.html

Luckily, my DNS provider supports URL Redirect with URL frame which means I can add an URL Redirect record into my DNS setting and point it to another URL which is an S3 resource URL. Note that this Redirect record is not a standard DNS record, it’s a special DNS record provided by some DNS providers. Behind the scenes, that record may point to their nginx server. The URL will be masked using a frame which means that the end user won’t see where it redirects to because the address bar remains the same. Everything seemed to work great until I found that it didn’t support HTTPS.

C’mon how can you make your site looks good without HTTPS?

Amazon S3 does not support HTTPS access to the website. If you want to use HTTPS, you can use Amazon CloudFront to serve a static website hosted on Amazon S3.
https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html

Well, I’m not going to use CloudFront and transfer my DNS to Route 53 just for this. On top of that I have to use ACM to issue the certificate, and all of these are not free.

My heart was broken and I had to look elsewhere. I found Firebase and decided that I would give it a try. I spent just an hour and most part of that was because I had to restructure my project to fit it with the firebase deployment process. I got static file hosting with built-in CDN support. The command line tool is awesome. I can deploy my site with one command. It’s super easy. Firebase doesn’t charge me a dime for all of these and they also provide a free certificate issued by Let’s Encrypt!. So I got HTTPs for free without doing anything extra on my part. Everything was already handled by Firebase. Additionally, Firebase also provides analytics for free.
I didn’t get paid for this. I was just impressed :)

If you don’t have your own domain, Firebase hosting also has a nice domain for your project and it looks nicer than AWS

https://${project-name}.web.app 

Thanks for reading!

TODO:

  • Create charts from Github activities.
  • Mobile Version (Wait.. 90s supports mobile? WAP)
  • S̵k̵i̵l̵l̵s̵ ̵c̵l̵o̵u̵d̵.̵
  • Mini-games
  • Add https://jsonresume.org/ adapter.
  • Make the guestbook work

--

--