diff --git a/.github/workflows/content-quality.yml b/.github/workflows/content-quality.yml index 0066e46..09ddc07 100644 --- a/.github/workflows/content-quality.yml +++ b/.github/workflows/content-quality.yml @@ -48,7 +48,9 @@ jobs: - run: npm ci - run: npm run build env: - SITE_URL: https://dashnaiv.com + SITE_URL: https://www.dashnaiv.com + - name: Calculators compute + run: node scripts/test_calculators.mjs - name: Links resolve, and nothing outbound follows run: python3 scripts/check_links.py - name: Every written page completed the content loop diff --git a/data/tools/reconstitution-calculator.json b/data/tools/reconstitution-calculator.json new file mode 100644 index 0000000..923d9f0 --- /dev/null +++ b/data/tools/reconstitution-calculator.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Peptide reconstitution calculator", + "slug": "reconstitution-calculator", + "status": "draft", + "formula": "concentration (mcg/mL) = mass in vial (mg) x 1000 / diluent volume (mL). volume to draw (mL) = mass wanted (mcg) / concentration. syringe units = volume to draw (mL) x units per mL on the chosen scale (100, 50 or 40). whole draws per vial = floor(mass in vial (mcg) / mass per draw (mcg)). Worked example: 5 mg in 2 mL is 2500 mcg/mL; 250 mcg is 0.1 mL; on a U-100 syringe that is 10 units, on a U-40 it is 4.", + "summary": "Converts vial mass, diluent volume and a chosen mass per draw into a concentration, a volume and that volume in syringe units on a U-100, U-50 or U-40 scale. It warns when a draw falls below the smallest graduation on the syringe or exceeds its capacity. It takes no body weight and no condition, and it never returns a dose.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to add a bacteriostatic-water volume preset for common vial sizes, which risks implying a recommended reconstitution rather than reporting arithmetic." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/data/tools/unit-converter.json b/data/tools/unit-converter.json new file mode 100644 index 0000000..d1cbb30 --- /dev/null +++ b/data/tools/unit-converter.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Peptide unit converter", + "slug": "unit-converter", + "status": "draft", + "formula": "1 mg = 1000 mcg = 1000000 ng, fixed ratios. millilitres to syringe units = volume (mL) x units per mL on the chosen scale (100, 50 or 40). International units are deliberately not converted: an IU is defined per substance by an international reference preparation, so micrograms per IU differ by compound and exist only where a factor has been published.", + "summary": "Converts between milligrams, micrograms and nanograms, and converts a volume into units on a U-100, U-50 or U-40 syringe. It refuses to convert international units, because no general mass equivalent for an IU exists and any converter offering one has invented it.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to accept a user-supplied IU factor for a named compound, which would be honest arithmetic but could be mistaken for the site endorsing that factor." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/data/tools/vial-planner.json b/data/tools/vial-planner.json new file mode 100644 index 0000000..1903398 --- /dev/null +++ b/data/tools/vial-planner.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Vial planner", + "slug": "vial-planner", + "status": "draft", + "formula": "whole draws per vial = floor(mass in vial (mcg) / mass per draw (mcg)), rounded down because a partial draw is not a draw. volume per draw (mL) = mass per draw / concentration. remainder (mcg) = total mass - whole draws x mass per draw. duration (weeks) = whole draws / draws per week.", + "summary": "Reports how many whole draws a vial yields at a stated mass per draw, the volume of each draw, how long the vial lasts at a stated frequency, and the remainder that is left behind because it cannot be measured. It refuses to suggest the mass per draw, which is the one input it will not guess.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to model in-use stability limits, which vary by compound and diluent and would need a cited source per compound rather than a general rule." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/scripts/test_calculators.mjs b/scripts/test_calculators.mjs new file mode 100644 index 0000000..c8675a8 --- /dev/null +++ b/scripts/test_calculators.mjs @@ -0,0 +1,92 @@ +// Runs the calculator scripts exactly as the built pages ship them. +// +// npm run build && node scripts/test_calculators.mjs +// +// The unit converter and the vial planner shipped for weeks as a heading and a +// paragraph with no calculator at all, and nothing in the build noticed, +// because a page with no form still builds, still validates and still passes a +// link check. This extracts the inline module from each built page, runs it +// against a stub DOM and asserts on what it writes into the output element. +import fs from 'node:fs'; +import path from 'node:path'; + +const DIST = path.resolve(import.meta.dirname, '..', 'dist'); +const read = (slug) => { + const file = path.join(DIST, 'tools', slug, 'index.html'); + if (!fs.existsSync(file)) throw new Error(`${slug}: page not built`); + const html = fs.readFileSync(file, 'utf8'); + if (!/`); + if (!/`); + const m = html.match(/ diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index 8f02554..7ef087c 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -14,7 +14,7 @@ const PREFIX = { compounds: '/compounds/', stacks: '/stacks/', comparisons: '/co // Rendered pages that are not records. Kept here rather than derived, because // a page that should not be indexed must not be listed, and that is a decision // per page rather than a pattern. -const STATIC = ['/', '/compounds', '/stacks', '/compare', '/about', '/contact', '/privacy']; +const STATIC = ['/', '/compounds', '/stacks', '/compare', '/tools', '/about', '/contact', '/privacy']; export const GET: APIRoute = async ({ site }) => { const urls: string[] = []; diff --git a/src/pages/tools/[slug].astro b/src/pages/tools/[slug].astro index f5e79cc..c7e0485 100644 --- a/src/pages/tools/[slug].astro +++ b/src/pages/tools/[slug].astro @@ -1,11 +1,13 @@ --- -// Tool pages come from the registry plan, so the reconstitution calculator -// exists before a reviewed record does. They stay noindex until a record in -// data/tools/ reaches published. Nothing here accepts a body weight or a -// condition: the calculator does concentration and volume arithmetic only. +// Tool pages come from the registry plan, so a calculator exists before a +// reviewed record does. Nothing here accepts a body weight or a condition, and +// nothing returns a dose: each tool states its formula above its inputs so the +// arithmetic can be checked by hand, and each names the one thing it refuses. import Base from '../../layouts/Base.astro'; +import Calculators from '../../components/Calculators.astro'; import { tools } from '../../lib/registry'; import { records } from '../../lib/collections'; +import { isIndexable } from '../../lib/site'; export async function getStaticPaths() { return tools.map((t) => ({ params: { slug: t.slug }, props: { tool: t } })); @@ -13,45 +15,59 @@ export async function getStaticPaths() { const { tool } = Astro.props; const toolRecords = await records('tools'); const record = toolRecords.find((r) => r.data.slug === tool.slug); -const indexable = record?.data.status === 'published'; +const indexable = record ? isIndexable(record) : false; const path = `/tools/${tool.slug}`; +const peers = tools.filter((t) => t.slug !== tool.slug); + +const COPY: Record = { + 'reconstitution-calculator': { + h1: 'Peptide reconstitution calculator', + dek: 'Enter what is in the vial and what you want to draw. It returns the concentration, the volume, and that volume in units on the syringe scale you pick, with the formula printed above the inputs.', + title: 'Peptide Reconstitution Calculator: mg to mL and Units', + description: 'Work out concentration, volume to draw and syringe units from vial mass and diluent. Formula shown, U-100, U-50 and U-40 scales, nothing stored.', + }, + 'unit-converter': { + h1: 'Peptide unit converter', + dek: 'Milligrams, micrograms and nanograms are fixed ratios. Millilitres to syringe units depends on the scale printed on the barrel. International units are neither, and this page explains why it will not convert them.', + title: 'Peptide Unit Converter: mg, mcg, ng and Syringe Units', + description: 'Convert mg, mcg and ng, and millilitres to units on U-100, U-50 or U-40 syringes. Explains why no honest converter offers a general IU conversion.', + }, + 'vial-planner': { + h1: 'Vial planner', + dek: 'How many whole draws a vial yields at a stated mass per draw, how long that lasts at a stated frequency, and how much is left that cannot be drawn.', + title: 'Peptide Vial Planner: Draws per Vial and How Long It Lasts', + description: 'How many whole draws a vial gives at a stated mass per draw, the volume of each, how long it lasts, and the remainder too small to measure.', + }, +}; +const copy = COPY[tool.slug] ?? { + h1: tool.name, dek: tool.computes, + title: tool.name, description: `${tool.name}: ${tool.computes}.`, +}; --- - -
-

