Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Create a Hugo Theme from Scratch - III
Xiaoliang Wang
Xiaoliang Wang

Posted on • Originally published attomo.dev

Create a Hugo Theme from Scratch - III

This is a blog series that documents my journey of building a Hugo themehttps://github.com/tomowang/hugo-theme-tailwind from scratch. The entire series consists of four articles, and this is the third:

Dark Mode

tailwindcss makes handling dark mode very convenient. Based on the original light theme, we only need to set the display style under the dark theme through thedark: prefix.

Add thedarkMode: 'class' configuration intailwind.config.js, and then we can modify the styles in the theme.
For details on dark mode, please refer to the official documentation:https://tailwindcss.com/docs/dark-mode

It's worth mentioning the dark mode toggle button. To keep the theme as simple as possible and avoid referencing additional front-end frameworks,
I researched some common theme button toggle examples intailwindcss and found that usinginput + peer + group could achieve the desired effect very well.
The code is as follows:

<divclass="darkmode-toggle flex flex-none ml-2"><labelclass="flex items-center px-3 cursor-pointer rounded-full bg-gray-100 dark:bg-gray-600"><inputtype="checkbox"class="sr-only peer"><divclass="group flex flex-row gap-1 justify-center h-8 px-1 rounded-full bg-white dark:bg-gray-700"><iclass="h-6 w-6 flex-none rounded-full bg-yellow-400 place-self-center peer-checked:group-[]:invisible">        {{ partial "icon" "brightness-down" }}</i><iclass="h-6 w-6 flex-none rounded-full place-self-center invisible peer-checked:group-[]:visible">        {{ partial "icon" "moon-stars" }}</i></div></label></div>
Enter fullscreen modeExit fullscreen mode

I use<input type="checkbox"> to control the theme variable, and then define a group of icons using<div>. Each icon usespeer-checked:group-[]:invisible andpeer-checked:group-[]:visible to control visibility and hiding.
This way, I only need to pay attention to the value of thecheckbox, while the icon switching of the button is completely handled bytailwindcss.

constthemeToggle=document.querySelector('.darkmode-toggle input');themeToggle.addEventListener('change',function(){if(this.checked){localStorage.theme=dark;document.documentElement.classList.add(dark);}else{localStorage.theme=light;document.documentElement.classList.remove(dark);}});
Enter fullscreen modeExit fullscreen mode

Regardinggroup andpeer,tailwindcss describes them as:

The dark theme implementation took me a day of spare time, and the final code is at#75c6b41.

Responsive Design

tailwindcss also makes responsive design easy to use. My goal is to support both mobile and PC. When designing with mobile-first in mind,
tailwindcss uses prefix-less components to design mobile styles first, and then uses prefixed components (e.g.,md:) to override styles at other screen sizes. My modifications are as follows:

index ad2e93f..e5b7d76 100644--- a/layouts/_default/list.html+++ b/layouts/_default/list.html@@ -15,9 +15,9 @@   <div>     <div>-      <section>+      <section>         {{ range (.Paginate $pages).Pages }}-          <article>+          <article>             <h2>               <a href="{{ .RelPermalink }}">{{ .Title | markdownify }}</a>             </h2>@@ -40,7 +40,7 @@           </article>         {{ end }}       </section>-      <aside>+      <aside>         {{ partial "sidebar.html" . }}       </aside>     </div>
Enter fullscreen modeExit fullscreen mode

As the code shows, I changed thesection tag tow-full by default and then usedmd:w-2/3 to display 2/3 width on larger screens.

The final code is at#279de84.

Multilingual Support

Hugo natively supports multiple languages. First, we need to introduce configuration for multiple languages in the configuration file. For example, our example site has the configuration fileexampleSite/config/_default/languages.toml, with the content:

