iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧩

[HubSpot] Hacks for Creating the Ideal Code Block 🧩

に公開

[HubSpot] Collection of hacks for creating the ideal code block 🧩

2025-04-22 · Reina

If you want to display code blocks in HubSpot custom modules, the one distributed on GitHub is the standard choice.

(👇 I also introduce it here)

https://zenn.dev/raynya/articles/hubspot-codeblock

But... it's just a bit lacking! (`・ω・´)

"The appearance is slightly off," "the behavior is a bit wonky," and so on—as you use it, these points start to bother you.

So this time, I'm going to introduce 3 custom hacks I performed to create Reina's ideal code block! 🛠️💖

ChatGPT Image 2025-04-22 22_37_28

🧠 A quick explanation of how the module works

This code block module is structured as a HubSpot custom module in the following 3 steps:

  1. Display the fields (language, code, label, etc.) configured in fields.json
  2. Define the structure of each code block and the label/button display in module.html
  3. Implement functionality through a Prism.js + custom JS + CSS trinity!


🔍 What is Prism.js?

The part that supports the highlighting of this module is Prism.js!
It's a lightweight & simple library for easily applying syntax highlighting.

  • Just add a class like <code class="language-js"> to the HTML
  • The atmosphere is easy to customize with CSS themes
  • However! Strong styles are applied to pre and code, so overrides are mandatory 💥

No special initialization is required, and you can even apply batch highlighting with Prism.highlightAll().

🧼 What are escape × escape_jinjava?

In HubSpot CMS, if HubL syntax like {{ my_variable }} exists directly in the HTML, it will be misunderstood as "This is a HubSpot template variable!" 💬.

To prevent this, a two-step escape is required:

  • Use | escape to treat HTML tags etc. as characters
  • Use | escape_jinjava to treat HubL curly braces as characters
<code>{{ module.code_snippet | escape | escape_jinjava }}</code>

With this, even template-like descriptions such as {{ my_variable }} can be safely displayed as code as they are!


🔧 Hack #1: Being at the mercy of Prism's behavior

ChatGPT Image 2025-04-22 22_36_27

While Prism.js makes it super easy to introduce line numbers and syntax highlighting, it also brings along the problem of overly strong styles and unwanted margins.

In particular, these two were troublesome:

  • margin-top gets added to pre[class*=language-]
  • font-size gets overwritten with 1rem (!)
pre,
pre[class*="language-"] {
  margin: 0 !important;
  font-size: 13px !important;
}

I fought back with the above, but the misalignment between line numbers and text was also a subtle pain...

Ultimately, I gave up and decided on "No line numbers!"

It was heartbreaking, but it's much cleaner now.

Another thing that bothered me was the strange blank space appearing on the first line of code!

Looking closely, it was caused by extra indentation (spaces) inside the code tag.

<pre>
  <code> ← This blank space gets output!
    {{ module.code_snippet }}
  </code>
</pre>

The solution is simple: close the <code> tag on the same line 💡

<pre><code>{{ module.code_snippet }}</code></pre>

This resolved the subtle bug where "only the first line is somehow misaligned 😭."

📎 Hack #2: Make copy/accordion cute and reliable

Prism doesn't include a copy function, so I made it myself. However, points like these were unexpectedly difficult:

  • Placement of the copy button (it got cut off when placed in the top right 😭)
  • Reaction after copying (changing to ✅ Copied)
  • Balance between the cuteness of the button and the color scheme

I added a @keyframes poko-scale animation that pops out a bit using CSS, aiming for a cute look that works properly 💪.

Furthermore, this time, added with the ambition of wanting to "make the code block itself collapsible!" I refactored the structure:

  • Built the accordion function into .code-body
  • Made it so clicking .label-bar toggles the open/closed state
  • Added a triangle mark ( / ) so the clickability is communicated

Code blocks sometimes have the "getting too long" problem, so this accordion feature has become quite convenient and makes everything look much cleaner 🎀.

🎨 Hack #3: Visual customization (a soft, unified look)

Since I like dark mode themes, I unified the background to #2d2d2d. I also set it so the label part has no background if the label is empty:

.label-bar:empty {
  background: none;
  padding: 0;
  margin-bottom: 0;
}

Furthermore, I made the color of the copy button a soft dusty pink and gray 🩰. With the added support for collapsing, I finished it with a clean and "soft-cute" look ♡.

ChatGPT Image 2025-04-22 22_36_57

📦 Final Code Summary (Complete Version)

Since this is #vibe coding, please excuse the minor details...

