June 28, 2025
So in part 1 of this series of articles, I wrote a little bit about my early adventures with building this blog, particularly with exploring the Vaadin Java framework and why I ultimately decided it wasn't a great fit for what I wanted to build. I briefly looked at a couple other frameworks that aren't really worth writing about here, but ended up settling on doing a build with Next.js.
As of the time of writing this article, I know I'm a bit late to the party with Next.js with how prevalent it's used now, but I'll do a bit of a dive into it anyway. For those out of the loop or more new to software development, Next.js is a React framework for web apps that follows a trend in the emergence of serverside-focused, monolithic architectures (a departure from the previous trend of apps with heavy clientside code making many computations and service calls in browser). In are the days of having servers do the heavylifting so web browsers don't have to!
Next.js itself focuses on server-side rendering, where the dynamic elements of a page are computed on the server, rendering in a resulting static HTML page which is then sent to the client browser on request. This is fantastic for something like a blog site, where I would want to fetch content to build article pages, but don't want a dynamic, script-heavy resulting webpage that would be difficult to index and crawl by search engines (resulting in being penalized in search engine rankings). This would also allow me to simply fetch the article content on the server and just serve a finished article page to the user, instead of each user's browser having to fetch the article content (something that would result in tons of network calls if I have a lot of users, even if caching is used).
The serverside rendering of Next.js is a pretty powerful feature, however it does introduce an extra dimension of possible errors and problems. Components that are prerendered serverside don't have access to certain features like references to the user's browser (things like window
, document
, or localstorage
) since those components are rendered on the application server. There's also the possibility of hydration errors, where a component's HTML can be rendered to look one way on the server, but then change when displayed in the user's browser. This does add an extra layer of complexity and was a bit of a learning curve to navigate around.
Hydration errors can arise from situations you wouldn't expect. One of the more frustrating ones I encountered occurred when I was building the blog post component of the site. For some context, the blog site uses a library called react-markdown
to generate HTML rendered blog posts from Markdown text. Now react-markdown
offers an override object to override various components rendered from Markdown (like lists, images, code snippets, etc.). I wanted to override the image (img
) Markdown component to display a styled Mantine UI Box
component containing a component for the image. While this should be a pretty standard thing to do normally, upon page load I was greeted with an error message reading In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error.
. For some context, nesting <div>
elements instead <p>
elements is considered invalid HTML structure. Because of this, if a browser receives an HTML document to parse and render that contains elements nested like this, it will automatically take measures to correct the document into something with valid HTML structure. However, this corrected HTML document will no longer match the original HTML on the server, causing a hydration mismatch (which would cause potential problems with the web app if not checked for).
In my case, it turns out this error was caused by the underlying structures of the Mantine UI Box
component and the ReactMarkdown
component. React Markdown rendered the Markdown content within a <p>
element, meanwhile the MantineUI Box
element used underlying <div>
elements, causing invalid HTML structure when React Markdown rendered Markdown documents that contain images. It's not good practice, but browsers normally would just correct this bad structuring for us and it would still technically work. However, since the page is rendered serverside using Next.js, when the browser corrected that structure, it produced a hydration error.
When it comes to avoiding these hydration errors with Next.js, I found there's a few tips you can follow, based on what I learned, to reduce the risk of encountering them:
react-markdown
. While this was an issue with the functionality of the package itself (see tip #1), you should take care that you don't introduce anything yourself that can cause invalid HTML structure. Then again, that's also just good practice to begin with!In the case of my hydration error issue with the ReactMarkdown
component, there is a handy plugin called rehype-unwrap-images that will, during react-markdown
's Markdown-to-HTML transformation process, go through and remove all <p>
tags enclosing images so the resulting HTML will be of valid structure. Seems I'm not the first person to run into this issue if someone went through the trouble to make a plugin for react-markdown
to solve it.
Speaking of react-markdown
, I should probably give some background on it. As I mentioned earlier and in the previous article, my idea for the blog website was to write my blog articles in Markdown format and then have the web app parse the Markdown files and generate HTML blog pages from them. Markdown is a pretty easy format to work with and works very well for creating stylized and structured documents much like how HTML can be used to create structured webpages with all of the various content we're familiar with. It seemed only natural to want to translate Markdown into HTML. Fortunately, the react-markdown
package does exactly that!
Under the hood, react-markdown
is actually a React component that uses the Unified ecosystem of plugins, including the Remark ecosystem and Rehype ecosystem and a critical component being the remark-rehype plugin, to transform Markdown content into HTML. As such, react-markdown
can utilize the ecosystems of both Remark and Rehype plugins as additional plugins, allowing me to use that handy plugin I mentioned earlier to fix the images. To get the bread and butter HTML of the blog article pages was as simple as:
<ReactMarkdown components={components} rehypePlugins={[rehypeUnwrapImages]}>{/* Markdown text here. */}</ReactMarkdown>
Here, components
is a Javascript object containing all of my customizations for each kind of HTML element that can be generated from my Markdown text.
Now that I had a working implementation for converting Markdown to HTML for blog articles, I needed a place to actually store my Markdown articles. While the initial plan was to upload actual Markdown files to a cloud-based file storage, I found it to be easier and more cost-effective to just store Markdown text entries in a database instead. For this, I wanted a cloud-based database that was easy to use, easy to set up, and offered a generous free-tier since I'm keeping this project low-budget, as in "Unless this gets real popular, I'm trying not to spend money on anything other than my domain" kind of low-budget. My eye landed on Google Firestore since it seemed pretty promising here.
Google Firestore is document-based NoSQL database. That means the structure of the database is based around the concept of "documents", which are basically objects with an ID and a collection of fields that can be anything you want them to be. Documents are organized into "collections" and documents themselves can even contain their own subcollections. Documents in the same collection don't even have to have the same fields! It's very flexible compared to the rigidness of traditional SQL databases. For something simple, like storing my Markdown blog articles, this was plenty sufficient though.
For Firestore, there's two separate packages that can be used to access Firestore databases from a React or Next.js app: firebase
, which is meant for running clientside or in your browser, and firebase-admin
, which is meant for running serverside in the backend (in fact, it can ONLY be run on the backend due to relying on Node.js APIs and admin privledges). I didn't want to run any Firestore queries on the client, since that would defeat the whole purpose of using Next.js's serverside rendering to render the blog articles (complete with article content) on the server before sending them to users' browsers. Considering that, I went with firebase-admin
.
The API for using firebase-admin
to retrieve blog articles from my Firestore database was pretty straightforward, however I did have to get creative a bit with credentials to login to Firestore. It's usually pretty common practice to store sensitive info that your application relies on, like passwords, API keys, etc., in environment variables in whatever environment you deploy your application to. This is a pretty reliable way to store sensitive info your application needs since environment variables are pretty isolated to your application environment. If someone can access the environment variables of the environment your application is deployed to, you have bigger problems!
Security for firebase-admin
however is managed through a service account, the credentials to which are stored in a JSON file that you download. I could just keep this file with the rest of my codebase, which would NOT be secure, or somehow retrieve it from elsewhere. However, I found a much easier solution by converting the contents of the JSON file to a string and, well... saving it as an environment variable! From there, it was just a matter of reading that environment variable and parsing that string back into a JSON inside my blog application. The code looks something like this:
const serviceAccount = JSON.parse(process.env.MY_FIRESTORE_JSON_FILE_AS_A_STRING as string); initializeApp({ credential: cert(serviceAccount) });
That's all there was to it!
Finally, I needed a place to actually deploy this blog application to. Again, I wanted to keep costs down, so I was on the lookout for a platform with a generous free tier. I also wanted something that required minimal configuration and time investment to get up and running, considering this is a solo project I'm working on during my rather limited free time. I ended up going with Vercel as my platform of choice, given its ease of use, reasonable free-tier options for hobby projects, and the fact that it's optimized for hosting Next.js apps in particular (Vercel are the maintainers of the Next.js framework actually, so this makes sense.).
The deployment pipeline and process was very easy to setup. Given my app was a Next.js app, it was just a matter of creating a new project in Vercel and pasting in the link to my blog app's repo on Github. After setting up the required permissions in Github and Vercel, it was able to automatically clone my repo's master
branch and deploy it. To get Firestore working, there was a section under the project settings to add my environment variables (You can specify which environment the environment variables apply to if needed.). There, I was able to create a new environment variable with the same name as the variable used to store my "stringified" Firestore service account JSON file and paste the content of the JSON as the value there.
Now that the environment variables were set, I was able to see that it was successfully pulling and displaying a few test blog articles on my deployed blog site! From there, it was just a matter of pointing my new domain to the Vercel-deployed app. There's a Domains section under the settings where I could add my domain. I don't have any custom DNS setup for the domain I reserved, so just switching the nameservers to Vercel's nameservers on my domain registrar's site was plenty sufficient.
This was a fun project to work on and has definitely been something long overdue for me personally. These days, there's so many different tools and technologies out there for development, all with their own sets of pros and cons, that half the battle is just knowing and figuring out what to build your project with, what the best tools for the job are, and what technologies play nice with each other and which are more trouble than they're worth. There's technologies that are good at some things and not others, and tech that could be good for the task at hand, but just isn't mature enough yet. Personally, this is why I try to keep fresh with a multitude of different tools and technologies and push myself out of my comfort zone from time to time.
Anyway, if you made it this far, thanks for taking the time to read! As I work on other projects in the future, I'll post about them here too, so watch this space!