Tool · wave {tool.pilot_wave}

-

{tool.name}

+ +
+

Tools

+

{copy.h1}

+

{copy.dek}

+
Computes
{tool.computes}
Refuses
{tool.refuses}
+
Sends anywhere
Nothing. The arithmetic runs in your browser and no value is transmitted or stored.
- {tool.slug === 'reconstitution-calculator' && ( -
-

Concentration and volume

-

Formula, stated in the open: concentration = mass in the vial ÷ diluent volume. Volume to draw for a given mass = that mass ÷ concentration. Syringe units assume a 100-unit-per-millilitre insulin syringe, which is the only scale this converts to.

-
- - - - -
- -

Worked example: 5 mg in 2 mL gives 2,500 mcg/mL; 250 mcg is 0.1 mL, or 10 units. The calculator takes no body weight and no condition, and it does not suggest what mass to draw.

-
- )} - {!record &&

This tool has no reviewed record yet and is not indexed. It is published to preview deployments so the arithmetic can be checked.

} + + + +

What this tool will not do

+

It refuses {tool.refuses}. That is not a missing feature. A calculator that turns a body weight into a milligram figure has produced a dose, and a dose is a clinical decision that depends on why someone is taking something, what else they take and what their kidneys and liver are doing. None of that is arithmetic, and none of it is knowable from a number typed into a box.