meta.json
{"global":false,"host_template_types":["PAGE","BLOG_POST"],"label":"Code Block_v2","is_available_for_new_content":true,"description":"code block with Prism.js and copy button"}
fields.json
[{"name":"block_label","label":"Label","type":"text"}, {"name":"language","label":"Language","type":"choice","display":"select","choices":[["html","HTML"],["css","CSS"],["javascript","JavaScript"],["jinja2","HubL"],["json","JSON"],["powershell","PowerShell"]],"default":"html"}, {"name":"code_snippet","label":"Code","type":"text","allow_new_line":true}]
module.html
<!-- Loading prism.js -->
{{ require_css("https://cdn.jsdelivr.net/npm/prismjs/themes/prism-tomorrow.min.css") }}
{{ require_js("https://cdn.jsdelivr.net/npm/prismjs/prism.js") }}
{{ require_js("https://cdn.jsdelivr.net/npm/prismjs/components/prism-jinja2.min.js") }}
{{ require_js("https://cdn.jsdelivr.net/npm/prismjs/components/prism-json.min.js") }}
{{ require_js("https://cdn.jsdelivr.net/npm/prismjs/components/prism-powershell.min.js") }}

<div class="code-container">
  <div class="label-bar js-toggle">
    {{ module.block_label }}
    <button class="copy-button">📋 コピー</button>
  </div>
  <div class="code-body">
    <pre><code class="language-{{ module.language }}">{{ module.code_snippet | escape | escape_jinjava }}</code></pre>
  </div>
</div>

module.css
  /* ========== Container ========== */

  .code-container {
  border-radius: 6px;
  background-color: #2d2d2d;
  overflow: hidden;
  margin-bottom: 1rem;
  }
  
  /* ========== Label Bar ========== */
  
  .label-bar {
  font-weight: bold;
  color: #cbd5e1;
  font-size: 12px;
  background-color: #2d2d2d;
  padding: 0.4rem 1rem;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  transition: background 0.2s ease;
  }
  
  .label-bar::before {
  content: "▾";
  margin-right: 0.5rem;
  transition: transform 0.2s ease;
  }
  
  .label-bar.collapsed::before {
  transform: rotate(-90deg);
  }
  
  .label-bar:hover {
  background-color: #3a3a3a;
  }
  
  /* ========== Collapsible Body ========== */
  
  .code-body {
  max-height: 1000px;
  opacity: 1;
  transition: max-height 0.3s ease, opacity 0.3s ease;
  overflow: hidden;
  }
  
  .code-body.collapsed {
  max-height: 0;
  opacity: 0;
  padding: 0 !important;
  pointer-events: none;
  }
  
  /* ========== Code Block ========== */
  
  .code-body pre,
  pre[class*="language-"] {
  margin: 0 !important;
  font-family: Menlo, Monaco, 'Courier New', monospace;
  line-height: 1.5;
  font-size: 13px !important;
  position: relative;
  color: #f8f8f2;
  background-color: #2d2d2d;
  padding: 0 !important;
  white-space: pre;
  }
  
  .code-body code,
  code[class*="language-"] {
  display: block;
  min-height: 1em;
  padding: 0.2rem 1rem !important;
  white-space: pre;
  }
  
  /* ========== Copy Button ========== */
  
  .copy-button {
  font-size: 0.75rem;
  color: #e5d9e7;
  background: #2d2d2d;
  border: 1px solid #2d2d2d;
  border-radius: 0.25rem;
  padding: 0.2rem 0.5rem;
  transition: all 0.2s ease;
  }
  
  .copy-button:hover {
  background: #3b3b3b;
  }
  
  .copy-button.poko {
  animation: poko-scale 200ms ease;
  }
  
  /* ========== Animation ========== */
  
  @keyframes poko-scale {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.15); }
  100% { transform: scale(1); }
  }
module.js
document.addEventListener("DOMContentLoaded", () => {
  // Accordion toggle logic
  document.querySelectorAll(".label-bar").forEach(label => {
    label.addEventListener("click", () => {
      label.classList.toggle("collapsed");
      const codeBody = label.nextElementSibling;
      if (codeBody && codeBody.classList.contains("code-body")) {
        codeBody.classList.toggle("collapsed");
      }
    });
  });

  // Copy logic
  document.querySelectorAll(".copy-button").forEach(button => {
    button.addEventListener("click", (e) => {
      e.stopPropagation(); // Prevent conflict with accordion!
      const codeEl = button.closest(".code-container").querySelector("code");
      if (codeEl) {
        navigator.clipboard.writeText(codeEl.textContent);
        button.textContent = "✅ コピー済み";
        button.classList.add("poko");
        setTimeout(() => {
          button.textContent = "📋 コピー";
          button.classList.remove("poko");
        }, 1200);
      }
    });
  });
});

📝 Conclusion

Ultimately, I chose to keep it simple by intentionally removing features like "tab switching" and "line numbers." However, by going all-in on "visual cuteness" and "reliable copying," I've created a module that prioritizes practicality!

Next, I'd like to use this code block as a starting point to implement dark mode for the entire blog theme... 💭
I hope this article serves as a guide for anyone venturing into the depths of custom module creation 🐾

Discussion