[en]languageCode='en-US'languageDirection='ltr'languageName='English'weight=1[en.menu][[en.menu.main]]identifier="post"name="Post"pageRef="post"weight=10[[en.menu.main]]identifier="about"name="About"pageRef="about"weight=20[zh-cn]languageCode='zh-CN'languageDirection='ltr'languageName='中文'weight=2[zh-cn.menu][[zh-cn.menu.main]]identifier="post"name="文章"pageRef="post"weight=10[[zh-cn.menu.main]]identifier="about"name="关于"pageRef="about"weight=20
Enter fullscreen modeExit fullscreen mode

Then, we need to organize and place the previously hardcoded strings in the theme into thei18n folder and modify the strings in the theme using thei18n function for replacement.
For example, part of the strings in thelayouts/404.html file:

diff --git a/layouts/404.html b/layouts/404.htmlindex 77d8885..32ec853 100644--- a/layouts/404.html+++ b/layouts/404.html@@ -2,11 +2,11 @@   <main>     <div>       <p>404</p>-      <h1>Page not found</h1>-      <p>Sorry, we couldn't find the page you're looking for.</p>+      <h1>{{ T "404.page_not_found" }}</h1>+      <p>{{ T "404.sorry" }}</p>       <div>         <a href="/">-          Go back home+          {{ T "404.go_back_home" }}         </a>       </div>     </div>
Enter fullscreen modeExit fullscreen mode

Finally, adjust the navigation bar to add a language switch button and a language list.
hugo multilingual language switch

For the complete code, please refer to#0244464.

Additional Benefits

My native language is Chinese, and I can read English quite well, so I initially only implemented Chinese and English.
But how can I add more languages to the theme? 2023 was a year of significant growth for large language models,
and with the help of LLMs, I translated the language files into more languages like Russian and Japanese using prompts.

Here, I used thegemini interface to process thei18n files:

importgoogle.generativeaiasgenai# get key from https://makersuite.google.com/app/apikeyGOOGLE_API_KEY='YOUR_GOOGLE_API_KEY'genai.configure(api_key=GOOGLE_API_KEY)model=genai.GenerativeModel('gemini-pro')prompt_template='''You are a translator and your task is to translate l10n file to different languages.The l10n file is provided in TOML format. The file contains {{ KEY }} for variables and use`one` for singular and `other` for plural.The TOML file is quoted in triple backtick. Please translate the content to {lang}and keep the original content structure, also remove triple backtick in output:~~~toml{en_file_content}~~~'''en_file_content=''withopen('./en.toml','r')asf:en_file_content=f.read()prompt=prompt_template.format(lang='Chinese',en_file_content=en_file_content)response=model.generate_content(prompt)print(response.text)
Enter fullscreen modeExit fullscreen mode

Note the~~~ in code is used for rendering, since Markdown here in dev.to does not support nested code block.

In the prompt, I included some hugo and code-related instructions, like the file format isTOML, using{{ KEY }} for variables,
and handling singular and plural withonefor singular andotherfor plural.
I also used instructions to ensure the LLM output retained the original format. I tested with Chinese, which I'm familiar with, and the above code output the following:

table_of_contents="目录"open_main_menu="打开主菜单"open_lang_switcher="打开语言切换器"[reading_time]one="阅读时长一分钟"other="阅读时长{{ .Count }}分钟"[header]darkmode_toggle="切换深色模式"[404]go_back_home="返回主页"sorry="抱歉,找不到您要查找的页面。"page_not_found="页面未找到"[footer]powered_by="由{{ .Generator }} {{ .Heart }} {{ .Theme }}提供支持"copyright_with_since="{{ .CopyrightSign }} {{ .SinceYear }} - {{ .CurrentYear}} {{ .Copyright }}"copyright_wo_since="{{ .CopyrightSign }} {{ .CurrentYear}} {{ .Copyright }}"[paginator]newer_posts="较新的文章"older_posts="较早的文章"[taxonomies]categories="分类"tags="标签"series="系列"
Enter fullscreen modeExit fullscreen mode

The final result was quite good. The file structure was correct, and the variables were preserved. I used a similar method to generate localization files for other languages. Of course, I don't actually speak Russian, Japanese, etc., so I couldn't accurately judge the quality of the translations.

