Automate GitHub social preview image updates from your repository README.
This utility does two things:
- Captures a screenshot of
https://github.com/<owner>/<repo>/blob/<default-branch>/README.md. - Uploads that image to the repo's Settings -> Social preview.
It uses Playwright UI automation because this setting is managed in GitHub's web UI.
- Node.js 18+
- A GitHub account that can edit the target repo settings (typically admin access)
- Chromium installed for Playwright
From npm (recommended):
npm i -g gh-social-preview
npx playwright install chromiumOr without global install:
npx gh-social-preview init-auth
npx gh-social-preview --repo owner/repoFrom source:
npm install
npx playwright install chromium- Authenticate once and save browser session state:
node gh-social-preview.js init-auth- Update social preview from README screenshot:
node gh-social-preview.js --repo owner/reponode gh-social-preview.js helpnode gh-social-preview.js init-auth [--storage-state /path/to/state.json] [--base-url https://github.com]Options:
--storage-state(optional): Path where Playwright session JSON is saved.- Default:
$XDG_STATE_HOME/gh-social-preview/auth/<host>.json - Fallback when
XDG_STATE_HOMEis unset:~/.local/state/gh-social-preview/auth/<host>.json
- Default:
--base-url(optional): GitHub base URL (for GitHub Enterprise, for example).
Notes:
- Opens a visible browser window.
- Waits until login is detected, then writes storage state.
node gh-social-preview.js --repo owner/repo [--storage-state /path/to/state.json] [options]Required:
--repo:owner/repoor full repo URL.
Optional:
--base-url: Defaulthttps://github.com--storage-state: Default$XDG_STATE_HOME/gh-social-preview/auth/<host>.json(fallback:~/.local/state/gh-social-preview/auth/<host>.json; uses<host>=githubfor GitHub.com)--width: Default960--height: Default480--format:pngorjpeg(defaultjpeg)--quality: JPEG quality1-100(default80)--out: Output path for screenshot (default$XDG_CACHE_HOME/gh-social-preview/images/<owner>__<repo>.<ext>, fallback~/.cache/gh-social-preview/images/<owner>__<repo>.<ext>)--headless:true|false(defaulttrue)
Update with visible browser and custom output:
node gh-social-preview.js \
--repo AnswerDotAI/exhash \
--headless false \
--out .social-preview/exhash.jpgUse against GitHub Enterprise:
node gh-social-preview.js init-auth \
--base-url https://github.mycompany.com
node gh-social-preview.js \
--base-url https://github.mycompany.com \
--repo team/repo \
--headless false- The screenshot is a viewport capture (not full page).
- README capture resolves the repo default branch via the GitHub REST API (
/repos/<owner>/<repo>), then targetsblob/<default-branch>/README.md, waits forarticle.markdown-body, and hides GitHub's sticky blob header (Preview | Code | Blame) before taking the screenshot. - Upload supports both states:
- no existing social card: adds a new one
- existing social card: replaces it
- If a JPEG output is over 1MB, the script retries with lower JPEG quality.
- If PNG output is over 1MB, the script warns but does not auto-convert.
- Upload completion primarily uses GitHub's upload response (
/upload/repository-images/...) plus non-empty social-image id; unchanged id is accepted for identical-image replacements.
- Redirected to
/loginduring run:- Re-run
init-authand use the same base URL + storage-state path (or default host path).
- Re-run
- Previously used
./.auth/<host>.jsondefaults:- Move that file to the XDG default path above (or pass
--storage-stateexplicitly).
- Move that file to the XDG default path above (or pass
- README container not found:
- Repo likely does not have a root
README.mdon its default branch, or GitHub markup/layout changed.
- Repo likely does not have a root
- Failed to query repo metadata from GitHub API:
- Ensure the repository exists and is publicly accessible.
- Upload controls not found:
- GitHub UI may have changed; selectors in
gh-social-preview.jsmay need updating.
- GitHub UI may have changed; selectors in
- Permission errors in settings:
- Ensure your account has admin-level access for that repository.
--storage-state contains authenticated browser session data. Treat it like a credential.
If you override defaults to store auth/screenshots in your repo, ignore those paths:
.auth/
.social-preview/ISC