@inizioevoke/veeva-astroclm
v1.1.4
Published
Scaffolding for an @inizioevoke/veeva-astroclm project — an [Astro](https://astro.build)-based framework for building Veeva CLM presentations.
Keywords
Readme
@inizioevoke/veeva-astroclm
Scaffolding for an @inizioevoke/veeva-astroclm project — an Astro-based framework for building Veeva CLM presentations.
Install in current directory
npx --yes @inizioevoke/veeva-astroclm@latestInstall in target directory
npx --yes @inizioevoke/veeva-astroclm@latest [target]Templates
When run, the scaffolder prompts you to pick a template:
- Standard template (default) — includes
GlobalLayout - Left nav template — includes
GlobalLayoutandLeftNavLayout - Top nav template — includes
GlobalLayoutandTopNavLayout - Empty template — no layouts
Skip the prompt with the --template flag:
npx --yes @inizioevoke/veeva-astroclm@latest [target] --template <name>Valid values: standard, left-nav, top-nav, empty.
Project structure
.
├── astro.config.ts Astro config + env schema + pruneBuild integration
├── veeva-config.ts Presentation metadata, slides, binder fields (created via veeva-config)
├── veeva-metadata.js Browser-console script that bulk-updates Veeva PromoMats fields
├── .env.development LOG_LEVEL, WEB_ENV, WEB_SCALING for `astro dev`
├── .env.veeva-client BUILD_TARGET for client builds/deploys
├── .env.veeva-client-training BUILD_TARGET + VEEVA_TRAINING for training builds
├── .env.veeva-evoke BUILD_TARGET + VEEVA_ENV="evoke" for internal builds
├── bin/create.js Scaffolder entry point
├── tasks/
│ ├── page-manager.ts Interactive page scaffolder
│ ├── veeva-config.ts Interactive veeva-config.ts generator/updater
│ └── veeva-deploy.ts Deploys built output to Veeva
├── public/shared/ Static assets served under /shared/
└── src/
├── pages/ Astro route entries (one file per slide)
├── contents/ Per-slide content modules imported by pages
├── layouts/ GlobalLayout, LeftNavLayout, TopNavLayout, ...
├── components/ Project components (content, isi, nav, ui)
├── data/pages.tsx Page registry / nav data
├── lib/{client,server}/ Shared client + server helpers (IVA config lives here)
├── scss/ Shared SCSS (_mixins, _variables, ipads.scss)
└── shared/img/ Shared image assetsPages and contents
Each slide is split in two:
- src/pages/<name>.astro — thin Astro route that imports the content module.
- src/contents/<path>/<name>.astro — the actual slide (layout + markup), paired with a sibling
.scssand.ts.
A page at src/pages/about-moa.astro typically imports from src/contents/about/moa.astro. The page-manager task (below) wires this up automatically.
Development
Start the Astro dev server:
npm run devThe root path / redirects to /home/ (see astro.config.ts).
Dev env lives in .env.development. Useful knobs:
LOG_LEVEL—debug,info,warn,errorWEB_ENV— set toproductionto simulate a production build locallyWEB_SCALING— whentrue, the resize script is included if<ResizeScript />is present in the layout
Creating pages
Manual
Given a page path like about/moa:
- Create
src/pages/about-moa.astro:--- import Page from '@/contents/about/moa.astro'; --- <Page /> - Create
src/contents/about/moa.astro:--- import GlobalLayout from '@/layouts/GlobalLayout/GlobalLayout.astro'; import './moa.scss'; --- <GlobalLayout> <!-- slide content --> <script> import './moa'; </script> </GlobalLayout> - Create empty
src/contents/about/moa.scssandsrc/contents/about/moa.ts. - If a veeva-config.ts exists, add a corresponding entry to
presentation.slides(see Veeva config below).
Interactive
npm run page-managerPrompts for the page path and layout, writes the four files above, and offers to add a slide entry to veeva-config.ts.
Bulk scaffold multiple pages at once by creating _contents.yml in the project root with a list of paths (see _contents.yml.example) before running the task.
As an alternative, Claude Code users with the
evoke-page-managerskill installed can ask Claude to create the page — the skill drives the same flow.
Building content
Slide content lives in src/contents/. A slide is an Astro component that wraps a layout and renders markup, scripts, and styles. Pair each slide with these building blocks.
Images — Image.astro
A thin wrapper around Astro's getImage() that fills in width/height from the source and defaults loading="eager" (Veeva slides should not lazy-load). Use for every image imported from src/shared/img/ or a sibling asset directory.
---
import Image from '@components/ui/Image.astro';
import hero from './hero.png';
---
<Image src={hero} width={800} alt="Hero" />Props: src (required ImageMetadata), width, height (one is enough — the other is derived from aspect ratio), format, quality (default 80), alt, plus any <img> attributes.
Internal links — Link.astro
Routes to another slide. In Veeva builds it emits data-gotoslide (the Veeva slide zip name) and an href="#"; in web builds it emits the real path. External URLs (http(s)://…) and shared/… paths are passed through unchanged. aria-current="true" is set automatically when the link points to the current page.
---
import Link from '@components/ui/Link.astro';
---
<Link href="about/moa" data-evotrk={{ type: 'button', name: 'learn_more' }}>Learn more</Link>
<Link href="https://example.com">External</Link>Props: href (a PageHref from src/data/pages.tsx, or any string), optional data-evotrk (object form — serialized via @inizioevoke/veeva-astroclm-core), plus any <a> attributes.
Nav links — NavLink.astro
A Link variant for navigation menus. Adds data-current="true" when the link belongs to the currently active section — useful for highlighting a parent nav item on child routes. Match the current section via hasCurrent:
---
import NavLink from '@components/ui/NavLink.astro';
---
<NavLink href="about/moa" hasCurrent="/about/">About</NavLink>
<NavLink href="data" hasCurrent={/^\/data\//}>Data</NavLink>
<NavLink href="resources" hasCurrent={(path, href) => path.startsWith('/resources/')}>Resources</NavLink>Props: everything Link accepts, plus hasCurrent (string prefix | RegExp | (pathname, href) => boolean).
Cross-presentation links — VeevaLink.astro
For jumping to a slide in another Veeva presentation. Emits a data-gotoslide with the slide name and (optionally) the target presentation ID — this is the only way to leave the current presentation inside Veeva. The href is always #; this component has no web-build fallback.
---
import VeevaLink from '@components/ui/VeevaLink.astro';
---
<VeevaLink slide="safety_main" presentation="my_other_presentation">Safety info</VeevaLink>Props: slide (required), presentation (optional — defaults to the current one), optional data-evotrk, plus any <a> attributes.
Page registry — src/data/pages.tsx
Pages is a const array that defines each slide's title (JSX) and href. Its type powers the PageHref autocomplete used by Link / NavLink, and the helpers (getPageElement, getPageComponent, PageComponents) let nav components render titles consistently across slides. Add new entries here whenever you add a page that nav or breadcrumbs need to know about.
Veeva config
veeva-config.ts describes the presentation to Veeva: dimensions, iOS resolution, platform (Salesforce or Vault), disabled actions (Swipe, Zoom, etc.), binder fields, and the slide list. tasks/veeva-deploy.ts and the build pipeline both consume it.
Creating it
npm run veeva-configOn first run, it prompts for product name, presentation name/ID, IVA dimensions (1024 × 768, 1366 × 1024, 1376 × 1032), iOS resolution, target platform, and which Veeva features to disable. It then writes veeva-config.ts to the project root with one slide entry per file in src/pages/.
Keeping it in sync
Re-run the same command to audit veeva-config.ts against src/pages/. It reports:
- Missing slides — pages in
src/pages/with no entry inslides - Extraneous slides — entries in
slideswhosepathhas no matching page
You can add missing slides, and remove or comment out extraneous ones.
Shape
export default function ({ veevaEnv = 'client', isTraining = false }: IVeevaConfigArgs = {}): IVeevaConfig {
return {
dimensions: { width: 1366, height: 1024 },
presentation: {
externalId: 'PRD_3K9BW12X',
presentationId: 'my_presentation',
product: 'My Product',
name: 'My Presentation',
country: 'United States',
crmTarget: 'Salesforce',
// ...binderFields
slideSettings: {
mediaType: 'HTML',
disableActions: ['Swipe', 'Zoom'],
iosResolution: 'Default For Device'
},
slides: [
{ path: 'home', title: 'Home', externalId: formatExtId('3K9BW12X') }
]
}
};
}The exported function receives { veevaEnv, isTraining } from the deploy task and conditionally shapes the config (e.g. appending _TRN to IDs for training, swapping binder fields for evoke builds).
As an alternative, Claude Code users with the
evoke-veeva-config-managerskill installed can ask Claude to create or sync the config.
Building
Three build targets are available, each binds to a different .env.* file via --mode:
| Script | Mode | Env file | Purpose |
|---|---|---|---|
| npm run build-client | veeva-client | .env.veeva-client | Production build for the client's Veeva org |
| npm run build-client-training | veeva-client-training | .env.veeva-client-training | Training variant (VEEVA_TRAINING=true) |
| npm run build-evoke | veeva-evoke | .env.veeva-evoke | Internal Evoke build (VEEVA_ENV=evoke) |
npm run build runs an untargeted Astro build. Preview any build with npm run preview.
Supported env vars (schema defined in astro.config.ts):
BUILD_TARGET—veevafor Veeva buildsVEEVA_ENV—client(default) orevokeVEEVA_PLATFORM— override the config's target platformVEEVA_TRAINING—truefor training buildsWEB_ENV,WEB_SCALING— web-mode knobsLOG_LEVEL,LOG_OUTPUT,LOG_TIME— logging
The pruneBuild integration from @inizioevoke/astro-core removes unused assets from the build output.
Deploying to Veeva
npm run deploy-client # client org
npm run deploy-client-training # client training
npm run deploy-evoke # evoke internalEach runs tasks/veeva-deploy.ts, which reads the matching .env.* and uploads the build output to Veeva.
Combined build + deploy shortcuts:
npm run veeva # build-client + deploy-client
npm run veeva-training # build-client-training + deploy-client-training
npm run veeva-evoke # build-evoke + deploy-evokeCSV-only variants (push the slide manifest without re-uploading the HTML):
npm run veeva-csv
npm run veeva-training-csv
npm run veeva-evoke-csvBulk-updating Veeva metadata
After a deploy, you often need to update metadata on each uploaded slide in Veeva PromoMats (expiration dates, CRM sharing settings, approved material relationships, lifecycle status). veeva-metadata.js is a browser-console script for this:
- Open the presentation in Veeva PromoMats in Chrome.
- Edit the
dataobject at the top of the file (expiration dates, CRM sharing teams, approved material codes, status, etc.). - Paste the whole script into DevTools and run it. It walks every slide, applies the metadata, and advances through the lifecycle.
Analytics
Click tracking is handled by @inizioevoke/veeva-iva, configured in src/lib/client/ (typically via IVA.configure(...) or IVA.init(...)). Tracked elements emit console.debug lines in the form:
DEBUG: track {slide: 'home', type: 'button', name: 'button_cta_learn_more'}To audit coverage, start npm run dev, visit each page, click every tracked element ([data-evotrk], <a>, <button>, etc.), and record the emitted events.
As an alternative, Claude Code users with the
evoke-analytics-auditorskill installed can ask Claude to run the audit — it drives a Playwright browser through every page and writes a Markdown + CSV report of tracking events, ambiguous events, errors, warnings, and logs.
Related packages
- @inizioevoke/astro-core — Carousel, FlipCard, Modal, PdfViewer, RightISI, Tabs, Zoom components; swipe/pinch gesture helpers;
pruneBuildandrelativeLinksbuild integrations. - @inizioevoke/veeva-astroclm-core — the CLM runtime; hosts the
pageManager,veevaConfigManager, anddeploytask implementations. - @inizioevoke/veeva-iva — IVA integration (click tracking, navigation hooks).
- @inizioevoke/veeva-clmjs — Veeva CLM JS runtime.
