1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
/* 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 };
|
|
}
|