Our Blog Just Got a Visual Upgrade — Here's How We Did It
+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.envand 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 block at the very end of 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 — fenced code blocks inside 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 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 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 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 / 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 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 blocks. They sit AFTER the ../style.css link in the , 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 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 ###### | – | |
| bold | | |
| italic | | |
| ` code | ` (inline) | |
| `lang ` | | Fenced code blocks |
| - list item or * list item | | Unordered only |
| !alt on its own line | | |
| — on its own line | | |
| text | | |
| Bare text | | |
What's NOT supported yet:
+-
+
> blockquote— the CSS callout rules are written and waiting
+ 1. ordered lists— nooutput
+ - 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 block at the end of — 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
*.htmlfile — 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 inside . Swap in a lightweight highlighter like highlight.js or shiki — or write a tokenizer that emits 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.
+ +