Files
blog/posts/blog-visual-upgrade.md
T
ozan d223708a1d post: blog visual upgrade — restyle story + toolkit guide
New public post documenting the blog's visual refresh:
- Design inspiration from internal team guide
- The three-layer styling architecture (site CSS → inline overrides)
- Full post template changes (gradient titles, code panels, pills, h2 bars)
- Index template changes (kicker, card hover, date pills)
- The build system (posts/*.md + templates → *.html via build.js)
- Comprehensive toolkit guide: adding posts, understanding templates,
  styling workflow, markdown dialect, extending build.js, cheatsheet
2026-06-03 02:28:44 +01:00

15 KiB
Raw Blame History

title, slug, date, description, og_description, og_image, excerpt, author, author_initials, author_role
title slug date description og_description og_image excerpt author author_initials author_role
Our Blog Just Got a Visual Upgrade — Here's How We Did It blog-visual-upgrade 2026-06-03 We gave the Tinqs blog a visual refresh, borrowing the dark, gradient-heavy aesthetic from our internal team guides. No external CSS tinkering, no framework, no build-step drift — just two template files and a Node script. How we restyled the Tinqs blog with a gradient-first, dark-code-panel aesthetic — using only two template files and a markdown build script. https://www.tinqs.com/img/og-cover.jpg We gave the Tinqs blog a visual refresh — borrowing the dark, gradient-heavy look from our internal team docs. Here's why, what we changed, and how the build system made it painless. Ozan Bozkurt OB CTO & Developer, Tinqs

Until yesterday, the Tinqs blog looked... fine. Readable. Semantic. It had the brand amber accent, proper typography, and all the SEO metadata in the right places. But it didn't have much personality. The code blocks were unstyled. The headings sat flat. And the design said "competent" more than "intentional".

Then we looked at our own internal team guide — the onboarding doc we keep at docs/team/dev-basics-env-secrets-git.html. It had gradient titles that clip to transparent. Dark, crisp code panels. Callout boxes with coloured left borders. Pill-shaped date labels. A restrained four-colour palette that felt cohesive without screaming.

We wanted the blog to feel like it came from the same shop. So we restyled it.

The design source

Our team guide is a single self-contained HTML file with a dark theme — background #0d1117, panels #161b22, ink #e6edf3. It uses a four-accent palette:

  • Green #22c55e — for .env and environment topics
  • Blue #38bdf8 — secondary links, kickers, syntax table headers
  • Purple #a855f7 — git topics, hover states
  • Amber #f59e0b — warnings, emphasis, callouts

The title is the star: an h1 filled with a linear-gradient across all four colours, clipped to the text via -webkit-background-clip: text. Code blocks live in dark #0a0e14 panels with #2a3340 borders. Blockquotes become amber-tinted callouts. The whole thing radiates a "well-maintained developer doc" energy without feeling like a Bootstrap template.

We wanted that energy on the public-facing blog.

The build system (and why it mattered)

The blog is generated by a zero-dependency Node script — build.js — that converts markdown posts into HTML. The pipeline is:

posts/*.md  +  _template.html / _index_template.html  →  *.html

This means we never touch a generated .html file by hand. Every visual change flows through the two templates. build.js then stamps out all 11 posts plus the index in under a second.

The site-wide CSS — navigation, footer, base typography, the brand amber --c-accent: #c9935a — lives in ../style.css, which is served by Git Studio from outside the blog repo. We deliberately did not touch that file. Instead, we injected a self-contained <style> block at the very end of <head> in both templates, after the ../style.css link. Cascade order handles the overrides.

What we changed

Post template (_template.html)

Gradient title. The post h1 now gets the gradient treatment — amber #c9935a → warm gold #f59e0b → blue #38bdf8. It's the same technique as the team guide: background-clip: text; color: transparent. The underlying text is still there for screen readers and SEO.

Date pill. The post date is now a monospace chip — blue text, 999px border-radius, uppercase, tight letter-spacing. It sits before the title like a kicker.

Code blocks. This was the biggest functional improvement. The site CSS only styled inline <code> — fenced code blocks inside <pre> had no styling at all. We added a dark panel (#0a0e14 background, #2a3340 border, 10px radius, monospace, overflow-x: auto). A reset rule on .post__body pre code prevents the inline-code styles from doubling up inside the panel. Inline code got its own treatment: #1c2230 background, green #9fe6c0 text.

Section cues. h2 headings now have a 4px amber left border as a visual anchor. h3 headings get a subtle purple tint — enough to signal a section break without pulling focus.

Links and emphasis. Body links are blue #38bdf8 and shift to purple #a855f7 on hover. Bold text picks up amber #f59e0b. Horizontal rules became a single dark #2a3340 line.

Blockquote callouts. We wrote the CSS for amber-tinted callout blockquotes — tinted background, 4px amber left border, rounded right corners — but build.js doesn't emit <blockquote> yet. The rules are there, ready to activate when someone adds blockquote handling or swaps in a full markdown library.

Index template (_index_template.html)

The blog listing page got the same treatment, translated to its own selectors:

  • .blog-header__title — same amber→gold→blue gradient
  • .section-label — a monospace kicker pill above the title
  • .blog-card__date — date pill matching the post pages
  • .blog-card:hover — border shifts to brand amber on hover
  • .blog-card__read — blue link, purple on card hover

The palette at a glance

Role Colour Where
Brand anchor #c9935a Gradient start, h2 left bar, card hover
Warm gold #f59e0b Gradient midpoint, bold text
Blue #38bdf8 Gradient endpoint, links, date pills
Purple #a855f7 h3 colour, link hover
Dark panel #0a0e14 Code block background
Border #2a3340 Code panels, hr, inline code

Four accent colours. No rainbow.

The rebuild

cd ~/tinqs-ltd/blog && node build.js
Building blog...
  agent-harness.md → agent-harness.html
  agentic-workflow.md → agentic-workflow.html
  ...
  studio-cli.md → studio-cli.html
  index.html (listing)
Done — 11 posts built.

Zero errors. Every regenerated HTML file now carries the inline <style> block. We confirmed with grep -l "background-clip" *.html — all 12 files (11 posts + index) ship the gradient.

What we didn't change

Navigation, footer, and site chrome are untouched. This was a CSS-only change — no markup was altered in either template beyond the <style> injection. The existing responsive behaviour is preserved. The blog still uses the same IBM Plex Sans font stack, the same SEO metadata, the same build.js pipeline.

What's next

The restyle is on a branch (style/team-guide-aesthetic) awaiting review. Once merged, the blog gets its new look with no deployment step beyond a git push — Git Studio picks it up automatically.

Two gaps we might address later:

  1. Blockquote support in build.js. The callout CSS is written and waiting. Adding > syntax to our markdown converter would let post authors drop amber callout boxes anywhere in a post.

  2. Ordered lists. Same story — the CSS isn't written because build.js doesn't emit <ol> / <li> inside ordered lists yet. Both are one-function additions.

The blog toolkit: a hands-on guide

If you're going to write for the Tinqs blog — or tweak how it looks — here's everything you need to know, all in one place.

Adding a new post

Every post starts as a markdown file in posts/. The filename doesn't matter for routing (that's driven by the slug field), but we name them descriptively: blog-visual-upgrade.md, pre-commit-agent.md, etc.

The file has two parts: YAML frontmatter (metadata wrapped in ---) and markdown body (everything after the second ---).

Frontmatter fields:

---
title: "Post Title — with optional subtitle"
slug: url-friendly-slug
date: "2026-06-03"
description: "One-sentence summary for meta tags and SEO."
og_description: "Shorter version for social cards (optional — falls back to description)."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "A teaser line shown on the blog index page."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---

All fields are required except og_description and og_image (they have defaults). The slug becomes the filename on disk — blog-visual-upgrade produces blog-visual-upgrade.html.

Markdown body: The first paragraph after frontmatter becomes the lead (shown above the fold on the post page). Everything after the first blank line is the body. build.js splits them automatically.

Once your .md file is ready:

node build.js

That regenerates the new post's HTML plus a fresh index.html with the updated card listing. No manual HTML editing, ever.

The template handshake

The blog uses two Handlebars-style templates (actually plain string replacement — no library needed):

File Role Key placeholders
_template.html Wraps a single blog post {{TITLE}}, {{DATE_DISPLAY}}, {{LEAD}}, {{BODY}}, {{AUTHOR_*}}
_index_template.html Wraps the blog listing page {{CARDS}} — replaced with an <a class="blog-card"> block per post

build.js reads both templates at startup, then for each post:

  1. Parses the frontmatter + splits lead from body
  2. Runs the markdown converter on the body (md() function)
  3. Does template.replace(/\{\{KEY\}\}/g, value) for every placeholder
  4. Writes the result to {slug}.html

After all posts are built, it generates index.html by sorting posts newest-first and replacing {{CARDS}} with a block of .blog-card links.

The critical rule: never edit a generated *.html file. They get overwritten on the next node build.js. Always change the templates or the markdown source.

The three-layer styling architecture

Styling has three layers, and they cascade in this order:

1. ../style.css          ← external, served by Git Studio (untouchable from this repo)
2. <style> in _template  ← post-page overrides (inline, at end of <head>)
3. <style> in _index     ← index-page overrides (inline, at end of <head>)

Layer 1 provides the nav, footer, base typography, and the --c-accent: #c9935a variable. It also defines bare selectors like .post__title, .post__date, .blog-card, etc. — but with minimal styling.

Layers 2 and 3 are our self-contained inline <style> blocks. They sit AFTER the ../style.css link in the <head>, so same-specificity rules win by cascade order. We never use !important — the position handles precedence naturally.

Why inline instead of a separate .css file? The blog repo is standalone — it doesn't control what Git Studio serves. Adding a file like blog-style.css would require coordinating a deploy to the parent site. Inline <style> blocks ship inside the generated HTML, so the blog is fully self-contained. One git push and it's live.

The markdown dialect (what build.js understands)

Our converter is intentionally minimal — zero dependencies, about 100 lines of Node. It handles the subset we actually use:

Markdown HTML emitted Notes
# Heading through ###### <h1><h6>
**bold** <strong>
*italic* <em>
`code` <code> (inline)
```lang ``` <pre><code class="language-lang"> Fenced code blocks
- list item or * list item <ul><li> Unordered only
![alt](url) on its own line <figure><img><figcaption>
--- on its own line <hr>
[text](url) <a href>
Bare text <p>

What's NOT supported yet:

  • > blockquote — the CSS callout rules are written and waiting
  • 1. ordered lists — no <ol> output
  • Nested lists, tables, inline HTML, footnotes

If you need one of these, the fix lives in the md() function in build.js. Each missing feature is a ~5-line addition.

Adding a new style rule

You're writing a post and want a new visual element. The workflow:

  1. Open _template.html (or _index_template.html for listing-only styles)
  2. Find the <style> block at the end of <head> — it's clearly marked with a /* ── Team guide aesthetic ── */ comment
  3. Add your rule inside that block. Follow the existing conventions:
    • Use the team guide palette (amber #c9935a, gold #f59e0b, blue #38bdf8, purple #a855f7)
    • Prefix body-content rules with .post__body to scope them
    • Match the existing code style (2-space indent, comment headers for sections)
  4. Rebuild: node build.js
  5. Verify: open the page and check; grep your new selector in *.html to confirm it shipped

The golden rules for style additions:

  • Never edit ../style.css — it's outside the repo
  • Never hand-edit a *.html file — the build will clobber it
  • Don't restyle .nav, .footer, or the mobile menu — those belong to the parent site
  • Do use the existing palette; don't introduce new colours unless there's a strong reason
  • Keep it self-contained — no external font loads, no CDN dependencies, no @import

Extending build.js

Here are the most likely extensions and where to add them:

Blockquote support (> lines in markdown). Add to the md() function after the list handler:

// Blockquote
const bqMatch = line.match(/^>\s?(.*)$/);
if (bqMatch) {
  closeUl();
  html += `<blockquote><p>${inline(bqMatch[1])}</p></blockquote>\n`;
  continue;
}

Ordered lists (1. item). Add after the unordered list handler, with a separate inOl flag and closeOl() function mirroring closeUl().

Syntax highlighting. The current md() function already adds class="language-{lang}" to <code> inside <pre>. Swap in a lightweight highlighter like highlight.js or shiki — or write a tokenizer that emits <span> classes matching the team guide's .c, .g, .p, .y, .r colour convention.

Swap in a full parser. If the feature gap gets annoying, replace the md() function with marked or markdown-it. The template system and frontmatter parsing stay the same — only the body conversion changes. One require() call and you get tables, blockquotes, ordered lists, and footnotes for free.

Quick reference cheatsheet

# Add a new post
nano posts/my-post.md       # write frontmatter + markdown
node build.js               # regenerate HTML

# Tweak styling
nano _template.html         # edit the <style> block
node build.js               # rebuild all pages
grep "your-selector" *.html # confirm it shipped

# Verify before deploy
git diff --stat              # should only show templates + *.html
node build.js                # must exit 0
ls *.html | wc -l           # 12 files = 11 posts + index

In the meantime, the blog already looks sharper and more intentional — and it took two template files, one build step, and zero external dependencies to get there. That's the kind of upgrade we like.