Overview#
Slatework is a multi-page marketing website for a recruiting software company: an applicant tracking system (ATS) for companies with 50 to 1,000 employees. The fictional Slatework, Inc. is based in Lexington, Kentucky, and works in Eastern time. This is the Nuxt version: Nuxt 4, Vue 3 single-file components, TypeScript and Tailwind CSS v4, generated as a fully static site. It has the same pages, design and interactions as the HTML version. This guide covers version 1.0.0. The template page, with a link to the live demo, is at freeframe.dev/templates/slatework.
The look is corporate and structured: white paper with hairline grids, a deep indigo brand color, a marigold highlight, Hubot Sans headings and Fira Sans body text, with a full dark theme.
Quick start#
-
Unzip the download and open a terminal in the folder.
-
Install and start the development server:
shpnpm install pnpm dev -
Visit
http://localhost:3000. Edit files inapp/and the page reloads.
Requirements: Node.js 24 and pnpm. pnpm install also runs nuxt prepare, which writes the types and the ESLint
config to .nuxt/. The fonts are bundled, so nothing loads from the internet.
| Command | What it does |
|---|---|
pnpm dev |
Development server with hot reload |
pnpm generate |
Static site in .output/public/ |
pnpm build |
Node.js server build in .output/ (pages still prerendered) |
pnpm preview |
Serve the last build locally |
pnpm lint |
ESLint |
pnpm typecheck |
nuxt prepare and vue-tsc |
Pages#
| Route | File | Purpose and notable features |
|---|---|---|
/ |
app/pages/index.vue |
Home: live pipeline board with funnel and projected time to hire, customer logos, six feature pillars, interactive scorecard, results, testimonials, call to action |
/product |
app/pages/product.vue |
Product tour: job board picker and posting preview, stage rules, weighted scorecard, panel scheduler, offer pay band check, SVG report charts |
/pricing |
app/pages/pricing.vue |
Monthly / yearly switch, three plans, cost and cost-per-hire calculator, comparison table, billing FAQ |
/customers |
app/pages/customers.vue |
Industry and company size filters, eight stories with logo marks, before-and-after charts and quotes |
/integrations |
app/pages/integrations.vue |
Search, category filters with counts, 22 integrations with drawn marks, detail dialog, API example |
/security |
app/pages/security.vue |
Controls, data retention example, voluntary self-ID reporting, subprocessors table |
/resources |
app/pages/resources.vue |
Most-read list, rolling monthly webinars with registration, six dated articles with a topic filter |
/demo |
app/pages/demo.vue |
Validated demo request form, plan preselected from pricing, success panel with a reference and proposed meeting time |
| 404 | app/error.vue |
Links to the main pages and a drawn board illustration (generated as 404.html) |
Project structure#
slatework/
app/
app.vue <NuxtLayout><NuxtPage /></NuxtLayout>, pages keyed by path and query
error.vue 404 page
router.options.ts writes query strings the way the HTML version does
layouts/default.vue skip link, header, page, footer, live region (#live-region)
pages/ one file per page (index, product, pricing, customers, integrations, security, resources, demo)
components/
SiteHeader.vue header, main menu, phone menu, header shadow on scroll
ThemeToggle.vue light/dark switch
SiteFooter.vue footer columns, contact details, copyright year
LogoMark.vue, ArrowIcon.vue the logo mark and the arrow icon
PipelineBoard.vue hero pipeline board (drag and drop, Move menu, stats)
InterviewScorecard.vue weighted interview scorecard (home and product)
JobBoards.vue product step 01: job boards and posting preview
PanelScheduler.vue product step 04: panel scheduler
OfferCheck.vue product step 05: offer pay band check
CaseCard.vue one customer story with its chart
IntMark.vue an integration's drawn mark
composables/
useNow.ts the page's clock: now, today in Eastern time, the visitor's zone
useFormValidation.ts form validation and success panels
usePageSeo.ts per-page title, description, canonical and Open Graph tags
useCurrentPage.ts aria-current for menu links
data/
site.ts content and settings: candidates, plans, customers, integrations, posts...
nav.ts main menu items
utils/
pipeline.ts board state, saved-state cleaning, counts, funnel, time to hire
schedule.ts free/busy intersection and slot proposals
webinars.ts the rolling webinar dates
time.ts Eastern "today", time zone math, business days, ?now= preview
format.ts money, plurals, initials, reference codes
safeStorage.ts localStorage with an in-memory fallback, storage keys (KEYS)
url.ts reading and writing URL parameters
announce.ts screen reader announcements
plugins/
reveal.client.ts scroll reveal animation
same-page.client.ts links to the page already showing start it afresh, like a page load
stable-build.server.ts reproducible builds
assets/css/main.css Tailwind import, fonts, color tokens (light and dark), component classes
assets/fonts/ Hubot Sans and Fira Sans (Latin woff2) with their OFL license texts
public/img/ favicon.svg, og-image.png (social image), og-image.svg (its source)
nuxt.config.ts base path, head defaults, theme script, SITE_URL, prerendered routes, Tailwind
eslint.config.mjs, tsconfig.json, pnpm-workspace.yaml
package.json, README.md, DOCS.md, CHANGELOG.mdCustomizing#
Brand and colors#
Colors are CSS variables in app/assets/css/main.css: :root holds the light theme and .dark the dark theme.
@theme inline maps them to Tailwind names, so bg-brand, text-ink-soft or border-line follow the theme.
| Token | Light | Used for |
|---|---|---|
--paper |
#ffffff |
Page and card background |
--canvas |
#f4f4f1 |
Alternate bands, board columns |
--line / --line-strong |
#dddfd7 / #878b7f |
Hairlines / inputs and chart axes |
--ink, --ink-soft, --ink-faint |
#15171e, #454955, #595d69 |
Text |
--brand / --brand-strong |
#3a3fd0 / #2b2fa8 |
Buttons, links, focus |
--applied, --screen, --interview, --offer, --hired |
stage colors | Board dots, chips, funnel bars |
Fixed colors that don't change with the theme (the night band, marigold) are in the @theme block.
To change the brand color, edit both themes:
:root {
--brand: #0f6b5c;
--brand-strong: #0b5649;
--brand-soft: #e3f3ef;
--brand-line: #b9e0d6;
}
.dark {
--brand: #7fd6c3;
--brand-strong: #a6e5d7;
--brand-soft: #12302a;
--brand-line: #24584d;
}The logo mark (app/components/LogoMark.vue), public/img/favicon.svg, public/img/og-image.svg and the
integrations page hero art use #3a3fd0 directly; change them too. theme-color is set in nuxt.config.ts.
Typography#
Headings use Hubot Sans (variable width, set wider with font-stretch: 112%) and body text Fira Sans. Both
are self-hosted: the @font-face rules at the top of app/assets/css/main.css load the Latin woff2 files in
app/assets/fonts/ under their real family names, and the @theme block sets them as --font-display and
--font-sans:
@font-face {
font-family: "Hubot Sans";
font-style: normal;
font-weight: 500 800;
font-stretch: 100% 125%;
font-display: swap;
src: url("../fonts/HubotSans-latin.woff2") format("woff2");
}To swap a font, put its woff2 files in app/assets/fonts/, change the @font-face rules and the family name in
--font-display or --font-sans. Hubot Sans draws its zero with a bar; that is the font's design.
Content#
Page text is in the page files (app/pages/*.vue) and the components. Everything interactive reads from
app/data/site.ts. The main structures:
| Constant | What it holds |
|---|---|
STAGES |
The five pipeline stages, in order: applied, screen, interview, offer, hired |
REQ |
The requisition shown on the board (title, openings: 2) |
STAGE_BASE |
Candidates in each stage not shown as cards: applied: 38, screen: 9, interview: 3, offer: 0, hired: 0 |
CANDIDATES |
Board cards: id, name, source, age (days since applying), starting stage, avatar hue, optional score |
STAGE_REMAINING, TIME_TO_HIRE_TARGET |
Time-to-hire projection (offer: 4, hired: 0; target 30 days) |
COMPETENCIES, RATING_LABELS, RECOMMENDATIONS |
Scorecard competencies, weights and default ratings; score thresholds |
BOARDS, POSTING_LOCATIONS |
Job boards with 30-day price and typical applicants; posting locations |
INTERVIEWERS, WORK_HOURS, SCHEDULE_*, DISPLAY_ZONES |
Scheduler panel, working hours, busy blocks and time zones |
OFFER_BAND |
Pay band, slider range and approval rules for the offer check |
PLANS, ANNUAL_DISCOUNT, MIN_EMPLOYEES, MAX_EMPLOYEES, ENTERPRISE_HINT, DEFAULT_PRICING |
Pricing |
INDUSTRIES, SIZES, CASES |
Customer story filters and stories |
INT_CATEGORIES, INTEGRATIONS |
Integration categories and directory entries |
RETENTION, DEFAULT_RETENTION |
Data retention choices on the security page |
TOPICS, POSTS |
Article topics and articles (fixed past dates) |
WEBINAR_* |
Webinar rule, time and topics |
PERSONAL_DOMAINS, COMPANY_SIZES, DEMO_LEAD_DAYS, DEMO_TIMES |
Demo form checks and proposed meeting time |
Example: add an integration to INTEGRATIONS. Give it a unique id, an existing category, a tile color and a
small mark drawn in white inside a 40×40 box:
{ id: "shiftnote", name: "Shiftnote", category: "messaging", bg: "#0f766e", setup: "2 minutes", direction: "One-way",
blurb: "Post interview reminders to the hiring team channel.",
syncs: ["Interview reminders", "Scorecard due alerts"],
mark: '<circle cx="20" cy="20" r="9" fill="#fff"/>' },Category counts, search and the detail dialog pick it up automatically. Update the count in the page's description
(usePageSeo in app/pages/integrations.vue) if you want it exact.
Example: add a customer story to CASES with industry set to a key of INDUSTRIES and size to s, m or
l. metrics[0] draws the before-and-after chart. The home page logo row and testimonials are written into
app/pages/index.vue; edit them there by hand if you change customers.
mark strings are trusted SVG markup rendered with v-html (CaseCard.vue, IntMark.vue); only put your own
drawings there, never text that visitors can enter.
Images and artwork#
All artwork is inline SVG drawn for this template: feature pillar icons, report charts, the security and 404
illustrations, and the integration and customer marks (in app/data/site.ts, as mark strings). There are no
photos. If you add photos, keep the existing aspect ratios: avatars are square, case study marks 40×40. Files in
public/ need the base path when you link them from a template: :src="`${$config.app.baseURL}img/photo.jpg`".
The social image is public/img/og-image.png (1200×630), rendered from og-image.svg. After editing the SVG,
regenerate the PNG with any SVG renderer. The favicon is public/img/favicon.svg.
Navigation, header and footer#
The main menu is NAV in app/data/nav.ts; app/components/SiteHeader.vue renders it in the desktop menu and the
phone menu, and marks the current page with aria-current="page" (useCurrentPage()). The footer columns are in
app/components/SiteFooter.vue. To add a page, create app/pages/<route>.vue with a <main id="main"> and a
usePageSeo({ title, description, page }) call, add the route to routes in nuxt.config.ts (so it is
prerendered), then add it to NAV and the footer. Internal links are <NuxtLink to="/pricing">, which adds the base
path.
Dark mode#
The theme button (app/components/ThemeToggle.vue) toggles the dark class on <html> and saves the choice as
slatework:theme. A short script in app.head.script in nuxt.config.ts applies the saved choice, or the visitor's
system setting, before the page paints. To make light the default regardless of the system setting, remove the
matchMedia(...) check from that script. To remove dark mode, delete <ThemeToggle /> from the header and the
theme part of the script; the .dark variables can stay.
Features#
Dates and the static build#
The generated pages can't know the visitor's date, time zone, URL parameters or saved choices, so everything that
depends on them is filled in once the page is mounted: components read window.location, Date and
localStorage in onMounted() or in event handlers, never while rendering. The generated HTML shows the written
defaults (for example the retention dates for November 9, 2026), and the real values follow without a hydration
mismatch. useNow() in app/composables/useNow.ts gives the page's clock (null until mounted).
Hero pipeline board#
app/components/PipelineBoard.vue with the logic in app/utils/pipeline.ts. The board shows the requisition REQ
with five columns from STAGES. Each column's count is STAGE_BASE[stage] plus the cards in it.
- Drag and drop: with a mouse or pen, drag a card anywhere; on touch screens, drag by the card's initials
(
data-grip), so swiping the board still scrolls it. The board scrolls sideways when you drag near its edge. Escape cancels a drag. - Keyboard and screen readers: each card has a "Move" button (
data-move) that opens a menu of stages (role="menu"; arrow keys, Home, End, Escape, Tab closes it). Choosing a stage moves the card, returns focus to its Move button and announces the new count and time to hire. - Funnel: "reached" counts each stage plus every later stage. With the starting data: Applied 61, Screen 19, Interview 7, Offer 2, Hired 1, so Applied to Hired is 1.6%.
- Projected time to hire: the average of
age + STAGE_REMAINING[stage]over cards in Offer and Hired, rounded. Start: Keziah W. (27 + 0) and Esmeralda P. (19 + 4) give 25 days, "5 days under the 30-day target". Moving Ramona D. (age 16) to Offer makes Offer 2 and time to hire 23 days. Then dragging Esmeralda P. to Hired makes Hired 2, "2 of 2 openings filled" and time to hire 22 days. - Moves are saved (
slatework:pipeline); "Reset board" restores the starting data. Moved cards go to the top of their new column.
Interview scorecard#
app/components/InterviewScorecard.vue, on the home page (<InterviewScorecard compact />, without the interview
prompts and the Reset button) and the product page. Each competency in COMPETENCIES has 1–4 rating buttons
(role="radio", arrow keys change the rating). The weighted score is sum(weight × rating) / 100. The defaults
(4, 3, 3, 2, 4 with weights 30, 25, 20, 15, 10) give 3.25, Hire. Rating Product knowledge 4 gives 3.55, Strong
hire; then rating Customer empathy 1 gives 2.65, No hire. Thresholds in RECOMMENDATIONS: 3.5 Strong hire,
2.75 Hire, 2.0 No hire, below that Strong no hire.
Job boards and posting preview#
app/components/JobBoards.vue (/product#sourcing). Checking boards updates the board count, the 30-day cost and
typical applicants. Default: careers page, Hirewire and Openroles, 3 boards, $0, 60 applicants; adding
Talentcast makes $179, 76. The posting preview follows the title, location and pay range fields, and flags a
maximum below the minimum.
Panel scheduler#
app/components/PanelScheduler.vue with the search in app/utils/schedule.ts (/product#scheduling). For each of
the next SCHEDULE_DAYS (5) weekdays after today in Eastern time, it intersects every chosen interviewer's working
hours (WORK_HOURS, 9:00 a.m. to 5:00 p.m. in their own zone) minus their busy blocks, then proposes up to
SCHEDULE_PROPOSALS (3) non-overlapping slots on 30-minute steps, at most two per day. Busy blocks are tied to
business day numbers (0 = the first weekday after today), so the demo never runs out of dates. Times are shown in
the visitor's zone by default; "Show times in" switches between US zones, and each slot lists every interviewer's
local time.
With the default panel (Estelle, Renzo, Vittorio), 60 minutes, on Monday, November 9, 2026, the slots in Eastern time are Tue, Nov 10, 12:30–1:30 PM; Wed, Nov 11, 3:00–4:00 PM; and Wed, Nov 11, 4:00–5:00 PM. Adding Ursula gives Wed 3:00 PM, Wed 4:00 PM and Thu, Nov 12, 1:00 PM.
Offer pay band check#
app/components/OfferCheck.vue (/product#offers). The slider runs from OFFER_BAND.sliderMin to sliderMax.
Inside the band ($68,000–$84,000) only the hiring manager approves ($76,000: "Inside band", 1 approver); below it
the people team is added ($64,000: "Below band", 2); above it the VP and Finance are added ($88,000: "Above band",
3). The rules are OFFER_BAND.rules.
Pricing and calculator#
app/pages/pricing.vue holds one state shared by the billing switches, plan prices and the calculator. Monthly
prices per employee are in PLANS (Essentials $5, Growth $8); yearly billing takes ANNUAL_DISCOUNT (20%) off: $4
and $6.40. Companies under MIN_EMPLOYEES (50) are billed as 50. The calculator shows the monthly cost, the yearly
cost and the cost per hire.
| Inputs | Result |
|---|---|
| 200 employees, Growth, yearly, 40 hires (default) | $1,280 a month, $15,360 a year, $384 per hire, saves $3,840 |
| Same, monthly | $1,600 a month, $19,200 a year, $480 per hire |
| 30 employees, Growth, monthly | $400 a month (billed as 50) |
| 800 employees, Essentials, yearly, 100 hires | $3,200 a month, $38,400 a year, $384 per hire, Enterprise hint shown |
The choice is saved (slatework:pricing). URL parameters override it: ?billing=monthly|annual,
?plan=essentials|growth, ?employees=1–1000, ?hires=1–2000. The "Book a demo with this estimate" link passes
plan and employees to /demo.
Customer stories#
app/pages/customers.vue with each card in app/components/CaseCard.vue. Industry buttons and a company size
select filter CASES; the URL keeps them as ?industry= (a key of INDUSTRIES) and ?size=s|m|l. Financial
services shows 2 of 8; with 201–500 employees, 1 (Pellworth); 501–1,000 employees alone shows 4.
Integrations directory#
app/pages/integrations.vue. Search matches name, description and category; category buttons show counts (HRIS
and payroll 4, Calendars and video 4, Job boards 6, Background checks 2, Assessments 2, E-signature 2, Messaging 2).
The URL keeps ?category= and ?q=. Each card opens a native <dialog> with what syncs; Escape, Close or a click
outside the box closes it and returns focus to the card.
Data retention example#
app/pages/security.vue (/security#retention). Choosing 12, 24 or 36 months (RETENTION) shows the date a
candidate rejected today would be anonymized, and the reminder date a month earlier, worked out from today in
Eastern time. On November 9, 2026, 24 months gives Thursday, November 9, 2028 with a reminder on Oct 9, 2028.
Webinars and articles#
app/pages/resources.vue, with the dates from app/utils/webinars.ts. Webinars follow a rule: the second Thursday
of each month at 1:00 p.m. Eastern (WEBINAR_TIME), the next WEBINAR_COUNT (3) at least WEBINAR_MIN_DAYS (3)
days away; on November 9, 2026 that is Nov 12, Dec 10 and Jan 14. The topic cycles through WEBINAR_TOPICS by
month. Times also show in the visitor's zone. Articles in POSTS keep their real dates and can be filtered by topic
(?topic=); each expands in place.
Menu, header and motion#
The phone menu button (#menu-toggle) opens #mobile-menu (aria-expanded; Escape closes it and returns focus
to the button, and it closes when the window grows to desktop width). The header gets a shadow once the page
scrolls. Sections with reveal fade in once as they scroll into view (app/plugins/reveal.client.ts); with reduced
motion they show immediately, and the card animation is turned off.
Links are client-side navigations. A link to the page already showing (the Pricing menu item on a pricing page
with ?billing=monthly, for example) starts that page afresh at the top, like a page load in the HTML version
(app/plugins/same-page.client.ts and the page key in app/app.vue).
Keeping the demo current#
Nothing dated goes stale. All "upcoming" dates are computed from today in Eastern time (COMPANY.timeZone):
| Content | Rule | Switch |
|---|---|---|
| Interview slots | Next 5 weekdays after today | Busy blocks use business day numbers |
| Webinars | Second Thursday of the month, at least 3 days away | WEBINAR_DATES = null; set an array of "YYYY-MM-DD" strings for fixed dates |
| Proposed demo time | First weekday at least DEMO_LEAD_DAYS (1) after today, at 10:00 a.m. or 2:00 p.m. Eastern |
DEMO_TIMES |
| Retention example | Today plus 12, 24 or 36 months | |
| Copyright year | Current year in Eastern time | SiteFooter.vue |
Articles keep their fixed past dates. For reviews and screenshots, ?now=YYYY-MM-DDTHH:MM (read as Eastern time)
pretends it is another moment; turn it off with ALLOW_NOW_PARAM = false at the top of app/utils/time.ts.
Forms#
All forms validate in the browser with inline messages linked by aria-describedby and aria-invalid on the
field, focus the first problem and then show a success panel. The helper is useFormValidation in
app/composables/useFormValidation.ts. No form sends anything until you connect it.
| Form | Page | Checks | Success |
|---|---|---|---|
Demo request (data-demo-form) |
demo.vue |
Names, work email (format, and not a personal domain from PERSONAL_DOMAINS), company, role, size |
"Thanks, {name}. Your demo request is in.", reference SW- + 6 characters, proposed time in the visitor's zone and in Eastern, next steps |
Webinar registration (data-webinar-form) |
resources.vue |
Name, email | Registration reference WB- + 5 characters and the session time |
/demo also reads ?plan=essentials|growth|enterprise&employees= to preselect the plan and company size, and
?topic=security to tick "Security and reporting".
To connect a form service such as Formspree, send the request in the submit listener in
app/composables/useFormValidation.ts (where it calls onSuccess) and show the success panel only when the
service answers OK:
root.addEventListener("submit", async (e) => {
// ...the checks above, unchanged...
const response = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(root),
headers: { Accept: "application/json" },
});
if (!response.ok) return; // keep the form; show your own error message here
onSuccess(new FormData(root));
sent.value = true;
});With pnpm build (a Node.js server build) you can post to a server route instead: add
server/api/demo.post.ts with export default defineEventHandler(async (event) => { const body = await readFormData(event); ... })
and pass the data to your CRM or email service.
Saved data (localStorage)#
All storage goes through safeStorage in app/utils/safeStorage.ts, which falls back to memory when the browser
blocks localStorage, so the page keeps working for the visit. The keys are in KEYS in the same file.
| Key | Shape | Validation |
|---|---|---|
slatework:theme |
"light" or "dark" |
Anything else is ignored |
slatework:pipeline |
{ "v": 1, "stages": { "ramona": "offer", ... }, "order": ["ramona", ...] } |
cleanPipeline in app/utils/pipeline.ts: unknown candidates or stages are dropped; missing cards keep their starting stage |
slatework:pricing |
{ "billing": "annual", "employees": 200, "plan": "growth", "hires": 40 } |
onMounted in app/pages/pricing.vue: each field checked against its allowed values and range |
Clear them with "Reset board", or from the browser's developer tools (Application → Local Storage).
SEO and sharing#
Each page sets its title, description and Open Graph tags with usePageSeo() from app/composables/usePageSeo.ts,
which also adds a canonical link. URLs are absolute: set SITE_URL to your domain when you build
(SITE_URL=https://www.example.com pnpm generate), so og:image (/img/og-image.png) and the canonical links point
at your site, which most social sites require. The 404 page is marked noindex. Add a public/robots.txt and a
sitemap (by hand, or with a module such as @nuxtjs/sitemap) if you want them.
Accessibility#
- Skip link, landmarks, one
<h1>per page andaria-current="page"in the menus. - Visible focus outlines, AA contrast in both themes, and reduced motion respected.
- The pipeline board has a full keyboard path (Move menu) and announces changes in a live region (
#live-regionin the layout, written byannounce()inapp/utils/announce.ts). - Scorecard ratings are radio groups; filters use
aria-pressed; the integration dialog is a native<dialog>. - Keep one
h1per page, label new inputs, and keep the Move menu if you change the board.
Building and deploying#
pnpm generate writes a static site to .output/public/, with every route listed in routes in nuxt.config.ts
prerendered to <route>.html (pricing.html, served at /pricing) and the 404 page to 404.html:
- Netlify / Cloudflare Pages / Vercel: build command
pnpm generate, publish directory.output/public. - Any static host: upload the contents of
.output/public/. The host should serve/pricingfrompricing.html(most do) and404.htmlfor unknown paths.
To serve from a sub-folder, build with DEMO_BASE_PATH=/hiring pnpm generate; nuxt.config.ts turns it into
app.baseURL, which every link, script and 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. The components use small lookup objects (BAR, TONE) for that.
TypeScript setup#
typescript is installed as an alias of @typescript/typescript6, the TypeScript 6 API that vue-tsc needs.
tsconfig.json only references the configs Nuxt writes to .nuxt/, so run pnpm install (or nuxt prepare)
before pnpm typecheck or pnpm lint.
Troubleshooting#
- New classes have no effect: class names built from pieces aren't generated; write them out in full.
- The board keeps an old arrangement: it's saved in
slatework:pipeline; click "Reset board". - Scheduler shows no slots: the panel has no shared time in the next five weekdays. Check
busyblocks andWORK_HOURSinapp/data/site.ts, or pick a shorter interview. - Dates look a day off: dates are computed in Eastern time. Change
COMPANY.timeZoneinapp/data/site.tsfor another home zone. - A new page shows the 404 page after
pnpm generate: add its route toroutesinnuxt.config.ts. - Saved choices don't persist in a preview frame: some sandboxed frames block storage; the site then keeps state in memory for the visit only.
- A hydration warning after editing a component: values that depend on the clock, the URL or storage must be
read in
onMounted()(or come fromuseNow()), not while the component renders. pnpm lintcan't find.nuxt/eslint.config.mjs: runpnpm installorpnpm typecheckfirst.
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. Hubot Sans and
Fira Sans are included under the SIL Open Font License (see app/assets/fonts/). Slatework, its customers,
integrations, interviewers and people are fictional.