← Back to Blog

Stop letting ArcGIS Online pick your thumbnails: brand every item in your org with 60 lines of Python

Stop letting ArcGIS Online pick your thumbnails: brand every item in your org with 60 lines of Python

Open your ArcGIS Online content page and look at it the way a client would. If you’re like most of us, it’s a patchwork: auto-generated map extents, gray placeholder grids, one or two hand-made images from that one time you had twenty spare minutes. Every item works fine — but the page doesn’t say “this organization sweats the details.”

Thumbnails are the first thing anyone sees in your content gallery, in group pages, in search results, and in every “add layer from my content” dialog your colleagues use. They’re also the single cheapest branding surface in the entire ArcGIS system: one PNG template plus one short Python notebook can restamp your whole org in minutes. We run this at All Things Spatial across 350+ items; here’s the complete workflow — template design, the SVG anatomy, and the ArcGIS API for Python batch job — with everything you need to run it on your own account.

What you’ll build

A notebook that walks every item you own in ArcGIS Online (feature services first), composites the item’s type and title onto a branded 600×400 template, and applies the result with one API call per item. Before/after:

Before ArcGIS Online My Content grid where every item shows the same generic default world-map thumbnail
Before — freshly published items all wear the same generic world map AGOL assigns by default. Nothing on the card says what the item is or whose it is.
After The same ArcGIS Online grid after running the tool: every item now shows a branded thumbnail with its type and title
After — one run of the notebook (or the Pro tool) and the same items read as branded: item type up top, title in the footer, the ATS lockup pinned in the corner.

Sample outputs straight from the pipeline (these are real renders, not mockups):

Feature service branded thumbnail sample
Feature service — single-line type
Coincident Points feature service branded thumbnail sample with a two-line title
Two-line title — auto-fit to the footer band

Why 600×400

Esri’s documentation recommends a thumbnail of 600 × 400 pixels (3:2) or larger, in PNG/JPEG/GIF — PNG renders crispest (Configure item details — ArcGIS Online Help). Anything smaller gets upscaled and looks soft; anything off-ratio gets cropped in some surfaces. Design at exactly 600×400 and you control every pixel everywhere the thumbnail appears. (Esri’s own Python API sample notebook “Examining item thumbnail size” is a good audit companion — it flags undersized thumbnails across an org.)

Part 1 — Design the template once

The master is an SVG (ours is built in Inkscape 1.4, 600×400 export). The design splits into three horizontal bands, two of which deliberately stay empty:

BandContentsFilled by
Header (white, top ~25%)Brand iconography in the corners (satellite, location pin) — center stays emptyPython stamps the item TYPE here, centered, brand blue #1C77FF
Body (middle ~45%)The visual identity: four raster-style map panels (hydrography blues, thematic heat, land cover greens, imagery)Static — part of the template
Footer (black, bottom ~30%)ATS logo lockup pinned right — left side stays emptyPython stamps the item TITLE here, white, left-aligned
The base 600x400 ATS thumbnail template
The base template — two text zones (header center, footer left) reserved on purpose.

Because those two zones are the only dynamic part of the image, the script needs nothing more than two pixel rectangles — everything else is baked into the template. Here is exactly where each string lands on the 600×400 canvas. The coordinates are the real values the renderer uses (HEADER_BOX and FOOTER_BOX); image coordinates run from the top-left origin, +x right and +y down, which is how Pillow addresses pixels.

600 by 400 thumbnail template coordinate map The two pixel rectangles where the tool auto-fits the item type (header, centered) and item title (footer, left-aligned) onto the static template. 0 100 200 300 400 500 600 0 100 200 300 400 X (pixels, +x →) Y (pixels, +y ↓) (0,0) top-left origin BODY — static map panels (baked into template) FEATURE SERVICE (95, 14) centre axis x = 300 HEADER_BOX · item TYPE x = 362 (x+w) Hydrography Network Centerlines FOOTER_BOX · item TITLE (24, 296) HEADER_BOX — item TYPE { x: 95, y: 14, w: 410, h: 110 } centred on x = 300 · auto-fits 54→30 px (≤2 lines) · brand blue, UPPERCASE FOOTER_BOX — item TITLE { x: 24, y: 296, w: 338, h: 98 } left-aligned at x = 24 · auto-fits 40→20 px (≤3 lines) · white · ellipsis only if still too long
The two text-injection zones on the 600×400 pixel grid. The item type is centred within HEADER_BOX (on the x = 300 axis); the item title is left-aligned from x = 24 inside FOOTER_BOX. Both blocks auto-fit by measured pixel width — the renderer only ever computes positions inside these two rectangles.

