Announce new features right where they live.
In-app changelog and feature hotspots generated from your pull requests. Zero backend, tiny web component.
Docs · Playground · Getting started · Changelog
shipdot shows your users what changed, inside your app: a bell with an unread count, a "What's new" panel, and hotspots, pulsing dots on the exact buttons a new feature lives in. Updates are Markdown files in your repository, drafted from your merged pull requests and reviewed like code. There is no server, database, account or tracking.
Note
shipdot is young (0.x). The shipdot.json format is versioned and stable; the widget, CLI and Action may still change between minor versions. Every change is in the changelog.
- Hotspots on the real UI. An entry with
target: "#btn-export"puts a pulsing dot on that button. Telling users a feature exists is good; pointing at it is better. Hotspots follow their target on scroll and resize, wait for elements rendered late in SPAs, and hide when the target is hidden. - Git-native, reviewed by a human.
shipdot draftturns merged pull requests into notes written for end users and opens a pull request. You fix the wording, merge, ship. - Multi-language from day one.
entry.es.mdnext toentry.md. The widget picks the user's language, falls back per entry, and ships its UI strings in English, Spanish and Portuguese. - Nothing to run or pay for per user. A static
shipdot.jsonand a web component of about 7 KB min+gzip. No backend and no telemetry: your announcements stay in your repository, and read state stays in your users' browsers.
- Feed panel as a popover, drawer or modal, with an unread badge.
- Drafts from pull requests: offline template mode, or rewrite and translate with your own LLM key (Anthropic or any OpenAI-compatible API).
- GitHub Action that opens a pull request with new drafts every week.
- Zero runtime dependencies, Shadow DOM, and a 10 KB min+gzip budget enforced in CI.
- Accessible: keyboard navigation, focus management, WCAG 2.1 AA contrast in light and dark themes, checked with axe.
llms.txtandllms-full.txtso AI assistants know what changed.- Themeable with CSS custom properties,
::part()and your own trigger.
You need Node.js 24 or newer for the CLI. The widget runs in any modern browser.
1. Create the config and a first entry
npx @shipdot/cli init2. Write entries and build the feed
Each update is a Markdown file in .shipdot/entries/, for example .shipdot/entries/2026-10-14-export-excel.md:
---
date: 2026-10-14
type: new # new | improved | fixed
title: Export to Excel
target: "#btn-export" # optional: hotspot on this element
---
You can now export any report to Excel with one click.npx @shipdot/cli build # → public/shipdot.json, public/llms.txt, public/llms-full.txt3. Add the widget
<script type="module" src="https://unpkg.com/shipdot@0.2"></script>
<ship-dot src="/shipdot.json"></ship-dot>Or npm install shipdot and import 'shipdot' once on the client. Try it without installing anything in the playground.
The bell starts without a badge: on a first visit, existing entries count as read. To see the badge and hotspots, add a new entry and run build again, or set first-visit="show-latest" while you try it out.
flowchart LR
A[Merged pull requests] -->|shipdot draft| B[Pull request with draft entries]
B -->|human review and merge| C[".shipdot/entries/*.md"]
C -->|shipdot build| D[shipdot.json]
C -->|shipdot build| E[llms.txt]
D --> F[ship-dot widget in your app]
F --> G[Bell and unread badge]
F --> H[Hotspots on target elements]
HTML is sanitized once, at build time, with a strict allowlist, so the widget stays tiny and never parses untrusted Markdown. The decisions behind the design are in docs/adr.
<ship-dot
src="/shipdot.json"
mode="popover"
locale="es"
theme="auto"
max-hotspots="3"
first-visit="mark-read"
storage-key="default"
></ship-dot>| Attribute | Default | Description |
|---|---|---|
src |
required | URL of shipdot.json. The only request the widget makes. |
mode |
popover |
popover, drawer or modal. |
locale |
navigator.languages |
Language to show, with fallback to the feed's default. |
theme |
auto |
light, dark or auto. |
hotspots |
on | off disables hotspots. |
max-hotspots |
3 |
Hotspots visible at once. |
first-visit |
mark-read |
mark-read (existing entries count as read) or show-latest. |
storage-key |
default |
Read state key: localStorage["shipdot:<key>"]. |
Methods: open(), close(), markAllRead(), markRead(ids), replayLatest(), refresh().
Events (bubbling, composed CustomEvents): shipdot:ready, shipdot:open, shipdot:close, shipdot:read, shipdot:hotspot-click, shipdot:error.
Styling: CSS custom properties (--shipdot-accent, --shipdot-hotspot, --shipdot-bg…), ::part() on every key piece, and a default slot for your own trigger.
Full reference: widget · theming · hotspots.
| Command | What it does |
|---|---|
shipdot init |
Creates shipdot.config.json and an example entry. Never overwrites. |
shipdot build |
Validates entries, renders sanitized HTML, writes shipdot.json, llms.txt, llms-full.txt and copies relative images. |
shipdot validate |
Prints file:line:column errors (duplicate IDs, bad dates, unknown types, orphan translations, undeclared locales…). Exit code 1 on errors. |
shipdot draft |
Drafts entries from PRs merged since the last tag. --dry-run, --since, --base, --repo, --no-llm. |
Full reference: CLI · config · feed format.
# .github/workflows/shipdot.yml
name: shipdot
on:
workflow_dispatch:
schedule:
- cron: '0 9 * * 1' # Mondays 09:00 UTC
permissions:
contents: read
jobs:
draft:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
- uses: pipe1os/shipdot@v0
with:
command: draft
# llm-provider: anthropic
# llm-api-key: ${{ secrets.SHIPDOT_LLM_API_KEY }}Important
Enable Settings → Actions → General → Allow GitHub Actions to create and approve pull requests so the Action can open the draft pull request.
| Input | Default | Description |
|---|---|---|
command |
draft |
draft opens a pull request with new drafts; build writes shipdot.json and llms.txt. |
github-token |
${{ github.token }} |
Token for the GitHub API. |
base-branch |
config, then repo default | Branch the pull requests are merged into. |
llm-provider, llm-api-key, llm-model, llm-base-url |
— | Optional LLM mode. Pass the key from a secret; it is masked in logs. |
working-directory |
. |
Folder with shipdot.config.json. |
Outputs: entries, pull-request-url, pull-request-number. More in the Action guide.
Add a ### shipdot section to your PR template and the note is written by the person who knows the change best:
### shipdot
title: Export to Excel
type: new
target: "#btn-export"
You can now export any report to Excel with one click.The note ends at the next heading or at a horizontal rule (---), so keep the section above any footer that tools add to the description. Write skip to opt a PR out. PRs labeled skip-shipdot, bot PRs and chore/ci/test/docs/refactor PRs are skipped automatically.
Template mode works offline. To let a model rewrite drafts in user language and translate them, bring your own key:
SHIPDOT_LLM_PROVIDER=anthropic SHIPDOT_LLM_API_KEY=... npx @shipdot/cli draftSHIPDOT_LLM_PROVIDER is anthropic or openai-compatible (with SHIPDOT_LLM_BASE_URL and SHIPDOT_LLM_MODEL, for example a local Ollama). Requests use plain fetch, no SDKs.
Only the pull request title and body are sent, never diffs, code, file names, comments or author data. PR text is treated as untrusted: it is wrapped in escaped delimiters, the system prompt forbids following instructions inside it, and the reply must pass schema validation or the PR falls back to template mode. Every draft is reviewed by a human before it ships. Details: Drafting guide.
{ "defaultLocale": "en", "locales": ["en", "es", "pt"] }Add 2026-10-14-export-excel.es.md with a translated title and body. The widget uses the locale attribute or navigator.languages, matches es-CL → es, and falls back to the default locale per entry. i18n guide.
Works anywhere custom elements do. Guides and typed JSX/template support for React, Vue, Svelte, Astro, Next.js and vanilla JS. Runnable apps live in examples/.
import 'shipdot'
import type {} from 'shipdot/react' // or shipdot/vue, shipdot/svelte, shipdot/astroChrome and Edge 123+, Firefox 125+, Safari 17.5+ (Popover API, <dialog>, light-dark()). Importing the module during server-side rendering is safe.
Why is nothing unread on my first visit?
A first visit marks existing entries as read, so new users aren't greeted by your whole history. Use first-visit="show-latest" or clear localStorage["shipdot:default"].
Is the HTML in entries safe?
shipdot build sanitizes it with an allowlist (no scripts, iframes, styles, event handlers or javascript: URLs), tested against common XSS vectors. The widget trusts shipdot.json, so serve it from your own site.
Can I use it without GitHub?
Yes. init, build and validate only read local files. Only draft and the Action talk to GitHub.
Can I open the panel from my own menu?
Put your own element inside <ship-dot> (it replaces the bell) or call document.querySelector('ship-dot').open().
More in the FAQ.
Not in the MVP, planned as opt-in features: analytics, segmentation by user or plan, A/B tests, RSS/Atom output, publishing to Slack, Discord and email, a web admin panel, and read-state sync across devices. See PLAN.md and share ideas in Discussions.
Contributions are welcome! Read CONTRIBUTING.md for the setup (pnpm install && pnpm build && pnpm test), conventions and review process. Please follow the Code of Conduct and report security issues privately as described in SECURITY.md.
MIT © 2026 pipe1os and shipdot contributors