Files

Arizona Institute for Autism Astro Site

Static Astro + TypeScript migration of the public Arizona Institute for Autism website.

Quick Start

Run commands from this directory:

cd www
npm install
npm run dev

Astro prints the local development URL, normally:

http://localhost:4321

The development server watches source files and refreshes the browser after edits. Stop it with Ctrl+C.

Environment Setup

Requirements:

  • Node.js 22.12 or newer (see package.json); the Nix shell supplies Node.js 24
  • npm

This repository also has a Nix development shell at the repository root:

cd ..
nix --extra-experimental-features "nix-command flakes" develop
cd www
npm install

Local Playwright environment (Linux)

From the repository root, enter the opt-in browser shell:

nix --extra-experimental-features "nix-command flakes" develop .#browser
cd www
TMPDIR=/tmp npm run build
TMPDIR=/tmp node tools/playwright-smoke.mjs

The existing flake.lock pairs the Nix Playwright driver and Chromium/headless shell. PLAYWRIGHT_MODULE points directly to that driver; PLAYWRIGHT_BROWSERS_PATH selects its matching browsers. Fontconfig supplies Latin/Arabic fallback fonts. The ordinary development shell and npm dependencies remain unchanged. This browser shell is offered on Linux; the existing Darwin development shell is unchanged. Do not run npx playwright install or mix an unrelated npm Playwright version with these browser revisions.

The smoke test starts Astro preview and Chromium in the same execution on an ephemeral 127.0.0.1 port. It blocks external and non-GET/HEAD requests, loads the real Spanish privacy and Library pages, verifies staging robots/title/H1, captures two full-page screenshots in a new /tmp/aia-playwright-smoke-* directory, and closes browser/preview. It does not submit forms or exercise production APIs. Success establishes browser transport only, not Phase 8 visual or human acceptance.

Use the Phase 8 guarded browser procedure for full review. Entering the Nix shell does not repair an already-running remote MCP transport or relax sandbox permissions. If the autonomous environment cannot access the Nix store, trusted certificates, sockets or browser runtime files, run the same smoke test from the host terminal. Do not disable TLS verification or bypass the sandbox. The Nix environment needs host validation before browser availability can be claimed.

Copy the example environment file when testing the like/view counter or staging SEO behavior:

cp .env.example .env

The default values are:

PUBLIC_AIA_API_BASE=https://api.azinstitute4autism.com
PUBLIC_COUNTERS_MODE=off
PUBLIC_COUNTERS_LIVE_APPROVED=false
PUBLIC_ALLOW_INDEXING=false

The site still renders when the AIA API is unavailable. Migration and staging builds emit noindex,nofollow by default. PUBLIC_ALLOW_INDEXING=true enables production publication policy, including per-page noindex exceptions. Exercise that mode locally with PUBLIC_ALLOW_INDEXING=true npm run build; this does not deploy anything or change environment files. Run npm run build afterward to restore staging output. Only explicitly authorized production deployments should publish indexing-enabled artifacts.

Ordinary local builds keep counters off. npm run deploy deliberately and non-overridably builds the production artifact with live counters, the approved API origin and PUBLIC_COUNTERS_LIVE_APPROVED=true. Article pages post one first-party view per real navigation; Library cards only batch-read statistics. Likes use the existing POST/DELETE API and store only the visitor's chosen slug set in localStorage. No view cookies or visitor identifiers are created. Run npm run test:counters, then build in live mode and run npm run verify:counters -- --browser=chromium (or firefox) for a fully intercepted browser regression that cannot write production counters.

View And Build

Development Server

Use this while editing:

npm run dev

Useful development URLs include:

/
/aba-therapy
/autism-evaluations
/learner-social-club
/client-consultation
/library
/es
/es/library
/ar/library

Static Build

Generate the static site with staging indexing protection:

npm run build

The autonomous Codex sandbox denies /etc/hosts, so its Node runtime cannot resolve localhost. Use this equivalent validation command only inside that sandbox:

npm run build:sandbox

Build output is written to:

dist/

Preview Production Output

After a successful build:

npm run preview

Astro prints the preview URL, normally http://localhost:4321.

