A small TypeScript helper for collecting change suggestions directly on a website. Click the floating button, add a title and description, then select the exact point that needs attention. Suggestions are saved in suggestions.xml on your server.
Requires Node.js 22 or newer.
npm install
npm run demoOpen http://127.0.0.1:4317. Submit an idea to create suggestions.xml in the current working directory.
npm install changelyIn your browser entry point, after the document body exists:
import { mountChangely } from 'changely';
const changely = mountChangely({ endpoint: '/api/changely' });
// changely.open(); changely.close(); changely.destroy();It works independently of your frontend framework. The card uses a shadow root to isolate its styles. Escape cancels point selection or closes the card. A failed save keeps the draft for another attempt.
For a plain HTML website, serve node_modules/changely/dist/widget.js as a static asset:
<script src="/assets/changely/widget.js"></script>
<script>
const feedback = Changely.mountChangely({ endpoint: '/api/changely' });
</script>You can also inject this script into a page you are reviewing and call Changely.mountChangely() with your server endpoint. The page's content security policy must permit that script and endpoint. Each document, including an iframe, needs its own mount.
The browser sends JSON to your backend. Mount the included Node handler at the matching endpoint:
import { createServer } from 'node:http';
import { createSuggestionHandler } from 'changely/server';
const save = createSuggestionHandler({
file: './suggestions.xml',
onError: console.error,
});
createServer((req, res) => {
if (req.url === '/api/changely') return void save(req, res);
// Serve your existing website here.
res.writeHead(404);
res.end();
}).listen(3000);For Express, mount this handler before JSON body middleware, because it consumes the request stream itself:
app.all('/api/changely', createSuggestionHandler());
app.use(express.json());The backend needs a persistent writable filesystem. Serverless instances with temporary or separate disks need a custom persistence integration instead. Browsers cannot silently write into your project directory.
For a different origin, list the exact website origins in allowedOrigins and point the widget at the backend's full URL. You can integrate authentication and rate limits using authorize(req). The handler accepts JSON POSTs up to 64 KiB, validates inputs, and returns the saved suggestion with status 201. Same-origin requests are allowed by default; other browser origins require an allowlist entry.
For a custom backend:
mountChangely({
onSubmit: async suggestion => {
await yourStore.save(suggestion);
},
});Every suggestion records:
reference: pathname, query, and the nearest ancestor's ID, such as/pricing#plans. Without an ancestor ID it preserves the current fragment.selector: a CSS path to the clicked element, using IDs and sibling positions.x,y: coordinates in document pixels, including scroll offsets.relativeX,relativeY: fractions within the clicked element.viewportWidth,viewportHeight: viewport dimensions at selection time.
Selectors and coordinates describe the page at the time of selection. Later layout or DOM changes can move the target.
<?xml version="1.0" encoding="UTF-8"?>
<suggestions version="1">
<suggestion id="generated-uuid" createdAt="2026-10-23T12:00:00.000Z">
<title>Clarify the plan price</title>
<description>Show whether tax is included.</description>
<location>
<reference>/pricing#plans</reference>
<selector>#plans > button</selector>
<x>150</x>
<y>640</y>
<relativeX>0.5</relativeX>
<relativeY>0.25</relativeY>
<viewportWidth>1024</viewportWidth>
<viewportHeight>768</viewportHeight>
</location>
</suggestion>
</suggestions>The writer escapes XML, validates an existing document before appending, serializes writers with a filesystem lock, and replaces the file atomically. Keep this file outside publicly served directories. If a process crashes while holding the lock, confirm no writer is active before removing suggestions.xml.lock. A busy lock times out after ten seconds; the widget preserves the draft for retry.
npm ci
npm test
npm pack --dry-runTests cover measured point selection, failed-save retry, cancellation, XML escaping, concurrent persistence, invalid documents, request validation, authorization, and origin handling. Build outputs include TypeScript declarations and a standalone browser bundle.
To release from an authenticated checkout:
gh repo create hamb1y/changely --public --source=. --remote=origin --push
npm login
npm publish --access publicIf the repository already exists, add its origin and push instead of creating it. Confirm the npm name is available to your account before publishing; use a scoped name if needed.