ShareMyPageDocs

Keeping a page up to date

A publish URL is a long-lived address for one page, with a token you keep in your secret store. PUT your HTML at it from a pipeline, a cron job or a script, and the page updates in place, keeping its link, its title and its version history.

Most pages are published once. Some are not: a dashboard your pipeline rebuilds every night, a report regenerated every Monday, a status page that follows a build. Re-uploading those by hand is the work, and it is the work that gets skipped.

A publish URL is a long-lived address for one page. Whatever you PUT at it, with your publish token in the Authorization header, becomes that page's new content. Your pipeline owns the update, and the link you already shared keeps working.

What a publish URL is

Two parts, and only one of them is a secret.

  • The address is https://sharemypage.app/api/publish/<pageId>. That page id is the same id in the page's own link, so it is not a secret and belongs in the URL.
  • The token looks like sp_pub_... and travels in the Authorization header. It stands in for your account on that one page: a script holding it can replace that page's content and nothing else, so it cannot read your other pages, change your settings or create anything new.

Treat the token like a password. Anyone who has it can rewrite that page.

Where to get one

Open the page, then Settings, Content, Publish URL, and choose Create publish URL. You get the address, the token and a ready command to copy. The token is shown only at that moment.

The same section tells you when the URL was last used, and lets you replace or revoke it at any time. Only the page's owner, or a workspace admin, can see or change it.

What the token is good for, and for how long

One page, and only that one. A token is bound to the page it was issued for. Point it at a different page id and the request is refused, so a token that leaks cannot be aimed somewhere else.

You see the token once. Only a hash of it is stored, so it cannot be shown to you again, not even by us. Copy it into wherever your pipeline keeps secrets at the moment it is issued. If you lose it, issue a new one.

Revoking is immediate. Every publish re-reads whether the token is still good, so a revoked token stops working on the next request rather than at the end of some window. The page keeps the content it already has.

Creating a new token replaces the old one. A page has one publish URL at a time, so issuing a new token revokes the previous one in the same step. That is also how you recover a token you lost. Paste the new value into your secret store straight away, because any run between the two uses the old token and will be refused.

It publishes as you. Each request re-checks that you may still manage the page, so a token pointed at a page you have lost access to stops working. It does not become a standing key that outlives your own permissions.

One line to publish

curl -X PUT \
  -H "Authorization: Bearer $SHAREMYPAGE_TOKEN" \
  --data-binary @page.html \
  https://sharemypage.app/api/publish/<pageId>

That is the whole contract. No login step, no page lookup, nothing to keep in sync. A success response is JSON with the page's url.

Store the token, not a URL

The secret you keep is the sp_pub_... token on its own. The address, page id included, can sit in your workflow file in plain sight. That split is the reason for the extra header: a token in a URL ends up in server logs, proxy logs and CI job output, where it quietly outlives the run. A token in a header does not. There is deliberately no query-parameter form of this, for the same reason.

From GitHub Actions

Put the token in Settings, Secrets and variables, Actions, as SHAREMYPAGE_TOKEN.

name: Publish dashboard

on:
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build the page
        run: ./scripts/build-dashboard.sh > dashboard.html

      - name: Publish to ShareMyPage
        env:
          SHAREMYPAGE_TOKEN: ${{ secrets.SHAREMYPAGE_TOKEN }}
        run: |
          curl --fail-with-body --silent --show-error \
            -X PUT \
            -H "Authorization: Bearer $SHAREMYPAGE_TOKEN" \
            --data-binary @dashboard.html \
            https://sharemypage.app/api/publish/<pageId>

--fail-with-body is what makes the step red when a publish is refused, and prints the reason. Without it curl exits 0 on an error response and your job goes green on a page that never changed.

From GitLab CI

Add the token as a CI/CD variable named SHAREMYPAGE_TOKEN, with Mask variable and Protect variable both on. Masking keeps it out of job logs; protecting keeps it off unprotected branches, so a merge request from a fork cannot read it.

publish:dashboard:
  stage: deploy
  image: curlimages/curl:latest
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - ./scripts/build-dashboard.sh > dashboard.html
    - >
      curl --fail-with-body --silent --show-error
      -X PUT
      -H "Authorization: Bearer $SHAREMYPAGE_TOKEN"
      --data-binary @dashboard.html
      https://sharemypage.app/api/publish/<pageId>

Run it on a schedule under Build, Pipeline schedules, and the page refreshes itself on whatever rhythm the data has.

