Documentation Index Fetch the complete documentation index at: https://mintlify.com/buttondown/cli/llms.txt
Use this file to discover all available pages before exploring further.
The Buttondown CLI provides bidirectional sync between your local files and Buttondown’s servers through pull and push commands.
Overview
The sync workflow enables you to:
Pull : Download content from Buttondown to local files
Push : Upload local changes back to Buttondown
Track : Maintain sync state to avoid unnecessary operations
Pull Workflow Download emails, images, and settings from Buttondown to your local machine.
Push Workflow Upload local changes, new content, and images back to Buttondown.
Pull Workflow
The pull workflow downloads content from Buttondown and converts it to local files.
Pull Process
From src/commands/pull.tsx:83-162, the pull operation follows these steps:
Pull base resources
Downloads automations, newsletter settings, and snippets as JSON files. for ( const resource of BASE_RESOURCES ) {
const data = await resource . remote . get ( configuration );
if ( data ) {
await resource . local . set ( data , configuration );
}
}
Download images
Fetches all images from Buttondown’s API and saves them to media/. const remoteImages = await IMAGES_RESOURCE . remote . get ( configuration );
await IMAGES_RESOURCE . local . set ( remoteImages , configuration );
Each image is:
Downloaded from its S3 URL
Saved with its original filename
Tracked in the sync state
Build image mapping
Creates a mapping between remote URLs and local paths. const syncedImages : Record < string , SyncedImage > = {};
for ( const image of remoteImages ) {
const filename = path . basename ( image . image );
const localPath = path . join ( configuration . directory , "media" , filename );
syncedImages [ image . id ] = {
id: image . id ,
localPath ,
url: image . image ,
filename ,
};
}
Download emails
Fetches all emails and converts absolute image URLs to relative paths. const remoteEmails = await REMOTE_EMAILS_RESOURCE . get ( configuration );
const processedEmails = remoteEmails . map (( email ) => ({
... email ,
body: convertAbsoluteToRelativeImages ( email . body , emailsDir , imageMap ),
}));
await LOCAL_EMAILS_RESOURCE . set ( processedEmails , configuration );
This step:
Downloads all emails via paginated API requests
Converts https://buttondown.s3.amazonaws.com/... to ../media/filename.png
Saves as Markdown files with frontmatter
Write sync state
Saves the image mapping to .buttondown.json for future sync operations. await writeSyncState ( configuration . directory , { syncedImages });
Pull Command
# Pull to default directory (./buttondown)
buttondown pull
# Pull to custom directory
buttondown pull --directory=./my-newsletter
Push Workflow
The push workflow uploads local changes back to Buttondown.
Push Process
From src/commands/push.tsx:84-184, the push operation follows these steps:
Read sync state
Loads the current sync state from .buttondown.json. const syncState = await readSyncState ( configuration . directory );
const syncedImages = { ... syncState . syncedImages };
Read local emails
Scans the emails/ directory for all Markdown files. const localEmails = await LOCAL_EMAILS_RESOURCE . get ( configuration );
Upload new images
Finds relative image references in emails and uploads any that aren’t already synced. for ( const email of localEmails ) {
const refs = findRelativeImageReferences ( email . body );
for ( const ref of refs ) {
const absolutePath = path . resolve ( emailsDir , ref . relativePath );
const alreadySynced = Object . values ( syncedImages ). find (
( img ) => img . localPath === absolutePath ,
);
if ( ! alreadySynced ) {
const result = await uploadImage ( configuration , absolutePath );
syncedImages [ result . id ] = {
id: result . id ,
localPath: absolutePath ,
url: result . url ,
filename: result . filename ,
};
}
}
}
Images are uploaded via multipart form data to Buttondown’s /images endpoint.
Convert image paths
Replaces relative paths with absolute URLs using the updated sync state. const processedEmails = localEmails . map (( email ) => ({
... email ,
body: resolveRelativeImageReferences ( email . body , emailsDir , imageMap ),
}));
Converts ../media/header.png to https://buttondown.s3.amazonaws.com/images/abc123.png
Detect changes
Compares local emails with remote versions to find what’s changed. const remoteEmailsById = new Map (
( await REMOTE_EMAILS_RESOURCE . get ( configuration )). map (( e ) => [ e . id , e ]),
);
const changedEmails = localEmails . filter (( email ) => {
if ( ! email . id ) return true ; // New email
const remote = remoteEmailsById . get ( email . id );
if ( ! remote ) return true ; // Doesn't exist remotely
return serialize ( email ) !== serialize ( remote ); // Content differs
});
Only emails that have changed are uploaded, saving time and API requests.
Upload changed emails
Pushes modified emails to Buttondown. await REMOTE_EMAILS_RESOURCE . set ( changedEmails , configuration );
Emails with an id are updated via PATCH
Emails without an id are created via POST
Push other resources
Uploads automations, newsletter settings, and snippets. for ( const resource of BASE_RESOURCES ) {
const data = await resource . local . get ( configuration );
if ( data ) {
await resource . remote . set ( data , configuration );
}
}
Update sync state
Saves the updated image mapping to .buttondown.json. await writeSyncState ( configuration . directory , { syncedImages });
Push Command
# Push from default directory (./buttondown)
buttondown push
# Push from custom directory
buttondown push --directory=./my-newsletter
State Tracking
The CLI uses .buttondown.json to track sync state and avoid unnecessary operations.
State File Structure
From src/sync/state.ts:4-13:
export type SyncedImage = {
id : string ; // Buttondown image ID
localPath : string ; // Absolute path on local filesystem
url : string ; // Remote URL on Buttondown's S3
filename : string ; // Original filename
};
export type SyncState = {
syncedImages : Record < string , SyncedImage >;
};
Example .buttondown.json:
{
"syncedImages" : {
"img_abc123" : {
"id" : "img_abc123" ,
"localPath" : "/Users/you/newsletter/media/header.png" ,
"url" : "https://buttondown.s3.amazonaws.com/images/abc123.png" ,
"filename" : "header.png"
},
"img_def456" : {
"id" : "img_def456" ,
"localPath" : "/Users/you/newsletter/media/chart.jpg" ,
"url" : "https://buttondown.s3.amazonaws.com/images/def456.jpg" ,
"filename" : "chart.jpg"
}
}
}
State Operations
export async function readSyncState ( directory : string ) : Promise < SyncState > {
try {
const filePath = path . join ( directory , ".buttondown.json" );
const content = await readFile ( filePath , "utf8" );
return { ... DEFAULT_STATE , ... JSON . parse ( content ) };
} catch {
return { ... DEFAULT_STATE };
}
}
Returns default state if file doesn’t exist (first sync).
export async function writeSyncState (
directory : string ,
state : SyncState ,
) : Promise < void > {
const filePath = path . join ( directory , ".buttondown.json" );
await writeFile ( filePath , JSON . stringify ( state , null , 2 ));
}
Pretty-prints JSON with 2-space indentation.
Image Path Resolution
The CLI intelligently converts between relative and absolute image paths.
Absolute to Relative (Pull)
From src/sync/emails.ts:233-251, convertAbsoluteToRelativeImages:
export function convertAbsoluteToRelativeImages (
content : string ,
emailDir : string ,
syncedImages : Record < string , SyncedImageInfo >,
) : string {
const regex = new RegExp ( ABSOLUTE_IMAGE_URL_REGEX );
return content . replace ( regex , ( match , altText , imageUrl ) => {
const syncedImage = Object . values ( syncedImages ). find (
( img ) => img . url === imageUrl ,
);
if ( syncedImage ) {
const relativePath = path . relative ( emailDir , syncedImage . localPath );
return `` ;
}
return match ; // Keep original if not found
});
}
Relative to Absolute (Push)
From src/sync/emails.ts:203-231, resolveRelativeImageReferences:
export function resolveRelativeImageReferences (
content : string ,
emailDir : string ,
syncedImages : Record < string , SyncedImageInfo >,
) : string {
const references = findRelativeImageReferences ( content );
let processedContent = content ;
for ( const ref of references ) {
const absolutePath = path . resolve ( emailDir , ref . relativePath );
const matchingImage = Object . values ( syncedImages ). find (
( img ) => img . localPath === absolutePath ,
);
if ( matchingImage ) {
processedContent = replaceImageReference (
processedContent ,
ref . match ,
matchingImage . url ,
ref . altText ,
);
}
}
return processedContent ;
}
Resource Types
The CLI syncs different types of resources with different strategies.
Resource Interface
From src/sync/types.ts:19-24:
export type Resource < Model , SerializedModel > = {
get ( configuration : Configuration ) : Promise < Model | null >;
set ( value : Model , configuration : Configuration ) : Promise < OperationResult >;
serialize : ( r : Model ) => SerializedModel ;
deserialize : ( s : SerializedModel ) => Model ;
};
Available Resources
From src/sync/index.ts:40-53:
export const BASE_RESOURCES = [
AUTOMATIONS_RESOURCE ,
NEWSLETTER_RESOURCE ,
SNIPPETS_RESOURCE ,
];
export const RESOURCES = [
AUTOMATIONS_RESOURCE ,
EMAILS_RESOURCE ,
IMAGES_RESOURCE ,
NEWSLETTER_RESOURCE ,
SNIPPETS_RESOURCE ,
];
Base resources are synced without special handling. Emails and images require special path conversion logic.
Operation Results
Both pull and push operations return detailed statistics:
export type OperationResult = {
updated : number ; // Items modified
created : number ; // New items added
deleted : number ; // Items removed
failed : number ; // Operations that failed
};
Example output:
emails pushed: 3 updated, 1 created, 0 deleted, 0 failed
images pushed: 0 updated, 2 created, 0 deleted, 0 failed
automations pushed: 1 updated, 0 created, 0 deleted, 0 failed
Best Practices
Pull before push
Always pull latest changes before pushing to avoid conflicts. buttondown pull
# Make your changes
buttondown push
Commit sync state
Consider committing .buttondown.json to version control if working in a team, but be aware it contains absolute paths.
Use consistent directories
Use the same --directory flag for all operations. # Good
buttondown pull -d ./newsletter
buttondown push -d ./newsletter
# Bad - will cause issues
buttondown pull -d ./newsletter
buttondown push -d ./different-folder
Review changes before pushing
Check what’s changed using git diff or your editor before pushing.
Troubleshooting
Images not syncing correctly
If images aren’t converting properly:
Check that images are in the media/ directory
Verify .buttondown.json contains the image mapping
Try pulling fresh to rebuild the state:
rm .buttondown.json
buttondown pull
“Email not found” errors
If push fails with email not found errors:
The email may have been deleted on Buttondown
Remove the id field from the frontmatter to create a new email
Pull fresh to sync the latest state
State file corruption
If .buttondown.json gets corrupted:
# Delete the state file
rm .buttondown.json
# Pull fresh to rebuild
buttondown pull
Deleting .buttondown.json will cause the CLI to re-download all images and lose track of previous uploads. Use with caution.