A detailed installation
How you install it, at length, with every option.
Before you start
They take routes away — each names a case you are already in, or one choice the page then stops repeating.
It fills in the examples.
Running the notes server yourself asks four more, and one address, and where the database is:
Your installer prints it.The command takes it, and checks that it leads to the folder you run it in. Left empty, the blocks guess.
Where the MySQL server is
The password is typed on the installer's screen, never on this page.The password is read from a file on the server, never typed on this page nor in the command.
What to install, and where
Two things to install, and the drawing says where each one goes.
Three things to install, and the drawing says where each one goes.
Encrypted note
The note server https, and the site’s origin declared — or open to anyone.The note server Already running. Nothing to install, nothing to declare.
Reads the notes
The assistantNode 18+, and the project key.Writes the fix
Your code
Deploy
Install the app on the server
-
Run one command on the server
It carries your answers above, and installs.
Every option:
--fetch, then--help.curl -fsSO https://annotepage.com/annotepage-install.php php annotepage-install.php \ --api-address=https://api.staging.example.com/api.php \ --answers-for=one-site \ --origins=https://staging.example.com \ --storage=sqlite \ --updated-by=cron \ --delete-installer -
Upload one PHP file
Download it onto the server, then open it in your browser.
It ends by printing your API address, for the field at the top.
Download annotepage-install.php
32 KB, one file, nothing to configure. Put it anywhere under your web root.
-
Answer its three questions
You have answered all three above.
Under them it shows the address your pages will point at, filled in from the request that opened it. Correct it if the name your visitors use is not the one this server sees — behind a proxy or a CDN, or through a temporary address. Change it and the installer writes a file there and asks for it back before doing anything else.
It writes the configuration, then offers to delete itself. Let it.
The server is downloaded. Three questions, and all three have a default that works.
First, can this server run it?
Yes. PHP 8.3, the extensions, a directory it can write to.
Then, your answers
What this server is, and where it puts the notes.
Which sites may write here
The ones I listAny site
Storage
SQLiteMySQL
The sites these notes will be written from
https://staging.example.com
The address these pages will call
https://api.staging.example.com/api.php
Where the MySQL server is.
The installer connects and creates the tables before writing anything. Every one of them is needed.
Host
127.0.0.1
Port
3306
Database name
your-database
User
your-user
Password
••••••••
How this server gets its updates.
Keeping this server up to date
A shell cronAn address to callIt updates itself
-
Declare your project
You chose The ones I list: the server only takes notes from the projects and addresses written in
internal/config-local.php. Paste this block there.'projects' => array( '7Qb1kZ3xNvA9dLpEqKf2Zt' => array( 'origins' => array('https://staging.example.com'), 'mode' => 'encrypted', ), ), -
Keep it up to date
Once a day, from your crontab: it updates when a release is out, does the maintenance, and tells the project three totals.
17 4 * * * php <your install>/internal/update.php >/dev/null -
Keep it up to date
Once a day, from your host’s scheduler: it updates when a release is out, does the maintenance, and tells the project three totals. The installer shows the secret once.
17 4 * * * curl -fsS 'https://api.staging.example.com/api.php?action=update&token=<the secret>' -
Change anything else, later
Everything else lives in one file, and every key in it is commented.
// an extract: the file only carries what // differs from the defaults in config.php return array( 'active' => true, 'deployment' => 'self-hosted', // THE PROJECTS. The installer leaves this empty: // the id is drawn in a browser, afterwards. 'projects' => array(), );
config-local.example.php sits beside it, in the same folder: the same file with every key in it and what each one costs, written out.
Install the tag on the site
-
Paste the tag
At the end of
<body>.The key was drawn in this browser, just now, and sent nowhere.
The id is derived from the key, in this browser, just now — the server recognises it, it never issues it.
<script src="https://cdn.jsdelivr.net/npm/annotepage-client@2/dist/annotepage.js" data-server="https://api.annotepage.com/api.php" data-key="<43 characters, drawn in your browser>" defer></script>Where it goes, in what you use:
Framework Static Builder CMSPoint at your tool to see where the tag goes. Once, in the layout every page shares — not again in a page or a component.
Next.js
app/layout.tsx, last in<body>, as a plain<script>— pages router:pages/_document.tsxNuxt
nuxt.config.ts, underapp.head.script, withtagPosition: 'bodyClose'SvelteKit
src/app.html, before</body>Astro the layout in
src/layouts/, before</body>, withis:inlineRails
app/views/layouts/application.html.erb, before</body>Laravel
resources/views/layouts/app.blade.phpwith Livewire,resources/views/app.blade.phpwith React, Vue or SvelteSymfony
templates/base.html.twig, before</body>Django the base template your pages
{% extends %}, before</body>Drupal your theme’s
html.html.twig, before</body>React Router 7 (Remix)
app/root.tsx, last in<body>, after<Scripts />SolidStart
src/entry-server.tsx, before</body>HTML before
</body>in every page you want annotated, not onlyindex.htmlVite (React, Vue)
index.htmlat the root — never imported into the bundleAngular
src/index.html— not inangular.jsonscriptsHugo
layouts/baseof.html, before</body>Jekyll
_layouts/default.html, before</body>Eleventy your base layout, before
</body>Webflow Site settings → Custom code → Footer code
Shopify
theme.liquid, before</body>, on an unpublished copy of the themeFramer Site settings → General → Custom code, end of
<body>Squarespace Settings → Code injection → Footer
Wix Settings → Custom code, placed at the end of the body
Ghost Settings → Code injection → Site footer
WordPress the plugin: it works on activation, and only the audience you choose receives the key — by hand, hook it on
wp_footerA Content-Security-Policy must allow
cdn.jsdelivr.netinscript-srcand your notes server inconnect-src— what else stops it appearing. -
Hand the key over
Each reviewer pastes it once.
<43 characters, drawn in your browser>
Keep this key. Lose it and the notes go with it — no recovery, no rotation. Put it where your team keeps its passwords, now.
Nothing appearing? A module or a build that combines the
scripts, a wrong integrity digest and a
Content-Security-Policy each stop it —
the four cases, answered.
Connect your assistant
-
Give it the key, once
One file, on the machine where your assistant runs. Write it yourself, or hand it the key and let it write the file (less secure: the key crosses a provider’s logs).
The address is on your tag. The key is the one your reviewers paste.
{ "projects": { "staging.example.com": { "api": "https://api.annotepage.com/api.php", "key": "<the key>", "origin": "https://staging.example.com" } } } -
Then ask it
One sentence, with the address of your site.
[figure: playing it again]
use npx annotepage-mcp to fix what the reviewers flagged on staging.example.com
Bash(annotepage open)… +41 lines (ctrl+o to expand)Three notes are open on staging.example.com, all on /pricing.
Read(pricing.html)… +214 lines (ctrl+o to expand)Reading the page they point at. From here it is the loop: measure, edit, reply in the thread, resolve.
❯
Nothing to install. Say npx when you ask, and your
assistant fetches the latest version itself.
Want it installed for good? See how.