Render the artwork
Silhouettes, cartridge drawings, photographs and 3D models, and the one decision that matters: a URL, raw bytes, or a data URI.
URL, bytes or data URI
Every asset can be had three ways, and picking wrongly is what makes a page heavy or a feed unreadable. The media page lists the kinds; this is which representation to ask for.
| Where it is going | Ask for | Why |
|---|---|---|
| An <img> on a web page | url, or sizes.display | The browser caches it, the CDN serves it, and your HTML stays small. |
| Markup you want to style | format=json | The SVG source, inlined into your own DOM, so CSS can fill and colour it. |
| An email, a PDF, a feed row | format=datauri | Nothing else can make a second request. The bytes travel with the document. |
| A file on disk or in your own bucket | the SDK raw helpers | Bytes plus content type, with the CDN redirect already followed. |
| A 3D viewer | /model | A GLB, served as model/gltf-binary. Load it once and cache it hard; it is the largest thing we serve. |
Line-art silhouettes
Most of the catalog has a drawing, which is why a silhouette is the right fallback when a photograph does not exist. format decides what you get back, and stroke_width with stroke_color injects an outline so the shape reads on any background.
TypeScript
Inline the source when the drawing has to inherit your theme:
import { GunSpec } from '@buun_group/gunspec-sdk' const gunspec = new GunSpec({ apiKey: process.env.GUNSPEC_API_KEY }) // an inlined SVG can be filled by CSS, an <img> cannot.const { data } = await gunspec.firearms.getSilhouette('glock-g17', { format: 'json' })const markup = data.svg- The drawings are filled shapes, so on a dark surface they disappear. Inject a stroke, or invert them in CSS when you inline the source.
- Inline the SVG when the drawing must match your theme. An
<img>cannot be recoloured; inlined markup can. - They scale to any size with no derivative, which is what makes them the cheap choice for list rows and comparison tables.
When a data URI is the right answer
A data URI is base64, so it is about a third larger than the file and it cannot be cached separately from the document that carries it. That is a fair trade in exactly three places.
| Situation | Verdict |
|---|---|
| HTML email, or a PDF you generate | Yes. The client will not fetch a remote image, and often will not be online. |
| A product feed or spreadsheet export a partner ingests | Yes, for small drawings. It removes a fetch step from their importer. |
| A list page in a browser | No. Forty rows of base64 is a megabyte of HTML that caches with the page and dies with it. |
| An avatar-sized icon repeated across a site | No. One cached file beats the same bytes inlined on every page. |
| A canvas or PDF renderer that cannot do async fetches | Yes. That is the case the format exists for. |
Cartridge drawings
Every catalogued load draws its own cartridge at /bullet.svg, and the drawings are to scale against each other, so a row of them is a size comparison rather than decoration. Pair them with the numbers from cartridges and ballistics.
TypeScript
Returns the SVG as a string.
const svg = await gunspec.ammunition.getBulletSvg('9x19mm-124gr-fmj')Photographs, renders and their derivatives
A hosted asset carries sizes with a thumb, a display and a full version, plus width, height, alt where one is written, and credit where the catalogue records it. Serve the derivative you display and render the credit, which the licence page explains is a condition rather than a courtesy.
// Photographs and renders come with their derivatives, so the// browser downloads the size it is going to display.const { data: media } = await gunspec.firearms.listMedia('glock-g17', { kind: 'photo' }) const hero = media[0]const img = { src: hero.sizes?.display ?? hero.url, srcSet: hero.sizes ? `${hero.sizes.thumb} 320w, ${hero.sizes.display} 1024w, ${hero.sizes.full} 2048w` : undefined, // Null, not an empty string, where nobody has written alt text. alt: hero.alt ?? `${firearm.name}`, width: hero.width, height: hero.height,} // The credit is a condition of use, not a caption you may drop.if (hero.credit) caption.textContent = hero.creditDownloading 3D models and files
Records that carry one serve a GLB at /model, as model/gltf-binary. It is the largest thing this API returns, so fetch it once, store it, and serve it from your own origin. The SDK raw helpers hand you the bytes with the content type already resolved.
- Filter a list with
has_3d_model=truewhen a feature only works for records that have one. - Load it lazily. A viewer that downloads a model before the reader has asked to see it is the heaviest page on your site.
- The same applies to schematics and large photographs: fetch once into your own storage rather than proxying every view.
Serving them well
Four habits that keep an image-heavy page fast and keep your quota for data rather than pictures.
- Assets are served from the CDN and are safe to cache hard. It is the metadata call that has a lifetime; see caching.
- Copy what you use into your own bucket if you need guaranteed availability and your own cache headers. The licence allows serving them in your product.
- Set
widthandheightfrom the metadata so the page does not reflow when the image lands. - Alt text is
nullwhere nobody has written it. Fall back to the record name; never render an empty string.