ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:54:35 +10:00
commit 344b1701dd
505 changed files with 56231 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
/* Structured data.
*
* Search engines can read the pages perfectly well; what they can't do is work out that
* ThreadCount is one free product, made by one identifiable outfit, for one narrow job. That is
* what this states — and it is stated only where it is true, because marking up claims a page
* doesn't make is the fastest way to lose the rich result entirely.
*
* Notably absent: aggregateRating and review. There are no ratings, and inventing them is both a
* policy violation and a lie. */
const SITE = process.env.NEXT_PUBLIC_SITE_URL || "https://threadcount.tech";
import { PRICES } from "./plan";
export const organization = {
"@type": "Organization",
"@id": `${SITE}/#organization`,
name: "ThreadCount",
url: SITE,
logo: `${SITE}/og.png`,
email: "hello@threadcount.tech",
description: "Uniform stock management for healthcare: hospitals, aged care, clinics and community care.",
areaServed: { "@type": "Country", name: "Australia" },
contactPoint: [{
"@type": "ContactPoint",
contactType: "customer support",
email: "hello@threadcount.tech",
availableLanguage: ["English"],
}],
};
export const website = {
"@type": "WebSite",
"@id": `${SITE}/#website`,
url: SITE,
name: "ThreadCount",
publisher: { "@id": `${SITE}/#organization` },
inLanguage: "en-AU",
};
/* The product itself, with its price as it stands. While plans are off the one offer is `price:
"0"` — the honest and useful part, since a free-tier signal is exactly what someone comparing
options is filtering on. Once plans are live the offers are the three editions from the pricing
page, the free one first, read from lib/plan.ts so the markup cannot drift from the page. */
export const softwareApplication = (live: boolean) => ({
"@type": "SoftwareApplication",
"@id": `${SITE}/#software`,
name: "ThreadCount",
applicationCategory: "BusinessApplication",
applicationSubCategory: "Inventory management",
operatingSystem: "Web, Android",
url: SITE,
publisher: { "@id": `${SITE}/#organization` },
description:
"Uniform stock management for hospitals, aged-care homes, clinics and community care: what is on the shelf, who took it, what it cost the ward or clinic, and the supplier orders and stocktakes in between.",
offers: live
? [
{ "@type": "Offer", name: "Community — self-hosted", price: "0", priceCurrency: "AUD", availability: "https://schema.org/InStock" },
{ "@type": "Offer", name: `Hosted Small — up to ${PRICES.freeStaff} staff records`, price: "0", priceCurrency: "AUD", availability: "https://schema.org/InStock" },
{ "@type": "Offer", name: "Hosted Facility — a year", price: String(PRICES.hostedAnnual), priceCurrency: "AUD", availability: "https://schema.org/InStock", url: `${SITE}/pricing` },
{ "@type": "Offer", name: `Health Service — up to ${PRICES.healthServiceFacilities} facilities, a year`, price: String(PRICES.healthServiceAnnual), priceCurrency: "AUD", availability: "https://schema.org/InStock", url: `${SITE}/pricing` },
]
: {
"@type": "Offer",
price: "0",
priceCurrency: "AUD",
availability: "https://schema.org/InStock",
},
featureList: [
"Stock on hand by size",
"Issue to a staff member against their entitlement",
"Barcode scanning with a USB scanner or a phone camera",
"Stocktakes by shelf with variance reasons",
"Supplier orders, deliveries and back orders",
"Cost centre reporting for finance",
],
});
export function faqPage(entries: { q: string; a: string }[]) {
return {
"@type": "FAQPage",
"@id": `${SITE}/faq#faq`,
mainEntity: entries.map(({ q, a }) => ({
"@type": "Question",
name: q,
acceptedAnswer: { "@type": "Answer", text: a },
})),
};
}
/** Wraps whatever is passed in the single @graph envelope crawlers prefer. */
export function graph(...nodes: object[]) {
return { "@context": "https://schema.org", "@graph": nodes };
}