Design rules that make the automation work:

  1. Reserve the text zones in the design, not in code. The header center and footer left are empty on purpose. The Python script only needs two rectangles’ coordinates — it never has to detect where it’s safe to draw.
  2. Pin fixed art to the edges. Corner icons and a right-aligned logo survive any text length. Centered art would fight the dynamic text.
  3. Export the SVG to PNG once. The batch job composites onto the PNG raster — fast, no SVG rendering dependency at run time, and the template can’t drift between runs.

Why SVG as the master? Because the day you rebrand — new logo, new accent color — you edit one vector file, re-export one PNG, and re-run the notebook. The whole org updates in one coffee break. That’s the actual payoff of this architecture: rebranding becomes a batch job, not a quarter-long project.

The ATS thumbnail template SVG open in Inkscape; the Layers panel shows Item Type and Title Area as separate empty layers
The master template open in Inkscape. The two reserved zones are real, empty layers — Item Type (header) and Title Area (footer); the script fills only those. To rebrand, you edit this one vector file, re-export the PNG, and re-run — the whole org updates in a coffee break.

Part 2 — The notebook

The full notebook — plus a no-code ArcGIS Pro tool version — are yours to download below. It runs as written — verified against ArcGIS API for Python 2.4.2 (current release) in a clean Linux environment, dry-run against live ArcGIS Online. Walkthrough of the four moving parts:

2.1 Authenticate — without putting credentials in the notebook

try:
    gis = GIS("home")           # ArcGIS Notebooks / hosted environment
except Exception:
    username = os.environ.get("AGOL_USERNAME") or input("ArcGIS Online username: ")
    password = os.environ.get("AGOL_PASSWORD") or getpass.getpass("Password: ")
    gis = GIS("https://www.arcgis.com", username, password)

Inside ArcGIS Notebooks (or Pro), GIS("home") / GIS("pro") inherits the signed-in identity — zero credentials in the file. Anywhere else, environment variables plus a getpass prompt keep secrets out of the notebook, out of git, and out of your published sample. If you run this routinely from a desktop, the GIS(profile=...) pattern stores the password in your OS credential manager instead (Working with different authentication schemes).

Never ship a notebook with a literal username/password — and remember that saved cell outputs leak too (item names, usernames, org URLs). Clear outputs before you commit.

2.2 Find the items

query = f"owner:{me.username}"
items = gis.content.search(query=query, item_type="Feature Service", max_items=400)

ContentManager.search scopes to your organization automatically; owner: narrows to your items, and the item_type parameter is the clean way to take feature services first (the full query grammar lives in the ArcGIS REST search reference). Start narrow; when the feature-service set looks right, set ITEM_TYPE = None and let it restamp everything — web maps, apps, scenes, service definitions.

2.3 Render — Pillow does the typography

The render function opens the template, stamps the type into the header and the title into the footer, and saves one PNG per item. The interesting bits are the guardrails, not the drawing:

Long title truncated with an ellipsis
Only a genuinely oversized title is ellipsized — short titles render big, long ones step down to use the whole band.

Because it measures real pixel widths (draw.textlength) instead of counting characters, the same function renders a 7-character title big and bold and steps a 70-character one down to fit — the artwork never has to be measured, only the text.

2.4 Apply — one line per item

item.update(thumbnail=str(thumb_path))

Item.update takes a local path (or URL) for thumbnail — that’s the whole portal write. A dedicated Item.update_thumbnail method also exists (adds Base64 and URL input modes) if you prefer the explicit form.