From cron or a script

No CI at all works the same way. Keep the token in a file only you can read, and have the script source it rather than carry it.

#!/usr/bin/env bash
set -euo pipefail

# chmod 600 ~/.config/sharemypage/token
source ~/.config/sharemypage/token   # SHAREMYPAGE_TOKEN="sp_pub_..."

./build-report.sh > /tmp/report.html

curl --fail-with-body --silent --show-error \
  -X PUT \
  -H "Authorization: Bearer $SHAREMYPAGE_TOKEN" \
  --data-binary @/tmp/report.html \
  https://sharemypage.app/api/publish/<pageId>
0 7 * * 1  /home/you/bin/publish-report.sh

What a publish changes, and what it leaves alone

Each of these is worth knowing before you point a pipeline at a page people are already reading.

The link never changes. The address you shared, the subdomain, the custom domain if you set one: all the same page afterwards. That is the point of publishing to a page you already have rather than uploading a new one.

Every push is a new version. The page's Versions tab lists each publish, so a bad build is a restore away rather than a rebuild away. How many versions are kept depends on your plan, which the FAQ spells out.

The title stays yours. The page keeps the title you gave it even when the HTML you upload carries a different <title>. A generated file often has a title like report-2026-11-10, and you do not want that in your nav every night. To rename the page from a publish, say so on purpose with ?title=New%20name.

Visibility is never touched. A private page stays private, a public page stays public, a password stays in place. Publishing moves content, nothing else, so a nightly job cannot quietly widen who can read a page.

4.5 MB is the hard ceiling on one request. Our hosting platform refuses a larger request body before it reaches us, whatever your plan allows. Your plan's page-size cap applies on top of that, so the smaller of the two wins. If a generated page is bumping into it, the usual cause is inlined base64 images: host them as page images and reference them by URL instead.

Thirty publishes a minute per token. Past that a request is refused until the minute is out. A scheduled rebuild is nowhere near it; a script looping over a list of pages can be, so pace a loop rather than firing it flat out.

The page updates at once; the raw link can lag five minutes. Readers opening the page see the new content as soon as the publish returns. The tokenless raw link for a public page is cached for up to five minutes, so an AI tool reading that URL right after a push can still get the previous version. It matters when a pipeline publishes and then immediately asks an agent to read the page back: give it five minutes, or have it read the page URL rather than the raw one.

When a publish is refused

Every failure answers with a status and a reason, which is why --fail-with-body is worth having in the command.

StatusWhat happened
400The body was empty, so there was nothing to publish
401The token is missing, unknown or revoked
403The token belongs to another page, or you no longer have access to this one
404The page has been deleted
413The content is over the page-size cap for your plan
422The HTML looks truncated, so nothing was saved
429More than 30 publishes in a minute with this token

A request body over 4.5 MB never reaches us at all, so it fails in your job rather than coming back with one of these.

Publishing Markdown

Add ?format=markdown and the body is treated as Markdown and rendered for you, the same as a Markdown upload anywhere else.

curl -X PUT \
  -H "Authorization: Bearer $SHAREMYPAGE_TOKEN" \
  --data-binary @report.md \
  "https://sharemypage.app/api/publish/<pageId>?format=markdown"

Useful when the thing your job produces is a report rather than a designed page: you write Markdown, the page stays styled. The Markdown you sent is kept as the page's source, so the page's own Download as Markdown still gives back what your job wrote. Without the parameter, the body is stored as HTML exactly as you sent it.

When the data is genuinely live

A publish URL suits data that is rebuilt on a schedule. If the numbers change by the minute and a nightly push would always be stale, let the page fetch them itself. Page content can make outbound requests, so a plain fetch() in an inline script works, and the page is then current every time someone opens it.

Two limits decide whether that is actually an option:

  • No credentials, no API keys. Anyone who can open the page can read its source, so anything in it is published. That rules out a secret, and leaves either a genuinely public endpoint or a publishable key paired with row-level security.
  • The request arrives with a null origin. Page content renders in a sandboxed frame with no origin of its own, so the far end sees Origin: null. An endpoint answering Access-Control-Allow-Origin: * works; one with a domain allowlist rejects it.

Data and backends covers both in full, including what to do when a hosted service only offers a domain allowlist.

See also

  • Pages for the one-off upload paths, in the browser and over MCP.
  • Access and sharing for who can open a page, which a publish never changes.
  • Data and backends for a page that reads live data at view time.

On this page