feat: add TinyLink docs section and Mermaid diagram support

Wire TinyLink into Eleventy collections, homepage highlights, and project navigation so the new integration design page is discoverable.

Enable Mermaid code fence handling in markdown and client-side rendering in the base layout, and normalize the TinyLink diagram syntax for compatibility.
This commit is contained in:
mahdahar 2026-04-07 14:18:51 +07:00
parent ca80720bfd
commit 73f8d67ee3
6 changed files with 262 additions and 14 deletions

View File

@ -1,3 +1,6 @@
const markdownIt = require("markdown-it");
const markdownItMermaid = require("markdown-it-mermaid");
module.exports = function (eleventyConfig) {
// Passthrough copy for static assets
eleventyConfig.addPassthroughCopy("src/assets");
@ -54,6 +57,16 @@ module.exports = function (eleventyConfig) {
return array.slice(0, n);
});
// Markdown: enable Mermaid diagrams
const mermaidPlugin = markdownItMermaid.default || markdownItMermaid;
const markdownLib = markdownIt({
html: true,
breaks: false,
linkify: true
}).use(mermaidPlugin);
eleventyConfig.setLibrary("md", markdownLib);
// Collections
// Blog posts / Proposals collection
eleventyConfig.addCollection("posts", function (collectionApi) {
@ -62,12 +75,13 @@ module.exports = function (eleventyConfig) {
});
});
// Projects collection (blog posts + CLQMS projects)
// Projects collection (blog posts + CLQMS + TinyLink)
eleventyConfig.addCollection("projects", function (collectionApi) {
const blogPosts = collectionApi.getFilteredByGlob("src/blog/**/*.md");
const clqmsPosts = collectionApi.getFilteredByTag("clqms");
const tinylinkPosts = collectionApi.getFilteredByTag("tinylink");
const allProjects = [...blogPosts, ...clqmsPosts].sort((a, b) => {
const allProjects = [...blogPosts, ...clqmsPosts, ...tinylinkPosts].sort((a, b) => {
return new Date(b.date) - new Date(a.date);
});
@ -81,6 +95,13 @@ module.exports = function (eleventyConfig) {
});
});
// TinyLink collection sorted by order
eleventyConfig.addCollection("tinylink", function (collectionApi) {
return collectionApi.getFilteredByTag("tinylink").sort((a, b) => {
return (Number(a.data.order) ?? 99) - (Number(b.data.order) ?? 99);
});
});
return {
dir: {
input: "src",

110
package-lock.json generated
View File

@ -8,6 +8,10 @@
"name": "5panda.11ty",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"markdown-it": "^14.1.1",
"markdown-it-mermaid": "^0.2.5"
},
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
"@tailwindcss/postcss": "^4.1.18",
@ -659,7 +663,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-buffer-byte-length": {
@ -1072,6 +1075,34 @@
"semver": "bin/semver"
}
},
"node_modules/d3": {
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
"integrity": "sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg==",
"license": "BSD-3-Clause"
},
"node_modules/dagre-d3-renderer": {
"version": "0.4.26",
"resolved": "https://registry.npmjs.org/dagre-d3-renderer/-/dagre-d3-renderer-0.4.26.tgz",
"integrity": "sha512-vOWj1uA4/APTrfDyfHaH/xpfXhPh9rszW+HOaEwPCeA6Afl06Lobfh7OpESuVMQW2QGuY4UQ7pte/p0WhdDs7w==",
"license": "MIT",
"dependencies": {
"d3": "3.5.17",
"dagre-layout": "^0.8.0",
"graphlib": "^2.1.1",
"lodash": "^4.17.4"
}
},
"node_modules/dagre-layout": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dagre-layout/-/dagre-layout-0.8.8.tgz",
"integrity": "sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ==",
"license": "MIT",
"dependencies": {
"graphlibrary": "^2.2.0",
"lodash": "^4.17.5"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@ -1892,6 +1923,24 @@
"dev": true,
"license": "ISC"
},
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/graphlibrary": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/graphlibrary/-/graphlibrary-2.2.0.tgz",
"integrity": "sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.5"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@ -2026,6 +2075,15 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -2970,7 +3028,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
@ -3030,6 +3087,12 @@
"node": ">=4"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@ -3051,10 +3114,9 @@
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
@ -3068,11 +3130,19 @@
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-mermaid": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/markdown-it-mermaid/-/markdown-it-mermaid-0.2.5.tgz",
"integrity": "sha512-ZUTFRX+cXEtWmn/9LMlpVklPJiDrHPWyHE/wamC2wm0Ojh1qOcuKWfWW3BqP83+7w6C59rS7M3OrGTs/u9mQTA==",
"license": "MIT",
"dependencies": {
"mermaid": "^7.1.2"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@ -3111,7 +3181,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true,
"license": "MIT"
},
"node_modules/memorystream": {
@ -3123,6 +3192,20 @@
"node": ">= 0.10.0"
}
},
"node_modules/mermaid": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-7.1.2.tgz",
"integrity": "sha512-bDLu3fQuf3/R0fNkNzB0GTaF7+6SxnZpfTs9DVQF1ougsuP23MBzvEIGfL0ML8zeyg7+J2D+0AaoLVhskW5ulw==",
"license": "MIT",
"dependencies": {
"d3": "3.5.17",
"dagre-d3-renderer": "^0.4.25",
"dagre-layout": "^0.8.0",
"he": "^1.1.1",
"lodash": "^4.17.4",
"moment": "^2.20.1"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@ -3196,6 +3279,15 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@ -3801,7 +3893,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -4639,7 +4730,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true,
"license": "MIT"
},
"node_modules/unbox-primitive": {

View File

@ -26,5 +26,9 @@
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"tailwindcss": "^4.1.18"
},
"dependencies": {
"markdown-it": "^14.1.1",
"markdown-it-mermaid": "^0.2.5"
}
}