The notebook ships dry-run by default: it renders the full set to a local folder for review and touches nothing until you flip DRY_RUN = False. Changed your mind on one item? delete_thumbnail() removes it, and create_thumbnail() regenerates the standard extent-based one for a feature service.

An ArcGIS Online item details page for a hosted feature layer, showing the branded thumbnail in context with the item title and type
The payoff in context — a branded item on its ArcGIS Online details page. The same thumbnail now identifies the item at a glance: in the gallery, in search, and in every “add layer from my content” dialog your colleagues use.
The notebook output cell showing RENDERED (dry run) lines scrolling, one per item, across the organization
The dry-run log — one RENDERED line per item, scrolling across the org. Nothing is written to the portal until you flip DRY_RUN = False; the same loop then prints UPDATED as it applies each thumbnail.

Results

On our account this restamped 355 items in a single run — feature services, web maps, apps, scenes, service definitions — with a handful of skips logged and retried. The content page went from patchwork to portfolio: every item answers “what is this and whose is it?” at a glance, and the org reads as designed rather than accumulated.

A note on the default auto-thumbnails: they’re doing honest work — an extent snapshot is genuinely useful metadata. This isn’t about defaults being bad; it’s about a content gallery being a brand surface you already own. If a default extent map serves a particular item better, keep it.

The reusable checklist

  1. Design a 600×400 PNG template with two reserved text zones (header = type, footer = title); keep fixed art pinned to edges. Master in SVG.
  2. Record the two boxes’ pixel geometry and a font auto-fit range (hi→lo); the renderer measures real pixel widths and picks the largest size that fits — short text big, long text stepped down.
  3. Authenticate with GIS("home") / env vars / getpass — never literals; clear notebook outputs before sharing.
  4. gis.content.search(query=f"owner:{me.username}", item_type="Feature Service") — start narrow, widen after review.
  5. Dry-run the full render set to disk; eyeball it.
  6. Flip the flag; item.update(thumbnail=...) does the rest.
  7. Re-run any time titles change, items are added — or you rebrand.

Make it your own — the notebook or the Pro tool

We ship this two ways, both driven by the same render core, so you can run it however you work — grab one or both:

The notebook exposes a single CONFIG cell — change the variables, Run All, done. The Pro tool is the same logic wrapped as a geoprocessing tool: drop the .pyt into a toolbox and fill in the dialog — no code at all. Both ship dry-run on, so the first pass only renders PNGs to a folder for review.

Tool parameterNotebook variableWhat it controlsDefault
Branding template (PNG)TEMPLATE_PATHYour 600×400 PNG template
Output folderOUTPUT_DIRWhere rendered PNGs are written
ScopeSCOPE / FOLDERSWhole account, or only chosen foldersEntire account
Item type filterITEM_TYPEe.g. Feature Service; blank = all typesblank
Max title charactersMAX_TITLE_CHARSPre-cap before an ellipsis50
Max title linesMAX_TITLE_LINESFooter title line budget3
Dry runDRY_RUNRender only vs. write thumbnails to the portalOn
Brand color (hex)BRAND_BLUEHeader (item type) color#1C77FF
Max itemsMAX_ITEMSSafety cap on items processed2000

Fitting it to your brand is two edits: point TEMPLATE_PATH at your own 600×400 PNG, then set HEADER_BOX and FOOTER_BOX (the rectangles from the map above) to your template’s two reserved zones. The auto-fit ranges (TYPE_SIZE_HI/LO, TITLE_SIZE_HI/LO) tune how big the text may get. It’s plain Python — no build step, and the only dependencies are Pillow and the ArcGIS API for Python, both of which ship with ArcGIS Pro.

Run it safely: keep Dry run on for the first pass and eyeball the output folder; credentials come from your signed-in Pro / Notebooks session (GIS("pro") / GIS("home")) or environment variables — never hard-coded. If you share your edited notebook, clear the cell outputs first.

We run this across whole ArcGIS Online portfolios.

Branded thumbnails, portal hygiene, and Python automation like the workflow above — set up once, run on a schedule. If you’d rather have it wired in than build it yourself, let’s talk.

Book a free intro call