Others

Shortcodes

Hugo providesshortcodes to extendmarkdown rendering capabilities.shortcodes are reusable code snippets that can make editingmarkdown more efficient. Hugo has some built-in commonshortcodes, likefigure,gist, etc. You can refer tohttps://gohugo.io/content-management/shortcodes/.

Some of my blogs useasciinema to record terminal interactions, and I useshortcodes to conveniently embedasciinema.
Hugo searches forshortcodes code in a specific order, with the file name being the final name used inmarkdown:

  1. /layouts/shortcodes/<SHORTCODE>.html
  2. /themes/<THEME>/layouts/shortcodes/<SHORTCODE>.html

I created thelayouts/shortcodes/asciinema.html file and referenced the official embedding documentation:
https://docs.asciinema.org/manual/server/embedding/#inline-player. I wrote the following content in the html:

{{$id:=.Get0}}<scriptasyncid="asciicast-{{ $id }}"src="https://asciinema.org/a/{{ $id }}.js"></script>
Enter fullscreen modeExit fullscreen mode

where.Get 0 means getting the first parameter. Of course, you can also use named parameters. The usage is:

{{</* asciinema 239367 */>}}
Enter fullscreen modeExit fullscreen mode

The effect is as follows:

{{< asciinema 239367 >}}

Render Hooks

Hugo usesgoldmark to rendermarkdown, andrender hooks allow developers to override the rendering of specific components. Currently supported are:

  • image
  • link
  • heading
  • codeblock

Simply, we can add additional attributes to thea tag rendered by the link to ensure security. For example, add the file
layouts/_default/_markup/render-link.html with the content:

<ahref="{{ .Destination | safeURL }}"{{with.Title}}title="{{ . }}"{{end}}{{ifstrings.HasPrefix.Destination"http"}}target="_blank"rel="noopener"{{end}}>{{.Text|safeHTML}}</a>
Enter fullscreen modeExit fullscreen mode

This code adds therel="noopener" attribute when rendering links to external sites.

Of course, more complex operations can be added to the rendering of images, such as lazy loading, scaling images to fit different resolutions,
and converting images to compressed formats. You can refer to the processing inlayouts/_default/_markup/render-image.html.

Code Block Syntax Highlighting and Copying

Hugo useschroma for code highlighting. It works well with the default configuration, but I want to go further,
such as having the code block theme switch along with the light and dark modes, and providing code block copying functionality.

To support custom styles, we need to adjust the code highlighting configuration in theconfig/_default/hugo.toml file to use CSS classes to apply highlighting styles:

[markup.highlight]noClasses=false
Enter fullscreen modeExit fullscreen mode

Then use the command to generate code highlighting styles for our light and dark themes:

hugo gen chromastyles--style=solarized-lighthugo gen chromastyles--style=solarized-dark
Enter fullscreen modeExit fullscreen mode

Write the styles into the main style file. For the dark theme, you can use the column editing mode of the IDE to add.dark before each line of style to restrict it.
The effects under light and dark themes are as follows:
hugo syntex highlight light
hugo syntex highlight dark

However, in some cases,chroma's default CSS styles may conflict withtailwindcss'stypography plugin. For example, when the line number configuration is enabled, you will find that there is a large gap between the code line numbers and the code block:
hugo syntex highlight lineno error

This is becausechroma uses table elements when rendering line numbers, whiletypography adds additional margins topre elements.
In this case, we need to find the conflict points and then override some styles. The following CSS styles will remove the margins and border-radius of thepre elements in code blocks with line numbers and change thedisplay property of the table:

/* fix highlight with line number */.highlight.chroma.lntable{margin:1.7142857em0;padding:0.8571429em1.1428571em;border-radius:0.375rem;overflow:hidden;}.highlight.chroma.lntablepre{margin:0;padding:0.8571429em0;border-radius:0;}/* https://caniuse.com/css-has */.highlight.chroma:has(table){background-color:transparent;}.highlight.lntabletr{display:flex;}.highlight.lntabletr.lntd:last-child{flex-grow:1;}
Enter fullscreen modeExit fullscreen mode

