iTranslated by AI
Building a Portfolio with a Gallery using Nuxt 3 and Tailwind CSS
About this article
This article is for day 19 of the WORDIAN Advent Calendar 2022.
I created a static portfolio with a gallery called "Sosakubutsu Shokai" (Creation Showcase) using Nuxt3 and Tailwind CSS, and deployed it to GitHub Pages with a custom domain.
In this article, I will describe that process.
About the Portfolio "Sosakubutsu Shokai"

Overview
Name: Sosakubutsu Shokai (Creation Showcase)
Repository
(Update 2023-12-16: The following is the commit before the renewal with Next.js)
Features
This portfolio has the following features and characteristics:
- Allows sharing my profile, background, interests, and skills.
- Enables publishing a gallery of works created so far (mainly illustrations).
- Supports publishing articles written in Markdown.
- Being a static website, pages load quickly.
Technical Stack
Node.js and npm versions are as follows. Installed with sudo n latest (as of 2022/12/12).
v19.2.0 (with npm 8.19.3)
package.json is as follows.
"devDependencies": {
"@nuxt/content": "^2.2.2",
"@nuxt/image-edge": "^1.0.0-27840416.dc1ed65",
"autoprefixer": "^10.4.13",
"nuxt": "3.0.0",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.4"
},
"dependencies": {
"github-markdown-css": "^5.1.0",
"vue-gtag": "^2.0.1"
}
Purpose of Creating the Portfolio
Letting people view my works and results freely
I enjoy drawing and creating things, and I frequently post my works on image-sharing SNS such as:
These are excellent services, and I am a regular user of them. However, they possess the following drawbacks (as an inevitable fate of large-scale external services that one does not create themselves):
- It's impossible to fix UIs or bugs oneself; one can only wait for an update.
- Since they handle many users and data, loading times for sites and images can sometimes be long.
- There is a risk that the companies or organizations managing the SNS might suddenly terminate the service or change their operating policies, hindering its use.
- There are advertisements that users are not interested in (krita-artists.org has no ads).
By launching a website on my own domain and publishing works and articles there, I can solve the problems mentioned above. I can decide everything myself, from the UI to the content I handle.
Using it for Job Hunting
By publishing results and their creation processes (though few for now) through the gallery and articles, I want to demonstrate my technical skills and passion to companies.
Furthermore, I (want to) believe that this portfolio itself serves as a tangible result showing a baseline level of enthusiasm for web technology.
Improving Technical Skills
I am not very well-versed in information technology and am still learning JavaScript and its surrounding frameworks. Therefore, I wanted to deepen my knowledge of web engineering through the process of creating visible results.
Since I managed to build the website while still having limited knowledge, there might be some poor implementation or parts where I'm not fully utilizing the strengths of modules and frameworks. I intend to improve these points through continuous development in the future.
About Nuxt3 and Tailwind CSS
Nuxt3
Nuxt.js is a web application framework based on Vue.js. Nuxt.js features automatic routing and data management, and makes meta tag management easy, allowing for more efficient development in various aspects compared to Vue.js alone.
Additionally, it supports a large number of useful modules, allowing various functions to be implemented easily.
It is also worth mentioning that it supports SSR, which was not possible with Vue.js alone. However, since "Sosakubutsu Shokai" does not need to display content dynamically, I am using SSG for page generation.
Some of the changes from Nuxt2 (partial) include:
- Faster static page generation and rendering.
- Default support for TypeScript.
- Automatic importing of plugin files placed in
~/plugins. - The directory name
/statichas been changed to/public.
npx nuxi init <project-name>
can be used for installation.
Tailwind CSS
Tailwind CSS is a type of CSS framework where styles are described by adding pre-defined classes to elements. It has advantages such as high customizability, being lightweight, and allowing for easy styling including animations and responsive design.
On the other hand, it has the disadvantage that the code can become cluttered because it takes a form close to inline description. However, by using frameworks like Vue.js or React, this can be managed in component units, mitigating this drawback to some extent.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
can be used for installation.
Development Points
From here, I will describe the key points of the implementation while creating the portfolio.
Design
For the design, I focused on the following aspects:
- Simplicity
- Avoiding a "cheap" impression
- Support for responsive design
- Ensuring information and site structure are easy to read
- Using a near-monotone color scheme (to prevent color clashes with various artworks)
Additionally, since UI frameworks with pre-made components often didn't quite match the design I wanted (which is also why I chose Tailwind CSS), I designed the UI myself, referring to other websites.
Design Highlights
- The header has a blurred, translucent background like frosted glass.
- A gallery based on square tiles, referencing Instagram and Pixiv.
Implementation of Work Pages and Gallery
The pages for works and outcomes, along with the gallery that neatly lists their thumbnails and links, are the most crucial parts of this portfolio. However, to implement a gallery that handles many images and operates efficiently, I needed to solve two challenges:
- Automatically generating thumbnail lists and pages for each work.
- Automatically optimizing images for different purposes (thumbnails, eye-catches, etc.) without having to prepare multiple versions of each image manually.
Automatically Generating Thumbnail Lists and Pages
Thumbnail List
In this portfolio, work images and their associated data are managed via a JSON file. For example:
[
{
"id": "zugadanten2022",
"image": "/images/artworks/zugadanten2022.jpg",
"title": "図画団展2022ポスター",
"tool": "Krita",
"href": "https://www.instagram.com/p/CkSHtqIBGFx/",
"caption": "2022年度図画団展のポスター",
"aspect_h": 1.414,
"aspect_w": 1
},
{
"id": "rainbow_dragon",
"image": "/images/artworks/rainbow_dragon.png",
"title": "虹龍",
"tool": "Krita",
"href": "https://www.instagram.com/p/Cb1wksDvX-i/",
"caption": "極彩色の龍",
"aspect_h": 9,
"aspect_w": 16
},
// continued
]
By loading this and iterating with v-for, I can create a gallery listing all work thumbnails.
<template>
<div class="card card-shadow bg-white m-3 p-3">
<h2 class="text-center mb-3">Gallery</h2>
<div
class="aspect-square overflow-y-auto over-contain card"
style="font-size: 0"
>
<div
v-for="(artwork, i) in artworks"
:key="i"
class="w-1/3 inline-block mb-0 border border-white"
>
<nuxt-link :to="'/artworks/' + artwork.id" :title="artwork.title">
<nuxt-img
:alt="artwork.title"
:src="artwork.image"
provider="ipx_fixed"
width="240"
height="240"
quality="25"
loading="lazy"
class="object-cover aspect-square w-full"
/>
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
import artworks from "@/assets/json/artworks.json";
export default {
data() {
return {
artworks: artworks,
};
},
};
</script>
Page Generation
Additionally, I use the dynamic routing feature for automatic page generation for the works. I referred to the following:
First, for example, if you want to automatically create pages within the /artworks directory:
-| pages/
---| index.vue
---| artworks/
-----| [id].vue
The string used for id can be anything, but make it descriptive as it will be referenced later.
Next, load the JSON file mentioned earlier using JSON.parse(JSON.stringify()), and ensure that only work pages satisfying artwork.id === this.$route.params.id; are generated.
(Here, this.$route.params.id represents the filename of [id].vue)
For reference, the entire [id].vue is as follows:
I think this is quite a "brute force" method, so I might revise this approach in the future.
Image Optimization
Image optimization was implemented using "Nuxt Image".
npm install @nuxt/image-edge
It can be installed with the above command.
The <nuxt-img> tag is a replacement for the conventional <img> tag, and it allows for various forms of image optimization through multiple options. For example, you can write it like this:
<nuxt-img>
src="/title.png"
width="720"
height="450"
</nuxt-img>
In this case, after the image size is set as specified in width and height, an image that is scaled and cropped to fit those dimensions is generated. In other words, by providing a single high-resolution image, nuxt-img can automatically generate images of any size and resolution you desire. When implementing the gallery, this significantly improved development efficiency as I no longer needed to manually prepare cropped or compressed images for thumbnails, for instance.
There are various other options available, such as quality for reducing file size, loading for enabling lazy loading, and format for converting image formats.
Generating Articles from Markdown
When writing relatively long articles for a personal website, it is laborious to write the text and layout using HTML and CSS. If possible, I want to write in Markdown, which is easier to write and automatically handles the layout. The Nuxt Content package fulfills this need.
npm install --save-dev @nuxt/content
It can be installed with the above. Also, since this alone doesn't provide styling, I'll install something like GitHub-style CSS as well.
npm install github-markdown-css
The Markdown files themselves are placed under /content. For example, the file structure would look like this:
-| pages/
---| about.vue
---| articles/
-----| [...slug].vue
-----| index.vue
-| contents/
---| about.md
---| articles/
-----| hello.md
-----| goodbye.md
Note that for some reason, there were times when things didn't work properly when directory names were capitalized, so I think it's better to name directories and files in lowercase.
Now, let's try actually loading a Markdown file within a Vue file.
For example, to associate about.vue with about.md, you can write:
<template>
<div class="card card-shadow p-6 m-3">
<ContentDoc path="/about" />
</div>
</template>
Here, the corresponding Markdown file at the specified path is rendered using the <ContentDoc /> component.
Automatic Deployment with GitHub Pages
I use GitHub Actions. The workflow is as follows:
It generates files under ./dist to the gh-pages branch and handles the deployment on that branch.
Reflections, Challenges, and Future Outlook
Reflections
Thanks to lightweight and easy-to-use frameworks like Nuxt.js and Tailwind CSS, along with excellent modules, I was able to implement a portfolio with multiple features, such as automatic generation of work pages and galleries and article creation using Markdown. Since then, simply adding new images or articles as needed automatically generates the corresponding pages, making maintenance relatively easy.
Also, perhaps thanks to image optimization, despite using many images in the gallery and elsewhere, the PageSpeed Insights scores were good. As shown below, it exceeds 90 points even on mobile, so visitors should not lose patience due to slow loading times.


