Overview#
Wardline is a corporate B2B SaaS website template for a compliance automation product: a platform that helps
startups and mid-size companies prepare for SOC 2, ISO 27001, HIPAA, GDPR, PCI DSS and NIST CSF with continuous
control monitoring, evidence collection, policies, vendor risk, access reviews and a public trust center. The
fictional company, Wardline, Inc., is based in Richmond, Virginia, and works in Eastern time
(America/New_York). This is the Next.js version: Next.js 16 (App Router), React 19, TypeScript and Tailwind CSS
v4, exported as a fully static site, version 1.0.0. See the template page at
https://freeframe.dev/templates/wardline, which links to the live demo.
The copy is careful about what a compliance tool can claim: Wardline helps customers prepare, and independent auditors (a licensed CPA firm for SOC 2, an accredited certification body for ISO 27001) issue reports and certificates. Keep that distinction if you adapt the template for a real product.
Quick start#
Requirements: Node.js 24 and pnpm (corepack enable pnpm) or npm.
pnpm install
pnpm dev # http://localhost:3000| Command | What it does |
|---|---|
pnpm dev |
Development server at http://localhost:3000 |
pnpm build |
Static site in out/ |
pnpm start |
Serve out/ locally (npx serve out) |
pnpm lint |
ESLint (Next.js core-web-vitals and TypeScript rules) |
pnpm typecheck |
Generate route types (next typegen), then type-check with tsc (TypeScript 7) |
pnpm build:demo |
Build for /demos/nextjs/wardline into demo/ (the freeframe.dev demo; you can delete it) |
Pages#
| Route | File | Purpose | Notable sections and features |
|---|---|---|---|
/ |
src/app/page.tsx |
Home | Hero with a control dashboard that switches between SOC 2, ISO 27001 and HIPAA, drawn customer marks, connect/monitor/fix/audit steps with isometric art, framework cards built from data, numbers band, "who does what" comparison, Kelvara story teaser, assessment CTA |
/platform |
src/app/platform/page.tsx |
Platform tour | Six keyboard tabs (controls, evidence, policies, vendor risk, access reviews, trust center), each with its own HTML/SVG mockup; roles; access table |
/frameworks |
src/app/frameworks/page.tsx |
Framework explorer | Framework chips, control families as bars, cross-mapping with a Venn diagram and a control table, a pairwise overlap matrix, "who issues what" table |
/assessment |
src/app/assessment/page.tsx |
Readiness assessment | Eight-question quiz, score ring, ranked gaps, preparation range, deadline verdict, method notes |
/pricing |
src/app/pricing/page.tsx |
Pricing | Three plan cards, calculator with an itemized estimate, comparison table, FAQ |
/integrations |
src/app/integrations/page.tsx |
Integrations directory | Search, category chips with counts, sorting, 24 cards with drawn monograms, empty state |
/trust |
src/app/trust/page.tsx |
Trust center | Control status by category, rolling report dates, documents, subprocessors, document request form with NDA |
/customers |
src/app/customers/page.tsx |
Customer stories | Framework filter, three stories with before-and-after charts |
/contact |
src/app/contact/page.tsx |
Book a demo | Three-step form with validation, saved draft, slot picker in the visitor's time zone, success state |
| any unknown URL | src/app/not-found.tsx |
Not found | "Control not found" illustration and links |
Project structure#
wardline/
src/app/
layout.tsx html shell: fonts, metadata, skip link, header, footer, notice, reveals
fonts.ts Anek Latin, Source Sans 3 and Roboto Mono via next/font/local
globals.css Tailwind import, @theme brand tokens and a few components
icon.svg favicon (the Wardline mark)
page.tsx, */page.tsx one route per page
not-found.tsx the 404 page
src/components/
SiteHeader.tsx status strip, logo, main menu and mobile menu (client)
SiteFooter.tsx footer with the office status and copyright year
HeroDashboard.tsx home page dashboard switch (client)
FrameworkCards.tsx home page framework cards from data
TourTabs.tsx platform tour tabs and panels (client)
FrameworkExplorer.tsx explorer (client) and the overlap matrix
Assessment.tsx readiness assessment (client)
Pricing.tsx pricing calculator and plan cards (client)
IntegrationsDirectory.tsx search, chips and sorting (client)
TrustStatus.tsx trust center control categories and subprocessors
TrustRequest.tsx document request form and "Request access" buttons (client)
Stories.tsx customer story filter (client)
DemoBooking.tsx three-step demo booking (client)
art.tsx framework seals, integration monograms and story charts (SVG)
live.tsx year, office status, relative dates and report dates (client)
Notice.tsx the polite notice bar and announce()
RevealController.tsx scroll reveals
NavigationSync.tsx tells URL-driven views when the router changes the address
icons.tsx logo mark and arrow icon
src/data/site.ts all content data: frameworks, controls, integrations, pricing, quiz, trust center, demo, stories
src/lib/
dates.ts Eastern time, federal holidays, business days, report dates, office hours
client.ts browser-only hooks: useNow (with ?now=), useSearch, useParam, replaceUrl
store.ts saved state as an external store (useStored, saveStored, removeStored)
safe-storage.ts localStorage with an in-memory fallback, storage keys
assessment.ts assessment steps, scoring and answer cleaning
pricing.ts quote(), plan rules and state cleaning
demo.ts time zones, slot hash, draft cleaning and specialist routing
util.ts own(), plural(), money(), reference codes, control library maths
stats.ts site-wide counts from the data
metadata.ts per-page title, description and Open Graph
asset.ts adds the base path to files in public/ (asset) and to plain page links (pageUrl)
src/fonts/ woff2 files and their SIL Open Font License texts
public/img/ og-image.svg (source), og-image.png (1200x630), favicon.svg
next.config.ts static export and optional base path
template.json, README.md, DOCS.md, CHANGELOG.mdCustomizing#
Brand and colors#
Colors and fonts are Tailwind theme tokens in @theme in src/app/globals.css, so every utility (bg-ink,
text-teal, border-line) follows them.
| Token | Value | Used for |
|---|---|---|
--color-ink |
#0b1a2c |
Headings, body text, dark bands and footer |
--color-paper / --color-card / --color-cloud |
#f6f8fa / #ffffff / #edf1f5 |
Page background, panels, alternate bands |
--color-line / --color-line-strong |
#dbe2ea / #93a1b3 |
Hairlines and input borders |
--color-slate / --color-slate-2 |
#3f4d61 / #566376 |
Secondary text and captions (AA on light backgrounds) |
--color-frost |
#a9b8ca |
Secondary text on ink |
--color-teal, -deep, -soft, -bright |
#0b6f80 … |
The accent: buttons, links, focus rings, charts |
--color-pass, --color-attn, --color-fail (+ -soft) |
green, amber, red | Status pills, always with a symbol and a word |
To change the accent, edit the teal tokens; the dev server picks the change up on save:
--color-teal: #1d4ed8;
--color-teal-deep: #1e3a8a;
--color-teal-soft: #dbeafe;The generated SVG art in src/components/art.tsx and FrameworkExplorer.tsx (seals, Venn diagram, charts,
monograms) and the inline SVGs in the pages use hex values directly; search for #0b6f80 to update them too.
Typography#
src/app/fonts.ts self-hosts three families with next/font/local, from the same Latin files Google Fonts
serves the HTML version (src/fonts/):
- Anek Latin (500–700) for headings (
--font-display,font-display) - Source Sans 3 (400–700, italic 400) for body text (
--font-sans) - Roboto Mono (400–500) for control IDs, labels and figures (
--font-mono,font-mono)
Each face is declared under its real family name, so globals.css and the SVG art refer to "Anek Latin",
"Source Sans 3" and "Roboto Mono" directly. A family name that starts a word with a digit must be quoted in CSS
and SVG, which is why fonts.ts writes 'Source Sans 3' with quotes. To swap a family, put its woff2 file in
src/fonts/, change the localFont() call and its font-family declaration, and update the matching variable
in @theme. Keep fallback set to the real family name and adjustFontFallback: false, so no Arial-based
fallback face can win on macOS or Windows.
Content#
Everything data-driven lives in src/data/site.ts:
| Constant | What it holds |
|---|---|
FRAMEWORKS |
Six frameworks with a one-letter code, seal shape, issuer, summary and prep |
FAMILIES |
Fifteen control families by two-letter prefix |
CONTROLS |
The 81-control library as [id, title, frameworkLetters] |
INTEGRATION_CATEGORIES, INTEGRATIONS |
Six categories and 24 integrations with checks, setup minutes and access |
ANNUAL_DISCOUNT, PLANS, ADDONS |
Pricing rules |
ASSESSMENT |
Sizes, five scored questions, targets, deadlines and score bands |
TRUST_CATEGORIES, SUBPROCESSORS, DOCUMENTS, PERSONAL_DOMAINS |
Trust center content and email checks |
DEMO, SPECIALISTS |
Demo form options, slot times and routing |
STORIES |
Customer stories and their before-and-after metrics |
Adding a control mapped to SOC 2, ISO 27001 and NIST CSF updates every count, bar, Venn diagram and matrix cell
(and the "81 common controls" figures, which come from src/lib/stats.ts):
["AC-10", "Break-glass accounts sealed and alerted", "SIN"],Adding an integration:
{ id: "shiftledger", name: "Shiftledger", category: "hr", checks: 7, summary: "…", access: "Read-only report token", minutes: 5 },Page copy (headings, story text, FAQ, the platform mockups) is JSX in each page.tsx.
Images and artwork#
All art is SVG drawn for this template: the logo, the four step illustrations, the six platform mockups (HTML
with inline SVG), the integrations hub, the 404 shield and the customer marks are inline in the pages. Framework
seals (Seal), integration monograms (Monogram) and the story charts (StoryChart) are React components in
src/components/art.tsx; the Venn diagram is in FrameworkExplorer.tsx. The seals are original generic badges,
not any standards body's mark; keep it that way unless you have permission to use an official logo.
The social image is public/img/og-image.svg; after editing it, render the PNG (1200×630) with a headless
browser or any SVG-to-PNG tool and save it as public/img/og-image.png. The favicon is src/app/icon.svg.
Navigation, header and footer#
The header and footer are shared components used by src/app/layout.tsx. To add a page, create
src/app/<route>/page.tsx (copy an existing one, including its pageMetadata() call), then add it to NAV (and
MOBILE_NAV if it should only appear in the mobile menu) in src/components/SiteHeader.tsx, and to the link
lists in src/components/SiteFooter.tsx. The header marks the current page with aria-current="page" from the
route.
Features#
Dates and the static build#
Anything that depends on the clock, the URL, saved state or the visitor's time zone renders after the page has
loaded. The hooks in src/lib/client.ts (useNow, useSearch, useParam) and useStored in
src/lib/store.ts use useSyncExternalStore and return null while the page is prerendered, so the static HTML
shows the text written in the page (for example "in 46 days", "Oct 1, 2025 – Sep 30, 2026" or "© 2026") and
there is no hydration mismatch; the browser then fills in the real values. useSearch also follows client-side
navigation (NavigationSync in the layout tells it when the address changes). Links that carry parameters into
a page whose state is read from the address (/frameworks?fw=…, /contact?plan=…) are plain <a> elements with
pageUrl(), so that page loads fresh from the new address, as in the HTML version.
Hero control dashboard#
src/components/HeroDashboard.tsx switches the dashboard on the home page between three frameworks with the
[data-dash-fw] buttons (aria-pressed). Each framework's passing, attention and failing counts, readiness
percentage and audit-window offset are in STATES at the top of the file. The audit window date is today plus
the offset (46 days for SOC 2, 73 for ISO 27001, 29 for HIPAA), so it never goes stale: on Nov 3, 2026 SOC 2
reads "Dec 19, 2026 · in 46 days".
Platform tabs#
src/components/TourTabs.tsx follows the ARIA tabs pattern: arrow keys, Home and End move between tabs, and the
URL hash (#controls, #evidence, #policies, #vendors, #access, #trust) opens a tab directly and scrolls
the tour into view. Choosing a tab writes its hash with history.replaceState. The tabs are listed in TABS in
src/app/platform/page.tsx; each panel is a <TourPanel id="…">. Dates inside the mockups use <DayOffset>
(see Keeping the demo current).
Framework explorer and cross-mapping#
src/components/FrameworkExplorer.tsx renders the selected framework's issuer, preparation note, control count
and families from FRAMEWORKS and CONTROLS, then compares it with a second framework. A control counts toward
a framework when its letters include the framework's code. Counts with the shipped data:
| Framework | Controls |
|---|---|
| SOC 2 | 61 |
| ISO 27001 | 63 |
| HIPAA | 36 |
| GDPR | 17 |
| PCI DSS | 49 |
| NIST CSF | 46 |
| Pair | Shared | Only first | Only second |
|---|---|---|---|
| SOC 2 + ISO 27001 | 57 | 4 | 6 |
| SOC 2 + HIPAA | 32 | 29 | 4 |
| SOC 2 + PCI DSS | 42 | 19 | 7 |
| ISO 27001 + GDPR | 9 | 54 | 8 |
The default sentence reads "57 of SOC 2's 61 controls (93%) also count toward ISO 27001. Adding ISO 27001 after
SOC 2 means 6 new controls, not 63." The view chips switch the table between shared and unique controls.
FrameworkMatrix draws the pairwise matrix; each number links to /frameworks?fw=<row>&vs=<column>#explorer.
URL parameters: ?fw=<id>&vs=<id|none> (ids: soc2, iso27001, hipaa, gdpr, pci, nist); unknown
values fall back to SOC 2 compared with ISO 27001. The explorer keeps both parameters in the address with
history.replaceState.
Readiness assessment#
src/components/Assessment.tsx steps through eight questions (size, cloud, policies, identity,
logging, vendors, target, deadline); options come from ASSESSMENT, and the hints under each question
are in STEP_COPY. The rules, in assess() in src/lib/assessment.ts:
- Score: the five scored answers add up their
points(20 each at best, 100 in all). - Band: 80+ "Well prepared", 55+ "Solid foundation", 30+ "Early stage", below that "Starting out".
- Gaps: every scored answer below 20 points, ranked by points missing (ties keep question order); 10 or more missing is "High priority", otherwise "Medium priority".
- Range:
low = round((targetLow + gapWeeks) × sizeFactor),high = round((targetHigh + gapWeeks × 1.5) × sizeFactor), with size factors 1, 1.15, 1.35 and 1.6. - Deadline verdict: "Realistic" when
highfits the deadline weeks (13, 26 or 52), "Tight" when onlylowfits, "At risk" otherwise; "No date" with no deadline.
Example: 51–200 people, one cloud set up by hand, draft policies, MFA without SSO, some logs, no vendor reviews, SOC 2 Type II, 3–6 months gives a score of 47 ("Early stage"), 16–24 weeks and "Realistic", with gaps in the order vendor risk reviews, security policies, logging and alerting, identity, cloud configuration.
Answers are saved as they are chosen, so a returning visitor resumes at the first unanswered question, or sees
their results when all eight are answered. "Change my answers" goes back to question 1 with the answers kept;
"Start over" clears them. The results link to /contact?fw=<framework>&size=<size>. Focus moves to each new
question and to the results heading; the copy says plainly that the result is an estimate, not an audit.
Pricing calculator#
src/components/Pricing.tsx (PricingCalculator and PlanCard) with quote() in src/lib/pricing.ts. The plan
follows the employee count (5–2,000): Essentials up to 100, Growth up to 500, Scale up to 2,000; the matching plan
card above the calculator is outlined.
monthly = base + extra employees × perEmployee + extra frameworks × extraFramework + add-ons
annual per month = round(monthly × (100 − ANNUAL_DISCOUNT) / 100)| Plan | Base | Included employees / frameworks | Per extra employee | Per extra framework | Included add-ons |
|---|---|---|---|---|---|
| Essentials | $690 | 25 / 1 | $8 | $240 | none |
| Growth | $1,450 | 100 / 2 | $6 | $200 | Trust center |
| Scale | $3,200 | 500 / 3 | $4 | $160 | Trust center, vendor risk |
Add-ons: vendor risk $260/month, penetration test coordination $350/month, security awareness training $2 per
employee per month, trust center $120/month. ANNUAL_DISCOUNT is 20.
Examples: the default (60 employees, 1 framework, annual) is $970 a month, or $776 a month billed annually
($9,312 a year). 120 employees, 2 frameworks, vendor risk and training is Growth at $2,070 a month, or
$1,656 a month billed annually ($19,872 a year, saving $4,968). Plan cards show "from" prices worked out from
PLANS: $552, $1,160 and $2,560 billed annually. "Book a demo with this estimate" links to
/contact?plan=<plan>&size=<s1–s4>.
Integrations directory#
src/components/IntegrationsDirectory.tsx filters INTEGRATIONS by category chip and a search over name,
summary and category, and sorts by name or by number of checks. Chip counts follow the search. The filters are
saved once the visitor changes one; ?category=<id> (cloud, identity, hr, ticketing, code,
endpoint) opens a category. Example: Identity and SSO plus "mfa" shows "2 integrations · 33 automated checks".
Trust center#
src/app/trust/page.tsx works out the summary (20 of 21 controls passing, 1 monitoring) from
TRUST_CATEGORIES; TrustCategories and Subprocessors in src/components/TrustStatus.tsx render the
categories (the first one open) and the subprocessor table. Report dates are <ReportDate> elements (see
Keeping the demo current). Status pills always pair color with a symbol and a word
("Passing", "Monitoring"). The document request form is described under Forms.
Customer stories#
StoryChart in src/components/art.tsx draws two bars per metric from STORIES[].metrics (before in gray,
after in teal, each with a text label and an aria-label). src/components/Stories.tsx builds the filter chips
from the frameworks the stories use and hides stories without the chosen one (frameworks on each
<StoryArticle>).
Demo booking and time zones#
src/components/DemoBooking.tsx runs three steps: team (role, size, at least one framework, timeline), details
(name, work email, company, note) and a time. Slots are DEMO.times in Eastern time (10:00, 11:30, 13:00, 14:30,
16:00) on the next five business days after today, skipping weekends and US federal holidays computed by rule in
holidaysFor(year) in src/lib/dates.ts (New Year's Day, Martin Luther King Jr. Day, Washington's Birthday,
Memorial Day, Juneteenth, Independence Day, Labor Day, Columbus Day, Veterans Day, Thanksgiving Day, Christmas
Day; Saturday holidays move to Friday, Sunday holidays to Monday). A note names any holiday skipped: at
?now=2027-11-24T12:00 the days run Fri, Nov 26 to Thu, Dec 2 with "Closed Nov 25 for Thanksgiving Day."
Some slots show as booked by a fixed hash of date and time (slotTaken() in src/lib/demo.ts).
Times show in the visitor's own zone (from the browser) with the Richmond time underneath; the "Show times in"
select switches between the visitor's zone and the others in VIEW_ZONES. ?tz=America/Los_Angeles (one of the
listed zones) preselects a zone.
The call is routed by SPECIALISTS, first match wins (specialistFor()): HIPAA to Soledad Ilunga, PCI DSS to
Evander Lachance, 201+ employees to Ravindra Satterwhite, everyone else to Leontyne Fairbairn. ?fw=<id>,
?size=<s1–s4> and ?plan=<essentials|growth|scale> prefill the first step (links from the assessment and
pricing pages use them).
Menu, notice and motion#
SiteHeader toggles the mobile menu (aria-expanded, the first link takes focus, Escape closes and returns
focus, a link or a wider window closes it). announce() in src/components/Notice.tsx shows a short message
in the polite notice bar (used by "Start over" and the pricing reset). Elements with .reveal fade in as they
scroll into view (RevealController); everything above a revealed element is revealed too, and reduced motion
shows everything at once. The pulse dot in the status strip stops under reduced motion.
Keeping the demo current#
Nothing dated goes stale:
KEEP_DEMO_CURRENT = trueinsrc/lib/dates.tsrolls the trust center dates (reportDates()): the SOC 2 Type II period ends on the latest September 30 whose report has been issued (issued on the fourth Wednesday of October), the ISO 27001 certificate runs in three-year cycles from March 14, 2025, and the latest penetration test is the most recent August. On Nov 3, 2026 that is Oct 1, 2025 – Sep 30, 2026, issued Oct 28, 2026; certified Mar 14, 2025, valid through Mar 13, 2028; August 2026. Set it tofalseto show the fixed dates inFIXED_REPORTS.<DayOffset offset={-3} format="medium" fallback="3 days ago" />(formatsshort,medium,weekday) insrc/components/live.tsxshows a date relative to today;<Today>shows today's date and<Year>the copyright year.- The hero audit window, the document review day and the demo slots are worked out from today.
?now=YYYY-MM-DDTHH:MMpreviews the whole site at another Eastern time, for reviews and screenshots.
For a real site with fixed dates, set KEEP_DEMO_CURRENT to false, edit FIXED_REPORTS, and replace the
<DayOffset> elements with real dates.
Forms#
| Form | Component | Validation | Success |
|---|---|---|---|
| Readiness assessment | Assessment.tsx |
One answer per question | Results panel |
| Document request | TrustRequest.tsx |
At least one document, full name, company, a valid work email (not a personal domain from PERSONAL_DOMAINS), the NDA box when any chosen document needs it |
Reference TR- + 6 characters, the next business day for review, next steps |
| Book a demo | DemoBooking.tsx |
Step 1: role, size, a framework, timeline. Step 2: first and last name, valid work email, company. Step 3: a slot | Reference DM-MMDD-XXXX (slot month and day plus 4 characters), time in Eastern and the visitor's zone, specialist, next steps |
Reference characters avoid 0, O, 1 and I. Errors appear inline next to each field, with aria-invalid on the
input, and focus moves to the first problem.
The demo forms don't send anything until you connect them. Each onSubmit has a comment at the spot to add a
request ("To connect a real endpoint" in TrustRequest.tsx, "To connect a real scheduler or CRM" in
DemoBooking.tsx). For example with Formspree, make the handler async:
const response = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(event.currentTarget),
headers: { Accept: "application/json" },
});
if (!response.ok) {
// show an error message and keep the form
return;
}
setDone({ ... }); // then show the success panel as the demo doesThe demo booking keeps its fields in React state (draft, slot, zone), so post those as JSON instead of
FormData there. For bookings you would usually call your scheduling tool's API and use its confirmed time.
With a server deployment (remove output: "export", see Building and deploying) you can
post to a Route Handler instead, for example src/app/api/demo/route.ts with
export async function POST(request: Request) that reads await request.json() and passes it to your CRM or
email service, or call a Server Action from the component.
Saved data (localStorage)#
| Key | Shape | Validation on read |
|---|---|---|
wardline:assessment:v1 |
{ "size": "s2", "cloud": "single", …, "target": "soc2t2", "deadline": "d6" } |
Each answer must be a known option key; unknown ones are dropped (cleanAnswers()) |
wardline:pricing:v1 |
{ "employees": 120, "frameworks": 2, "addons": ["vendor"], "billing": "annual" } |
Whole numbers in range, known add-on ids, monthly or annual; otherwise defaults (cleanPricing()) |
wardline:integrations:v1 |
{ "category": "identity", "q": "mfa", "sort": "name" } |
Known category or all, a string of up to 60 characters, name or checks |
wardline:demo-draft:v1 |
{ "role", "size", "frameworks", "timeline", "first", "last", "email", "company", "note" } |
Known keys only, strings cut to length (cleanDraft()); removed after booking |
Every read and write goes through safeStorage in src/lib/safe-storage.ts, which falls back to memory when the
browser blocks localStorage (private modes, sandboxed iframes), so the features keep working for the page view.
Keys from storage and the URL are checked with own() (Object.prototype.hasOwnProperty), so values like
?fw=constructor are ignored. To reset, clear the site's data or run
localStorage.removeItem("wardline:pricing:v1") in the console. Bump the :v1 suffix if you change a shape.
SEO and sharing#
Each page exports metadata from pageMetadata(title, description) in src/lib/metadata.ts, which also sets
Open Graph and Twitter tags with the social image. The layout's metadataBase comes from
NEXT_PUBLIC_SITE_URL; set it to your domain when you build so the image URL is absolute:
NEXT_PUBLIC_SITE_URL=https://www.example.com pnpm buildAdd src/app/sitemap.ts and src/app/robots.ts (both work with static export) listing the nine content pages.
A SoftwareApplication or Organization JSON-LD block is a good addition once you have real details.
Accessibility#
- Skip link, landmarks (
header,nav,main,footer), oneh1per page and ordered headings. - Visible focus rings in teal (bright teal on dark bands).
- Tabs follow the ARIA tabs pattern; toggle buttons use
aria-pressed; the menu usesaria-expanded. - Status is never shown by color alone: each pill has a symbol and a word, and charts have text labels and
aria-labelsummaries. - Live regions announce result counts, overlap text and estimates; errors are inline and linked with
aria-describedby. - Scrollable tables are keyboard-focusable; reduced motion turns off reveals, the pulse and transitions.
When editing, keep one h1 per page, label every new input and keep body text at the slate tokens or darker.
Building and deploying#
pnpm build writes a static site to out/ (output: "export" in next.config.ts):
- Vercel: import the repository; no settings needed.
- Netlify / Cloudflare Pages: build command
pnpm build, publish directoryout. - Any static host: upload the contents of
out/.
To serve from a sub-folder, build with DEMO_BASE_PATH=/compliance pnpm build; next.config.ts passes it to
basePath and NEXT_PUBLIC_BASE_PATH, which asset() uses. pnpm build:demo does this for the freeframe.dev
demo (demo/, which you can delete).
Tailwind scans the source files for class names; keep each class name whole (not built from pieces such as
`bg-${color}`), or it won't be generated.
To use server features (Route Handlers, Server Actions), remove output: "export" and images.unoptimized from
next.config.ts and deploy to a Node-capable host such as Vercel.
TypeScript setup#
package.json installs @typescript/native (TypeScript 7, which provides tsc) and typescript as an alias of
@typescript/typescript6, because typescript-eslint and parts of the Next.js toolchain still need the TypeScript 6
API. When typescript-eslint supports TypeScript 7, replace both with typescript@7. eslint.config.mjs pins
settings.react.version, because eslint-plugin-react can't detect it under ESLint 10.
Troubleshooting#
- Dates, the year or the office status show the written text for a moment. That is the static build; the browser fills in today's values right after the page loads. If they never change, check the browser console for errors.
- A new class has no effect. Class names must appear as whole strings in the source; restart
pnpm devif you added a new file Tailwind hasn't seen. - The assessment opens on the results. Answers are saved; use "Start over", or remove
wardline:assessment:v1. - Demo slots are on the wrong days. Dates come from the visitor's clock in Eastern time; check
?now=isn't in the URL, and editholidaysFor()insrc/lib/dates.tsfor your own office closures. - Counts on the frameworks page changed. They come from
CONTROLS; check the letters on any control you edited. - Report dates don't match your real report. Set
KEEP_DEMO_CURRENTtofalseand fillFIXED_REPORTS. - Links or images break under a sub-folder. Build with
DEMO_BASE_PATHset, and useasset()for files inpublic/andnext/linkfor pages.
License#
Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not
be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and
at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Anek Latin,
Source Sans 3 and Roboto Mono are included under the SIL Open Font License (see src/fonts/). Wardline, its
customers, integrations, subprocessors and people are fictional.