Build Carbonmade

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project.

estimated effort weekendthe files for this build are in the project pack

ContentProjects as folders on disk, one metadata file each, images beside themGeneratorA local static build producing plain files, nothing running in productionImagesOne derivative ladder generated from untouched originalsEditorA local admin that edits metadata and ordering, never raw markupPublishBuild into a fresh directory, verify, then swap, keeping the previous build

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the generator, the admin and the image pipeline.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why The whole derivative pipeline rests on it, and the ones that quietly drop orientation are the reason portfolios launch sideways.

    Get it Choose a maintained native library for your runtime and read its documentation on metadata handling before you build the ladder.

    Verify A deliberately rotated fixture comes out upright with no orientation tag

  3. usually free at portfolio traffic; the domain is the real cost

    Why Publishing is a file upload, and the domain is the thing that makes the portfolio yours rather than a tenant of someone else's brand.

    Get it Any static host or object storage with a CDN in front, plus your own domain pointed at it.

    Verify A published build is reachable at your domain over https

  4. have readyfree

    Why A pipeline tested on three tidy exports fails on the panorama, the portrait scan and the thirty megabyte camera file that are already in your archive.

    Get it Copy a representative selection into the content directory, including at least one rotated photograph and one very large file.

    Verify A full build completes and reports derivative sizes per project

  5. decidefree

    Why The priority image rule needs a definition, and deciding it per page after the fact means deciding it inconsistently.

    Get it Write down the rule, for example the project cover on a project page and the first grid item on the index.

    Verify The rule is in the README and enforced by the build

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published
- media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal)
- derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete
- pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has
- builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms)
- redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes
- alt_text is a column rather than an optional front matter key, and a published image without one fails the build.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
CONTENT_DIRrequired./contentProject folders and originals. This is the portfolio; back it up first.
BUILD_DIRrequired./buildsNumbered build outputs. The previous one is kept for rollback.
DERIVATIVE_WIDTHSrequired480,960,1440,2048The ladder. Changing it invalidates the derivative cache.
DERIVATIVE_FORMATrequiredavifPrimary output format. A fallback is always emitted alongside.
SITE_URLrequiredhttps://example.comUsed for canonical links, the sitemap and redirect targets.
BUILDS_KEPTrequired3How many previous builds stay on disk for rollback.

The build, in order

  1. Content on disk that outlives the generator

    Everything the site is made of lives in plain files you could edit with any text editor.

    1. Your content should not need this program to be readable.

      Files src/content/scan.tstests/rebuild.test.ts

    2. Folders get tidied; URLs you sent people do not.

      Files src/content/schema.ts

    3. A missing image is usually a typo, not a decision.

      Files src/content/scan.ts

    done when · tick each as it passes
    watch out
    • Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output.
  2. Derivatives from originals nobody touches

    Every displayed image is generated from an untouched original at a fixed ladder of widths.

    1. Otherwise the browser rotates an already rotated image.

      Files src/images/derive.tstests/orientation.test.ts

    2. A rebuild of an unchanged portfolio should be fast.

      Files src/images/cache.ts

    3. This is the file you cannot get back.

      Files tests/originals-immutable.test.ts

    done when · tick each as it passes
    watch out
    • Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures.
    • Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test.
  3. The page that loads fast on a phone

    Each page declares exactly one priority image and everything else defers, with the build enforcing it.

    1. The hero is the number that decides how the site feels.

      Files src/render/image.tstests/priority-image.test.ts

    2. Reflow is the other half of a slow-feeling page.

      Files src/render/image.ts

    3. A number nobody records is a regression nobody notices.

      Files scripts/measure.mjssrc/build/record.ts

    done when · tick each as it passes
    watch out
    • Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold.
    • Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing.
  4. Editing without breaking the site

    A local admin edits metadata, reorders images and previews, and cannot publish something invalid.

    1. Two renderers means two behaviours and one surprise.

      Files src/admin/preview.ts

    2. It is the only accessibility rule that matters here.

      Files src/build/validate.tstests/alt-text.test.ts

    3. The folders are canonical, including the order.

      Files src/admin/reorder.ts

    done when · tick each as it passes
    watch out
    • A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce.
    • Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more website builders.