+

Every figure this returns is a conversion of numbers you supplied. It carries no opinion about whether those numbers are appropriate, and the site's evidence pages are where the question of what has actually been studied belongs.

+ +

Checking it by hand

+

The formula is printed above the inputs so you can verify any result with a calculator you already trust. If a number here disagrees with one you worked out yourself, the discrepancy is worth reporting through the corrections page; arithmetic is the one thing on this site that can be checked without reading a paper.

+ + {!record &&

This tool has no reviewed record yet, so it is excluded from search indexing. The arithmetic works and can be checked against the formula above.

} + +

Other tools

+
    {peers.map((t) =>
  • {t.name}
    {t.computes}
  • )}
diff --git a/src/pages/tools/index.astro b/src/pages/tools/index.astro index b5fbb0a..1c1e69a 100644 --- a/src/pages/tools/index.astro +++ b/src/pages/tools/index.astro @@ -1,9 +1,19 @@ --- import Base from '../../layouts/Base.astro'; import { tools } from '../../lib/registry'; +import { records } from '../../lib/collections'; +import { isIndexable } from '../../lib/site'; +const entries = await records('tools'); +const live = entries.some((e) => isIndexable(e)); --- - -

Directory

-

Tools

-
    {tools.map((t) =>
  • {t.name}
    {t.computes}
  • )}
+ +
+

Directory

+

Peptide calculators

+

Arithmetic, shown. Each tool prints the formula above its inputs so any result can be checked by hand, runs entirely in your browser, and names the one thing it refuses to do.

+
    {tools.map((t) =>
  • {t.name}
    {t.computes}
  • )}
+

What none of them will do

+

None takes a body weight, an age or a condition, and none returns a dose. A calculator that turns a body weight into a milligram figure has made a clinical decision, and that decision depends on why someone is taking something, what else they take and how their kidneys and liver are working. None of that is arithmetic.

+

What each one does is convert numbers you already have into other numbers, by a formula you can read. Whether those numbers are appropriate is a different question, and the compound records are where the evidence for it lives.

+