Code block copying is achieved by checking if there arepre elements on the page, then finding the.highlight elements on the page, and adding corresponding copy buttons.
I used the statement{{- if (findRE "<pre" .Content 1) }} inbaseof.html to search for elements,
which reduces JavaScript loading. Additionally, note that code blocks with line numbers will render twocode elements, and we need to take the last one.
Relevant code can be found at#4d62bba
and#df6ac6d.

Pipes

When allowing users to modify the theme, we need to keep the originalCSS andJavaScript code.
But for general direct users, we need to shield these code compilation logic while ensuring that the final generated static resources are compressed.
Here we can use thepipes feature provided by Hugo.
Fortailwindcss, I initially placed the original CSS style inassets/css/main.css, the compiled code in the static folderstatic/main.css, and directly referenced the static file inhead.html:

<!-- Theme CSS (/static/main.css) --><linkrel="stylesheet"href="{{ "main.css"|relURL}}"/>
Enter fullscreen modeExit fullscreen mode

I wanted to display the original CSS style during development but use the compressed version with a hashed filename when deployed.
Ultimately, I adjusted the location of theCSS files, placing both the pre-compiled and post-compiledCSS code inassets/css
(so that Hugo can process the resources), and adjusted the inclusion ofCSS files:

{{-$styles:=resources.Get"css/index.css"-}}{{-$env:=getenv"HUGO_THEME_DEVELOPMENT"-}}{{-ifeq$env"true"}}{{-$styles=resources.Get"css/main.css"|postCSS(dict"config""./postcss.config.js")-}}{{end-}}{{-ifhugo.IsDevelopment}}{{-$styles=$styles|resources.ExecuteAsTemplate(printf"css/main.dev.%v.css"now.UnixMilli).-}}{{else}}{{-$styles=$styles|minify|fingerprint-}}{{end-}}<linkrel="stylesheet"href="{{ $styles.RelPermalink }}">
Enter fullscreen modeExit fullscreen mode

Here, I useHUGO_THEME_DEVELOPMENT to identify the theme development mode, and process CSS styles usingPostCSS in theme development mode.
When using thehugo server command, Hugo is in development mode,hugo.IsDevelopment istrue,
and filenames with timestamps are used in development mode to prevent caching. In production mode,| minify | fingerprint is applied for compression and file caching.

Regarding the environments corresponding to different Hugo commands, you can refer to the following table:

CommandEnvironment
hugoproduction
hugo --environment stagingstaging
hugo serverdevelopment
hugo server --environment stagingstaging

Similarly, we can handleJavaScript files in a similar way, such as for code block copying:

{{-$jsCopy:=resources.Get"js/code-copy.js"|js.Build"code-copy.js"-}}{{-ifnothugo.IsDevelopment}}{{-$jsCopy=$jsCopy|minify|fingerprint-}}{{end-}}<scriptsrc="{{ $jsCopy.RelPermalink }}"></script>
Enter fullscreen modeExit fullscreen mode

Lighthouse

I've completed most of the code writing, but I still need to check if the pages rendered by my theme conform to some standards and best practices.
I used the built-inlighthouse developer tool inchrome to check the site.

It can be seen that the performance score of the site is good, but there is room for improvement in accessibility and SEO. For example, the contrast of some colors in the theme, thealt attribute of images, etc.
hugo theme lighthouse original report

Based on lighthouse's suggestions, I made adjustments and optimizations for each suggestion, and finally, the lighthouse score reached a high level:
hugo theme lighthouse report after modification

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

🚀 Engineer fluent in Python, Golang, JavaScript, Java. Currently mastering Tailwind CSS. Passionate problem-solver and perpetual learner. 🌐
  • Location
    Nanjing, China
  • Education
    Nanjing Univertisy
  • Joined

More fromXiaoliang Wang

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp