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
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
---
|
||||
title: "Our Blog Just Got a Visual Upgrade — Here's How We Did It"
|
||||
slug: blog-visual-upgrade
|
||||
date: "2026-06-03"
|
||||
description: "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."
|
||||
og_description: "How we restyled the Tinqs blog with a gradient-first, dark-code-panel aesthetic — using only two template files and a markdown build script."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "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."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "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
|
||||
|
||||
```bash
|
||||
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:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
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:
|
||||
|
||||
```bash
|
||||
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 |
|
||||
| `` 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:
|
||||
|
||||
```js
|
||||
// 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
Reference in New Issue
Block a user