Challenges and Future Outlook
I realized that not having a solid understanding of TypeScript, and by extension JavaScript, made it difficult to fully understand Nuxt's features. Therefore, I want to start by studying these fundamentals properly.
Based on that, I plan to review the parts I implemented somewhat crudely (such as managing dynamic routing with JSON and my understanding of Nuxt Content).
Additionally, I would like to implement features such as:
- Embedding external links as blog cards within articles.
- Clicking on an image on a work page to expand it to full screen (there seems to be a module that can do this easily).
- Dark mode.
- A button to share works on Twitter.
- Organizing articles and works with tags.
Appendix
Regarding the Nuxt Image Bug
As mentioned in the image optimization section, the nuxt-img tag has a bug where SSG cannot be performed under certain conditions when multiple options are used. The cause is likely the inclusion of commas (,) in the URL.
For instance, consider an image optimized as follows:
<nuxt-img
:alt="artwork.title"
:src="artwork.image"
width="240"
height="240"
quality="25"
loading="lazy"
class="object-cover aspect-square w-full"
/>
When this image is optimized, the resulting image link looks like this (this works fine in development, but fails when generating static files because the link breaks):
https://omemoji.com/_ipx/q_25,s_240x240/images/artworks/zugadanten2022.jpg
With the image above, there's no issue if only width (w) and height (h) are set, but the bug occurs when the quality (q) option is added. Therefore, it's inferred that separating multiple options with commas is the "certain condition" causing the issue.
Given this, the problem should be solvable by separating options with a character other than a comma. Since the behavior of image optimization options is determined by the provider, you can create a custom provider by placing a modified version of the default ipx provider in the /providers/custom directory. The modified ipx_fixed.ts is as follows:
This is almost the same as the default ipx configuration file, except that & is used in the joinWith part instead of ,. I was able to resolve the issue by specifying this ipx_fixed as the image provider.
Note that the PR mentioned earlier suggested using _ as the delimiter, but that didn't work in my environment.
Discussion