Validate Before Committing

npm run build
npm run audit:links
npm run audit:images
npm run audit:blog

The link audit reads dist/ after a successful build. If rendered output is unavailable, it audits generated source routes, relative and root-relative Markdown links, and public assets instead. It writes:

reports/broken-links.md

Editing Content

Normal content editing happens in Markdown:

src/content/pages/en
src/content/pages/es
src/content/pages/ar
src/content/blog/en
src/content/blog/es
src/content/blog/ar
src/content/authors/en
src/content/authors/es
src/content/authors/ar

Page and post frontmatter is validated by src/content.config.ts.

Example:

---
title: "Example Article"
description: "Short search and social description."
slug: "example-article"
canonical: "https://www.azinstitute4autism.com/library/example-article"
lang: "en"
translationKey: "example-article"
featuredImage: "/assets/images/example.webp"
alt: "Descriptive image alt text"
date: "2026-06-07"
author: "rula-diab"
category: "Library"
tags:
  - ABA Therapy
draft: false
---

Article content goes here.

Set draft: true while preparing unpublished content.

Display Headings and Social Metadata

Pages and articles may supply an optional plain-text displayH1 in frontmatter:

title: "The existing SEO title"
displayH1: "The source-supported visible heading"

The override changes only the visible H1. It does not change the document or social title, article cards, navigation labels, or related-post text. Empty, whitespace-only and non-string overrides fail content validation. Without an override, articles keep title, regular pages keep their existing service/page heading mappings and title-derived fallback, and homepage/Library components keep their existing headings. Homepage heading markup stays intact unless a plain-text override is explicitly supplied. Library template SEO titles remain unchanged. Front Matter CMS exposes the optional field for pages and blog posts.

Existing featured images and page banner fallbacks supply og:image and twitter:image. Relative paths resolve against the public origin in src/data/site.json, never a preview host or an external canonical. Existing alt text supplies both image-alt metadata tags only when it describes the selected featured image; no descriptions are invented for banner fallbacks. Pages without a selected image omit image tags and retain the summary card; image pages retain summary_large_image.

og:locale is omitted: the content languages en, es and ar do not establish the regional language_TERRITORY values required by the Open Graph protocol. Regional targeting remains a human localization decision. HTML language/direction and hreflang are unchanged.

Run npm run test:seo for isolated real-Astro builds covering heading defaults, overrides, schema rejection, image metadata and shared publication invariants in staging and indexing modes. Fixtures never modify the working content or the project's dist/. See the dated Phase 4A section of reports/migration-summary.md for source evidence.

Publication, Canonicals, and Sitemap

src/utils/publication-policy.ts owns route eligibility, normalized public URLs, canonical validation, robots values, and sitemap eligibility. src/utils/publication.ts loads the Astro collections into that manifest. src/pages/[...slug].astro generates every content route from eligible entries, including home and Library indexes; their existing presentation components are under src/components/pages/. Shared SEO and the generated sitemap use that same manifest. The route and source-link audits also reuse the policy.

  • draft: true means unpublished: no generated HTML route in either build mode. The Arabic homepage /ar remains a draft; published Arabic Library routes remain available. Navigation does not link to the unpublished homepage.
  • noindex: true keeps a published route accessible, emits noindex,follow in indexing-enabled builds, and excludes it from the sitemap. It defaults to false. Staging always overrides this with noindex,nofollow.
  • An omitted canonical gets the normalized public URL from site.json and its local route. A supplied local canonical must match that route. Invalid canonicals, duplicate canonical targets, and route collisions fail validation.
  • Verified syndicated articles use their existing canonical field for the external publisher URL. External-canonical routes are excluded from the sitemap even when they are not marked noindex. Do not use canonical values to create local routes or sitemap locations.
  • src/pages/sitemap.xml.ts generates dist/sitemap.xml on every build. Staging emits an empty sitemap and does not advertise it in robots.txt. Indexing-enabled builds include only eligible, indexable, self-canonical local routes. There is no hand-maintained public/sitemap.xml.
  • npm run generate:sitemap is a compatibility command that runs the Astro build; it no longer writes a separate public sitemap from frontmatter strings.

