Start with pages and privacy choices. Each part works on its own; you can add a backend later.
${starterPath()}
When your site needs more
Choose the next part
Follow the path from site configuration to visitor controls and retained records. Open only the areas your site needs.
${stageOverview()}
npm packages
Install the parts your site needs
The core package has no runtime dependencies. Use its vanilla interface or build your own from the same headless controllers. Add notabenda-cf when you need a Cloudflare evidence workspace.
${syntax.renderBlock("npm install\nnpm run dev\n\n# Pack the reusable frontend\nnpm pack --workspace notabenda", "sh",{dark:true})}
Typed configuration. Semantic HTML. Optional CSS.
Coverage and limits
Check what is covered
${counts.requirements} requirements linked to sources, code and verification. The count is not a compliance score. Legal research is initial and unreviewed; no jurisdiction pack is production-supported.
Requirements & current status →
`);
files['assets/syntax.css']=syntax.stylesheet();syntax.dispose();
files['schemas/service-catalog-v1.json']=await read('catalog/schema.json');
files['404.html'] = layout('Page not found','
404
Page not found
This address does not match a page.
Back to Notabenda');
// Backend bundles stay OUTSIDE the uploaded static asset tree.
const config = JSON.parse(await read('examples/static/notabenda.config.json'));
const initial = compileLegalDocuments(config,{mode:'preview'});
const next = structuredClone(config); next.purposes[0].retention='Sample: 30 days after an enquiry is resolved.';
next.documents.terms.sections[1].paragraphs.push('This draft revision clarifies the fictional content example.');
for(const doc of Object.values(next.documents)) doc.version='draft-2';
const latest=compileLegalDocuments(next,{mode:'preview'});
const catalogExample=catalogReleaseExample(config);
const agreements=[]; const releases=[{artifact:initial.artifact,metadata:{recordedAt:'2026-10-17T10:00:00Z',changeNote:'Initial fictional example documents.'}},{artifact:latest.artifact,metadata:{recordedAt:'2026-10-18T10:00:00Z',changeNote:'Clarify sample retention and fictional terms content.'}}];
releases.push(...catalogExample.releases,...(await authoringExample()).releases,...(await retainedReleaseExample()).releases);
for(const [theme,prefix] of [['light',''],['dark','dark/']]) {
const offer=JSON.parse(files[`${prefix}agreement/offer.json`]);
agreements.push({theme,offer}); releases.push({artifact:JSON.parse(files[`artifacts/${offer.artifactId}.json`]),metadata:{recordedAt:'2026-10-18T10:00:00Z',changeNote:`Fictional ${theme} agreement demonstration.`}});
}
const localizedOffer=JSON.parse(files['localization/agreement/offer.json']);agreements.push({theme:'pt',offer:localizedOffer});releases.push({artifact:JSON.parse(files[`artifacts/${localizedOffer.artifactId}.json`]),metadata:{recordedAt:'2026-10-19T12:00:00Z',changeNote:'Localized terms agreement example.'}});
// Seed evidence from the build-time site, never the intentionally smaller browser descriptor.
const choiceConfig=exampleConsentConfig(config);
for(const [kind,doc] of Object.entries(choiceConfig.documents))doc.route=`/choices/${kind}`;
const choiceBuild=compileLegalDocuments(choiceConfig,{mode:'preview'});
const consent={artifact:choiceBuild.artifact,notice:createConsentNotice(choiceBuild.artifact,{scopeId:'sample-cookie-choices'},{mode:'preview'})};
const recordedNotice=JSON.parse(files['recorded-choices/notice.json']);
const recordedConsent={notice:recordedNotice,artifact:JSON.parse(files[`artifacts/${recordedNotice.artifactId}.json`])};
const processingNotice=JSON.parse(files['processing-choices/notice.json']);
const processing={notice:processingNotice,artifact:JSON.parse(files[`artifacts/${processingNotice.artifactId}.json`])};
const permissionOffer=JSON.parse(files['form-permissions/offer.json']);
const formPermissions={offer:permissionOffer,artifact:JSON.parse(files[`artifacts/${permissionOffer.artifactId}.json`])};
const paidOrders=Object.fromEntries(['mixed','deferred','conditional'].map(route=>[route,JSON.parse(files['orders/'+route+'/template.json'])]));
const contractDefinitions=Object.fromEntries(['withdrawal','cancellation'].map(route=>[route,JSON.parse(files['contract-actions/'+route+'/definition.json'])]));
const contractActions={definitions:contractDefinitions,artifact:JSON.parse(files['artifacts/'+contractDefinitions.withdrawal.artifactId+'.json'])};
const server={releases,agreements,consent,recordedConsent,processing,formPermissions,contractActions,paidOrders,evidence:syntheticRecords}; server.version=createHash('sha256').update(JSON.stringify(server)).digest('hex');
await writeBuildFiles(output,files);
for(const path of media) { await mkdir(dirname(resolve(output,path)),{recursive:true}); await copyFile(resolve(root,path),resolve(output,path)); }
await writeFile(new URL('./dist/server-data.json',import.meta.url),JSON.stringify(server));
const expected=new Set([...Object.keys(files),...media]);
// Fail rather than upload unexpected/stale files left in a development output directory.
async function inspect(directory,prefix='') {
for(const entry of await readdir(directory,{withFileTypes:true})) {
const path=prefix+entry.name;
if(entry.isDirectory()) await inspect(resolve(directory,entry.name),path+'/');
else if(!entry.isFile() || !expected.has(path)) throw new Error(`Unexpected asset output (${path}); inspect and remove unmanaged files before deploying.`);
}
}
await inspect(output);
await writeFile(new URL('./dist/asset-manifest.json',import.meta.url),JSON.stringify([...expected].sort(),null,2)+'\n');
console.log(`Built Notabenda website: ${docs.size} guide pages, public examples and protected synthetic workspace.`);