View File

@ -52,6 +52,7 @@
<summary>Projects</summary>
<ul>
<li><a href="/projects/clqms01/" class="{% if '/projects/clqms01/' in page.url %}text-primary font-medium{% endif %}">CLQMS</a></li>
<li><a href="/projects/tinylink/001-design/" class="{% if '/projects/tinylink/' in page.url %}text-primary font-medium{% endif %}">TinyLink</a></li>
</ul>
</details>
</li>
@ -90,6 +91,11 @@
<span class="mr-2">🏥</span>CLQMS
</a>
</li>
<li>
<a href="/projects/tinylink/001-design/" class="{% if '/projects/tinylink/' in page.url %}text-primary bg-primary/10{% endif %} hover:text-primary hover:bg-primary/10">
<span class="mr-2">🔗</span>TinyLink
</a>
</li>
</ul>
</details>
</li>
@ -153,6 +159,16 @@
<p class="text-sm text-base-content/40">&copy; {% year %} 5Panda. Built with 11ty & Tailwind CSS.</p>
</aside>
</footer>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
(function () {
const diagrams = document.querySelectorAll('.mermaid');
if (!diagrams.length || !window.mermaid) return;
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
mermaid.run({ nodes: diagrams });
})();
</script>
<!-- Theme toggle script -->
<script>
document.getElementById('theme-toggle').addEventListener('click', function () {

View File

@ -110,3 +110,36 @@ description: Innovative projects and ideas
</div>
</section>
{% endif %}
<!-- TinyLink Section -->
{% if collections.tinylink.length > 0 %}
<section class="py-20">
<div class="section-container-wide">
<div class="text-center mb-12">
<span class="badge badge-outline mb-4">TinyLink</span>
<h2 class="text-3xl md:text-4xl font-bold mb-4">Integration Hub</h2>
<p class="text-base-content/70 max-w-2xl mx-auto">
Bi-directional instrument integration and message translation workflows.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{% for post in collections.tinylink | head(4) %}
<a href="{{ post.url }}" class="post-card group">
<div class="flex items-center gap-3 mb-3">
<span class="text-sm text-base-content/50">TinyLink</span>
<span class="text-base-content/30">•</span>
<span class="text-sm text-base-content/50">{{ post.content | readingTime }}</span>
</div>
<h3 class="text-xl font-bold mb-2 group-hover:text-primary transition-colors">{{ post.data.title }}</h3>
<p class="text-base-content/70">{{ post.data.description | excerpt(120) }}</p>
</a>
{% endfor %}
</div>
{% if collections.tinylink.length > 4 %}
<div class="text-center mt-10">
<a href="/projects/tinylink/001-design/" class="btn btn-outline">View TinyLink Docs</a>
</div>
{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,84 @@
---
layout: post.njk
tags: tinylink
title: "TinyLink: Integration Design"
description: "Bi-directional integration hub design for instrument results and CLQMS01 workflows."
date: 2026-04-07
order: 1
---
# TinyLink Integration Design
## Overview
TinyLink is a bi-directional integration hub between laboratory instruments and the CLQMS01 host. It receives instrument results, wraps raw payloads into a canonical envelope, translates them to JSON, and pushes them to CLQMS01. It also retrieves test requests from CLQMS01 and delivers them to instruments through download and query workflows (planned).
## Goals
- Provide reliable, automated data exchange between instruments and CLQMS01.
- Normalize and translate messages between instrument formats and JSON for CLQMS01.
- Support both download (new requests) and query (SampleID) workflows.
## Non-Goals
- User interface or manual data entry.
- Business rule orchestration beyond mapping and routing.
- Long-term analytics or reporting.
## High-Level Architecture
```mermaid
graph LR
subgraph Instrument
INST[Instrument]
end
subgraph App
RCV[Receiver / Raw Capture]
XLT[Translator]
ROUTE[Message Router - planned]
POLL[Scheduler / Poller - planned]
end
subgraph CLQMS01
CLQ[CLQMS01 Host]
end
%% Result flow
INST -->|Result data| RCV
RCV --> XLT
XLT -->|JSON| CLQ
%% Download request flow
POLL -->|Check new requests| CLQ
CLQ -->|New requests| ROUTE
ROUTE -->|Instrument message| INST
%% Query flow
INST -->|Query + SampleID| ROUTE
ROUTE -->|SampleID| CLQ
CLQ -->|Ordered tests| XLT
XLT -->|Instrument response| INST
```
## Data Flows
- Result flow (Instrument → CLQMS01): receive instrument output, wrap raw payload into a canonical envelope, translate to JSON, send to CLQMS01.
- Download request flow (CLQMS01 → Instrument): poll CLQMS01 for new requests, map to instrument message, send to instrument. (planned)
- Query flow (Instrument → CLQMS01 → Instrument): instrument sends query with SampleID, fetch ordered tests from CLQMS01, translate to instrument response, send back. (planned)
## Current vs Planned
- Current: result ingest → raw capture → translator → queue → delivery worker → CLQMS01.
- Planned: scheduler/poller for download requests and message router for download/query workflows.
## Key Components
- Receiver/Raw Capture: accepts instrument output and records it as raw payload for translation.
- Translator: maps instrument fields to JSON schema and vice versa.
- Message Router: routes messages to the correct workflow and destination. (planned)
- Scheduler/Poller: periodically checks CLQMS01 for new requests. (planned)
- CLQMS01 Adapter: handles request/response and authentication to the host.
## Reliability and Error Handling
- Retry on transient network failures.
- Log and flag translation/normalization errors with raw payload references.
- Idempotency safeguards for resends when possible.
## Security and Configuration
- CLQMS01 host and API key stored in config.
- Instrument connection details stored in config.
- No secrets committed to source control.