The verified noindex exceptions are /schedule-consultation, /employee-portal, /library/community-highlight-meet-rula-diab, and /library/new-aia-scottsdale-office. The last two also retain their verified external canonicals. The dated evidence is in reports/migration-summary.md.

Run npm run test:publication for policy tests and isolated builds of the actual application in staging and indexing-enabled modes. Fixtures test draft content, draft home/Library indexes, missing and external canonicals, and generated head/sitemap consistency without modifying the working site's output. The policy is TypeScript; Node audit/test commands use --experimental-strip-types for compatibility with the declared Node 22.12 minimum. Dependencies are unchanged.

After publication changes, build and run npm run audit:routes -- --offline before npm run audit:routes -- --check. This creates a newly dated local reconciliation using saved production observations, preserving earlier evidence. Saved rendered metadata is specific to its build mode: after restoring the normal staging build, reconcile that output again before checking it.

Use normal Markdown links for links within prose:

Read the [ABA therapy guide](/aba-therapy) for more information.

Prose links render as the live site's orange inline links. Do not place an ordinary Markdown link on its own line merely to make it look like a button.

Use .mdx and the Button component only for a source-verified call to action:

import Button from '../../../components/Button.astro';

<Button href="/client-consultation">Request an Appointment</Button>
<Button href="/services" variant="outlined">Explore Services</Button>
<Button href="https://example.com" target="_blank">External Action</Button>

Available Button variants are filled (default), outlined, and light. Available sizes are default and small. A _blank target automatically adds rel="noopener noreferrer".

Normal Markdown links remain preferred for ordinary page and article content.

Astro automatically generates an ID for each Markdown heading. In .md files, when a section needs a stable, editorially controlled fragment link, add an explicit ID with Sätteri heading-attribute syntax:

## Accepted Insurance Carriers {#accepted-insurance-heading}

Link to it with the resulting fragment URL:

[View accepted carriers](/insurance#accepted-insurance-heading)

Keep explicit IDs unique within the page, lowercase, and hyphenated. Treat a published ID as permanent so inbound links do not break when heading text is edited. The installed MDX parser rejects this shorthand, independently of directive parsing. In .mdx files, use an explicit HTML heading instead:

<h2 id="accepted-insurance-heading">Accepted Insurance Carriers</h2>

Markdown Directives

Sätteri directive parsing is disabled. The content corpus has no intentional container (:::name), leaf (::name), or text (:name) directives, and no directive transformation plugins are registered. Enabling parsing silently removed literal text such as :1 in 1:1, :00 in times, and :Plan after an adjacent colon. Heading attributes remain enabled.

Run npm run test:markdown after changing the Markdown/MDX configuration. It builds temporary fixtures and the affected content through the actual Astro configuration, checking ratios, punctuation, times, URLs, code, and explicit heading IDs. It makes no production requests and leaves no fixture routes in the site. Any future directive feature needs compatible rendering and must preserve these literal-content checks.

Front Matter CMS

The project includes frontmatter.json for the Front Matter CMS VS Code extension. Open the www folder as the editor workspace so its content folders and media paths resolve correctly.

Edit The Homepage

The English and Spanish homepages are driven from the structured home: frontmatter block in:

src/content/pages/en/index.md
src/content/pages/es/index.md

HomePage.astro renders those named sections in order. Use the section names as the editing surface:

hero
servicesIntro
benefits
skills
insurance
esa
hsaFsa (optional; English only where published)
financialHelp
process
director
testimonials
logos (optional)

Keep homepage copy in those named blocks so the page remains easy to scan in Front Matter CMS and VS Code. src/content.config.ts validates the structure. hsaFsa uses heading, body, image, and imageAlt; omit the whole block where no section is published. It renders between ESA and financial help. logos.items holds file/alt pairs after testimonials. Process steps accept an optional href; use an existing intake route and section ID in the same language. Steps without a destination remain plain content.

Edit The Team Page

The English Team page uses the validated team: frontmatter block in src/content/pages/en/team.md. That block is the single source for the intro, ordered clinical groups, ABA care team, member names/roles/images, and careers content. The Markdown body contains no second roster. Update the structured frontmatter rather than hardcoding people in TeamPage.astro; image filenames refer to public/assets/images.

Images And Downloads

Store content-referenced assets in:

public/assets/images
public/assets/downloads
public/assets/media

Reference public assets from Markdown using root-relative paths:

![Descriptive alt text](/assets/images/example.webp)

Use src/assets/images for images imported directly by Astro components and processed by Astro.

Components And Styling

Reusable page sections are in:

src/components
src/layouts

Global styles and design tokens are in:

src/styles/variables.css
src/styles/global.css
src/styles/rtl.css

Navigation, services, redirects, and site details are in:

src/data

Adding Content

Add An English Blog Post

  1. Create src/content/blog/en/post-slug.md.
  2. Add valid frontmatter and article Markdown.
  3. Place referenced images in public/assets/images.
  4. Run npm run dev and view /library/post-slug.
  5. Run the production validation commands before committing.

Add A Translation

Use the same nonempty translationKey across genuine translated entries in the same collection. The key is an explicit equivalence declaration, not a URL or an entry reference. Slugs may differ; similar slugs alone never create a relationship. Do not change keys to manufacture translation pairs.

src/content/blog/en/post-slug.md
src/content/blog/es/post-slug.md
src/content/blog/ar/post-slug.md

The URLs become:

/library/post-slug
/es/library/post-slug
/ar/library/post-slug

Arabic routes automatically render with lang="ar" and dir="rtl".

The shared publication manifest supplies both the language switcher and SEO alternates. A translation set needs at least two eligible members. Drafts, unpublished placeholders, noindex pages, and external-canonical pages do not participate. Mark placeholders draft: true; /ar remains unpublished. Author records have no generated routes yet and cannot participate.

One member per language per collection/key is allowed, including draft members; duplicates and blank/whitespace-padded keys fail validation. A lone key is valid and emits no alternate: it does not assert that a particular missing language exists. Dangling graph references, wrong-family targets, ineligible targets, and nonreciprocal links fail validation. The rendered-output regression checks also fail if an alternate target has no generated HTML file.

Indexing-enabled builds emit the same absolute hreflang links on every member, including its own language. x-default points to the eligible English member only; sets without one omit it. Staging suppresses hreflang while retaining the eligible language menu and global noindex,nofollow. Unpaired/excluded pages retain their current-language label without an empty interactive menu.

Run npm run test:publication, npm run build, and PUBLIC_ALLOW_INDEXING=true npm run build to validate both modes. Finish with npm run build to restore staging. See reports/migration-summary.md for Phase 3B's exact route sets, evidence, and route-audit refresh commands.

Do not publish unreviewed machine translations. Do not invent translations; substantive translation changes require human language review.

Edit A Blog FAQ

Posts with live-style FAQ sections use .mdx and import:

import FAQAccordion from '../../../components/FAQAccordion.astro';

Edit the component's label, heading, and items data in the post. Keep answer and answerHtml consistent. JSON-LD derives its answer text from the actual visible answerHtml when supplied, otherwise from the rendered answer fallback. Shared FAQ markup, styling, interaction, and schema generation remain in src/components/FAQAccordion.astro, using src/utils/structured-data.ts.

The standalone FAQ page passes its rendered Markdown/MDX slot as sourceHtml to the same component. Its server schema describes the direct H3 questions and following answer nodes that the existing browser code turns into accordions. Runtime-only sources without server content emit no invented FAQ schema. Do not mix items with sourceSelector. Duplicate questions, multiple schema producing accordions on one page, empty answers and conflicting canonical props fail validation; consolidate questions into one accordion per page.

Article and FAQ Structured Data

Article JSON-LD uses the existing SEO title, description, featured image, language, date, optional updatedDate, and the exact locale/slug match in the authors collection. Dates use calendar-day precision; article/card bylines format that day in UTC to avoid shifting it on the build host. Missing modification dates are omitted. Missing or ambiguous author references fail validation. No author archive URL or publisher is invented. displayH1 does not change the schema headline. No new frontmatter fields are required.

BlogPosting and FAQPage emission reuse the manifest's sitemapEligible record gate: drafts, noindex records and external-canonical records are excluded. This gate is independent of the build's global indexing switch, so staging can exercise schema while retaining its global noindex, empty sitemap and absent hreflang. The existing site organization block remains unchanged.

These schemas describe the rendered local records; they do not certify that older article dates or FAQ answers have been reconciled with production. Phase 6C drift observations and validation results are recorded in reports/migration-summary.md. Run npm run test:seo and npm run test:publication, and validate both build modes before publication.

Reports And Utilities

For the current migration checkpoint, next Codex phase, ratified decisions, and outstanding cutover blockers, begin with the living session handoff. Update that short handoff at each accepted milestone transition; detailed historical evidence remains in reports/migration-summary.md.

Migration reports are stored in reports/.

Useful commands:

npm run crawl:live
npm run generate:sitemap
npm run generate:redirects
npm run audit:links

The live crawl is optional and may fail in restricted network environments.

Referral CTA

The public /referrals page preserves the existing site's Refer a Client button, linking directly to https://form.jotform.com/231638510219149. The site does not embed a referral form or require Form C. This destination is separate from Form A's 16 allowlisted placements. Legacy-hosted-form privacy, recipient, accessibility and operational acceptance remain the owner's release responsibility.

Forms

As of the owner decision on 2026-10-30, npm run dev and npm run build use live Jotform Form A on exactly 16 approved placements: 10 immediate frames and six deferred bilingual service drawers. No Form A flags are required. Public development-host forms are owner-authorized. This does not authorize cutover.

Use PUBLIC_AIA_FORMS_MODE=off npm run build for an offline artifact, or PUBLIC_AIA_FORMS_MODE=mock npm run dev for synthetic fixtures. Missing/empty mode means live; obsolete isolated-test and other invalid values produce an explicit configuration error. Remove old approval/environment variables from local settings. Indexing and form mode remain independent of counter mode.

See the current Form A guide for the exact registry, safeguards, rollback and owner acceptance requirements. Form C remains inactive. Employee-portal private input retains its own separate gates.

Verify matching output with node tools/phase10-artifact-verify.mjs --mode=staging --forms=live --counters=off; use --mode=indexing for indexing output and --forms=off or --forms=mock for those explicit builds. Production counter artifacts require --counters=live. No actual submissions or production counter writes run in automated tests.

Static releases and employee portal

The Contabo deployment interface is intentionally separate from NixOS changes. Use the release runbook for npm run deploy, dry-run, rollback, the encrypted production portal input and the separate one-time employee-resource installer. The server-side installer is a root-only POSIX shell script using NixOS base tools; it does not require Node.js on Contabo. Ordinary releases never copy employee documents into dist; protected staff and management resources remain under server-controlled Basic Auth namespaces. After a successful unlock, the page keeps that role's code only in memory and uses it for same-origin authenticated document fetches. The response is downloaded as a local Blob, so employees do not receive a second browser authentication prompt. Cross-role requests are stopped before credentials are sent; direct resource URLs still require nginx authentication. Codes are never put in URLs or browser storage. The accepted interim portal is explicitly non-PHI.

Google Ads consultation-form conversion handoff

The Google Ads conversion ID, label, and privacy-gated Astro integration instructions are in reports/google-ads-consultation-conversion.md. This is documentation only: do not install Google tags or conversion snippets on health-service pages or turn on enhanced conversions until AIA's privacy/HIPAA and advertising-policy review explicitly approves the complete data flow. Any future measurement needs separately approved success and privacy semantics; the former API-success premise is historical and no tracking is active.

Troubleshooting

astro: command not found

Dependencies are not installed:

npm install

npm TLS or certificate errors

Do not disable npm certificate verification. Run npm install from a normal host terminal or trusted development environment with working CA certificates.

Build first:

npm run build
npm run audit:links
npm run audit:images
npm run audit:blog

A content page is missing

Check:

  • its Markdown file is in the correct language/content folder
  • draft is false
  • slug is unique within that language and content type
  • frontmatter satisfies src/content.config.ts