{"version":"https://jsonfeed.org/version/1.1","title":"Engineer — Brand — Portfolio and notes","home_page_url":"https://brand.engineer.company/","feed_url":"https://brand.engineer.company/feed.json","description":"Engineer ApS — software development \u0026 IT consulting in Copenhagen, Denmark. A portfolio of proven engineering achievements across software, data, cloud and IT.","language":"en","icon":"https://brand.engineer.company/assets/images/brand/card.webp","favicon":"https://brand.engineer.company/assets/icons/apple/apple-touch-icon.png","authors":[{"name":"Engineer ApS","url":"https://brand.engineer.company/"}],"items":[{"id":"https://brand.engineer.company/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/","url":"https://brand.engineer.company/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/","title":"Increased brand popularity by 200x through successful brand development across offline and online platforms.","summary":"Grew brand reach roughly 200x across offline and online platforms, turning an unknown newcomer into a recognised name.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Engineer ApS was a brand‑new consultancy with real technical depth and almost nobody who\u0026rsquo;d heard of it. That\u0026rsquo;s a specific kind of frustrating: the skill is there, the work would be good, but none of it matters if the clients and partners who\u0026rsquo;d want it don\u0026rsquo;t know you exist. A young firm has to be seen before it can win anything.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The task was to build a brand people would actually recognise — across both the offline and online sides — and to turn the firm\u0026rsquo;s engineering credibility into visible market presence, rather than leaving it as a well‑kept secret.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The brand was built deliberately and kept consistent. It started with a clear identity — a voice, a visual language, and a portfolio that led with concrete engineering outcomes instead of the vague \u0026ldquo;we deliver value\u0026rdquo; language everyone else uses. Then it ran across the channels that matter for this kind of firm — the website, LinkedIn, GitHub, in‑person events — with every touchpoint saying the same thing rather than each drifting off on its own. The throughline was leading with real case studies and real results, so the credibility was something you could see evidence for, not just a claim.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Brand reach grew around 200‑fold across the offline and online platforms — an unknown newcomer turned into something people actually recognised. And it wasn\u0026rsquo;t vanity reach; the visibility started producing a steady flow of inbound conversations and opportunities that simply hadn\u0026rsquo;t been there before.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Brand \u0026 Marketing","Product \u0026 Requirements","Stakeholder \u0026 Reporting","Brand, Marketing \u0026 SEO","Product Strategy \u0026 Requirements"]},{"id":"https://brand.engineer.company/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/","url":"https://brand.engineer.company/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/","title":"Drove brand engagement and loyalty by 100% through market trend analysis and consumer behavior insights.","summary":"Doubled brand engagement and loyalty (+100%) using market-trend analysis and consumer-behaviour insight to win a returning audience.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e As the visibility climbed, Engineer ApS was reaching more people — but the engagement was thin. People noticed and moved on; the early interest wasn\u0026rsquo;t turning into relationships that lasted. Reach without engagement is just noise, and the brand was making noise more than connections.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The aim was to deepen the engagement and the loyalty, and to do it by actually looking at what the market and the audience were responding to rather than trusting gut instinct.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e So the brand became data‑informed instead of intuition‑led. That meant looking at the market trends and how the audience actually behaved across the channels — not what anyone assumed they\u0026rsquo;d like, but what they demonstrably engaged with. It surfaced the topics and formats that drew real attention, and the content and outreach got steered toward those. The important part was tightening the loop: watch what landed, publish more of that shape next time, and let each cycle be a bit better‑aimed than the last.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Engagement and loyalty doubled — a 100% improvement — with an audience that came back and engaged rather than glancing once and leaving, and noticeably stronger relationships with prospects and partners. The brand stopped broadcasting into the void and started building something that returned.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Brand \u0026 Marketing","Data Analytics","Product \u0026 Requirements","Stakeholder \u0026 Reporting","Brand, Marketing \u0026 SEO","Data Analytics \u0026 BI Dashboards","Product Strategy \u0026 Requirements"]},{"id":"https://brand.engineer.company/portfolio/drove-client-web-success-by-combining-custom-website-91/","url":"https://brand.engineer.company/portfolio/drove-client-web-success-by-combining-custom-website-91/","title":"Drove client web success by combining custom website development and design with SEO, content strategy and copywriting.","summary":"Drove client web success by combining custom website development and design with SEO, content strategy and copywriting.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Clients didn\u0026rsquo;t really want a website; they wanted the thing a website is supposed to do for them — to be found, to bring in customers, to actually work as a channel. A beautiful site nobody can find is a failure that looks like a success.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Combining the build and the growth side into one offering was the task, rather than handing over a site and wishing them luck.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The two halves were put together — custom website development and design on one side, SEO, content strategy and copywriting on the other — so a client got something that was both well‑built and actually discoverable. Those usually get treated as separate jobs, which is how you end up with a gorgeous site that ranks nowhere, or a well‑optimised site that\u0026rsquo;s unpleasant to use. Doing both meant the site was designed from the start to be found, not optimised as an afterthought.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Clients ended up with stronger visibility and more engagement — their sites working as genuine growth channels rather than online brochures. Building the thing and making it findable in one go is what turned a website from a cost into something that actually earned its keep.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Brand \u0026 Marketing","Frontend Engineering","Product \u0026 Requirements","UX / UI Design","Web Development","Brand, Marketing \u0026 SEO","UI/UX Design \u0026 Design Systems","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/","url":"https://brand.engineer.company/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/","title":"Replaced 27 rendered font sizes whose nearest neighbours were 0.6% apart with a six‑step Major Third scale, and wrote the linter that fails the twenty‑eighth.","summary":"Six sizes where there were twenty-seven, with a stated ratio, a stated measure and a check that refuses the next unplanned value.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e A measurement of the site\u0026rsquo;s rendered text found twenty‑seven distinct font sizes in use. Several were separated by less than one per cent — five values between 0.8 and 0.85 of the body size were all live at once, which is a difference no reader can perceive and every future editor will add to. Two of the headings were not sized by the stylesheet at all and were falling through to the browser\u0026rsquo;s own defaults, a ratio of 1.33 that matched nothing else on the page.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The sizes had to become a scale with a stated ratio, and something had to prevent the twenty‑eighth.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The scale is a Major Third, ratio 1.25, six named steps from fine print to display, each a token rather than a value. Headings are mapped explicitly onto steps instead of inheriting whatever the browser thinks, which is what fixed the two that had no size of their own. Only the bottom step is floored, so fine print stays legible on a phone without the whole scale being pinned. Logotypes are exempt by name rather than by accident. The linter is the part that makes it hold: it renders the site and fails on the twenty‑eighth distinct size. It has already earned its place twice — it caught a heading arriving at 1.17 of the body size, which is not a step of anything, and named the ratio in the failure message; and it caught inline code at 0.9, which is precisely the kind of value the scale exists to prevent. Alongside the scale went a measure of about seventy‑two characters, replacing an inherited fixed width that produced ninety‑one characters on a review page and a hundred and ten on the contact page.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Six sizes where there were twenty‑seven, with a stated ratio, a stated measure and a check that refuses the next unplanned value. The constraint is real and occasionally inconvenient: a design that wants a size between two steps has to move to a step or argue for changing the scale, and that argument has been had and lost more than once.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Design Systems \u0026 UI","Documentation","Frontend Engineering","Testing \u0026 QA","UX / UI Design","Brand, Marketing \u0026 SEO","Frontend Development","UI/UX Design \u0026 Design Systems"]},{"id":"https://brand.engineer.company/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/","url":"https://brand.engineer.company/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/","title":"Generated 495 achievement pages across three languages from a read‑only SQLite export, with the page address authored as data so that correcting a sentence no longer moved the page and broke the link.","summary":"Four hundred and ninety-five pages generate from one source, correcting a statement costs nothing, and the addresses this site has ever published continue to…","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The company\u0026rsquo;s portfolio content is authored in a separate database — the same one that produces the CV — and the website has to publish it as pages, in three languages, without the two copies drifting apart. The naive approach, writing the pages by hand and keeping them in step, fails on the first correction.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The website had to generate its content from the database as a build input, with page addresses that survive the sentences on them being rewritten.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e An exporter reads the database read‑only and writes one page per achievement per language — 495 pages — plus the taxonomy and services data the templates need. It uses only the standard library, so the site\u0026rsquo;s build has no dependency on the generator\u0026rsquo;s environment, and the committed output means the site builds standalone. The single most consequential decision in it concerns addresses. The site used to derive a page\u0026rsquo;s URL from the leading words of its English statement, so correcting a sentence silently moved the page and broke every link to it — a site whose argument for itself is that it corrects things, charging itself a dead link every time it did. The address is now authored as data: one row per address per achievement, the first is current and every later one is a retired address the site emits as a redirect. Two other traps are recorded from the same work. A period comparison against a text column silently matched all thirty‑two rows because of how the database assigns type affinity across a comparison. And the language list is now the single axis the write loop, the data files and the queries all derive from, so adding a fourth language is one map entry rather than a search.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Four hundred and ninety‑five pages generate from one source, correcting a statement costs nothing, and the addresses this site has ever published continue to answer. The exporter also owns exactly one presentation decision — how services group into themed sections — and that is deliberate: everything else it writes is the database\u0026rsquo;s, and a group heading missing a language fails the export rather than rendering English over translated content.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Data Pipelines (ETL/ELT)","Databases","Frontend Engineering","Internationalization","Python","SQL","Web Development","Brand, Marketing \u0026 SEO","Data Pipeline Development (ETL/ELT)","Internationalization \u0026 Localization","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/portfolio/closed-the-colour-system-at-28-colours-133/","url":"https://brand.engineer.company/portfolio/closed-the-colour-system-at-28-colours-133/","title":"Closed the colour system at 28 documented colours with a linter that fails a value painted but undocumented, documented but unpainted, mis‑measured, respelled as a literal, or within a perceptual distance of 0.02 of one already there.","summary":"The palette is a closed, measured set that cannot quietly grow, and the two near-duplicate spellings that prompted it are gone.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Colour on a site with a dark default, a light theme, a print stylesheet, a forced‑colours mode and an illustrated brand does not stay a small set on its own. It had already started to drift in the way it always does: two spellings of the same colour sitting close enough that nobody could tell them apart, values re‑typed as literals beside the tokens that defined them, and documented colours that nothing painted.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The palette had to become a closed set with a stated distinctness threshold, and something had to enforce the closure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Twenty‑eight colours, each documented with the contrast ratio it measures on the surface it appears on, and eleven brand values declared once as tokens. The threshold is numeric rather than editorial: two colours closer than a perceptual distance of 0.02 in a uniform colour space are one colour with two spellings. The linter fails five different ways — a value painted but undocumented, documented but unpainted, recorded with the wrong ratio, within the threshold of one already there, or a brand value re‑spelled as a literal. It found two immediately: a theme colour that existed in two spellings 0.018 apart, and a background colour standing in for a wall it sat 0.021 from. Transparency uses relative colour syntax rather than the mixing function, specifically because the guard cannot see through a mix and a palette guard that can be evaded is not a guard. The rule that goes with it is short: change a token rather than a rule, add a colour only when none fits, and never lower a documented ratio to make a design work.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The palette is a closed, measured set that cannot quietly grow, and the two near‑duplicate spellings that prompted it are gone. It is a constraint that occasionally says no — a design wanting a slightly different blue has to take the one that exists or make the case for a twenty‑ninth colour, and that case has to include the ratio.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Design Systems \u0026 UI","Documentation","Frontend Engineering","Testing \u0026 QA","UX / UI Design","Brand, Marketing \u0026 SEO","Frontend Development","UI/UX Design \u0026 Design Systems"]},{"id":"https://brand.engineer.company/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/","url":"https://brand.engineer.company/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/","title":"Replaced 963 anonymous structured‑data blocks that restated the company 1,671 times with one linked graph of 16 types minted from stable origin identifiers.","summary":"One graph with stable identity replaces 963 anonymous restatements, and the pages carry less rather than more.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The site emitted structured data the way most sites do: a block per page, each one describing the organisation again from scratch. Across the site that came to 963 blocks restating the same company 1,671 times, anonymously — no stable identifier anywhere, so nothing consuming it could tell that the organisation on one page was the organisation on another.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The structured data had to become one graph with stable identity, rather than a pile of blocks that happen to contain the same words.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Identifiers are now minted from the site\u0026rsquo;s origin — one for the organisation, one for the site, one for the person — and every block references them rather than restating their contents. The identifiers are origin‑wide rather than per language, because the company is the same company in Danish. Sixteen types are in play, covering the service catalogue and its offers, the reviews and the work they describe, the collections and their breadcrumbs. Two decisions kept the weight down. List pages publish their items by URL only rather than inlining them, which was measured: naming them on the portfolio index would have meant 107 full statements and a 52 per cent increase in the compressed page. And the check that guards it does not merely validate syntax — it asserts that every block parses, that every identifier resolves, and that every URL the graph names was actually built, so a graph pointing at a page that does not exist fails the gate.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e One graph with stable identity replaces 963 anonymous restatements, and the pages carry less rather than more. The measurement that started the work is worth keeping in mind: the site had been emitting the same organisation description 1,671 times without anything being able to join them up.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Automation \u0026 CI/CD","Brand \u0026 Marketing","Data Governance","Frontend Engineering","Web Development","Brand, Marketing \u0026 SEO","Data Governance \u0026 Quality","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/","url":"https://brand.engineer.company/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/","title":"Cut the stylesheet bundle from 87 KB to 31 KB by taking a base64 font out of it, and dropped 756 KB across 22 files that were published on every deploy and referenced by nothing.","summary":"The bundle is 31 KB instead of 87, 756 KB of dead assets stopped being deployed, and each decision has a measurement attached — including the one that went the…","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The site\u0026rsquo;s stylesheet bundle was 87 KB, which for a document site with no framework is most of a page weight spent before any content arrives. The site also published a directory of icons on every deploy, generated once and referenced by nothing since.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The weight had to come down without the design changing, and it had to come down for a reason that could be pointed at rather than by general tidying.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Measurement first. Two thirds of the largest stylesheet was one base64‑encoded font — about 56 KB of text encoding about 42 KB of font — inlined for a first‑paint benefit that a preloaded external file gets anyway, and paid for on every page whether the weight was needed or not. Taking it out took that file from 84 KB to 28 and the whole bundle from 87 to 31. The three font weights are preloaded instead, and the third joined the list only when field data showed it closing the longest critical chain rather than because three sounded complete. Separately, an audit of what the deploy actually published found twenty‑one generated icons and a duplicate configuration file, 756 KB, shipped every time and referenced from nowhere; and the site\u0026rsquo;s icon file itself came down from 145 KB to 15. A modern image format was evaluated for the brand mark, measured, and declined — it came out larger, and the selection mechanism picks by order rather than by size, so a modern browser would have taken the heavier file everywhere. Two checks now hold the line: no photograph may be displayed wider than half its source pixels, and every illustration must be published at a size its own pixels support at both standard and high density.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The bundle is 31 KB instead of 87, 756 KB of dead assets stopped being deployed, and each decision has a measurement attached — including the one that went the other way, which is the more useful record of the two.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Design Systems \u0026 UI","Frontend Engineering","Performance Tuning","Web Development","Brand, Marketing \u0026 SEO","Frontend Development","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/","url":"https://brand.engineer.company/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/","title":"Fixed a sitemap where 172 of 176 URLs shared one modification timestamp, by taking the date from git history after establishing that the export rewrites every file on every run.","summary":"A re-sync after a month of content edits now touches eight files of 186 instead of all of them, and the sitemap says something true.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The site\u0026rsquo;s sitemap told search engines that 172 of its 176 pages had last changed at the same instant. That is not a subtle inaccuracy — a modification date is a promise to a crawler that a page\u0026rsquo;s content changed, and a site making that promise 172 times simultaneously is either telling the truth about a full rewrite or telling nobody anything useful.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The dates had to describe the content rather than the file, without inventing precision the repository does not have.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The cause was that the date came from the file\u0026rsquo;s modification time and the content export rewrites every file on every run, so a single sync stamped the whole corpus. The fix moves the source to the version history, with a fallback chain that tries an explicit field first, then the commit history, then the file. The trap in that fix is worth recording: the literal field name has to appear in the list or the generator never reads it, so a configuration that looks like it prefers an authored date but omits the name silently ignores every authored date. Two other date decisions came out of the same work. The database now carries when each record was written and last revised, kept deliberately separate from when the work happened — those are years apart and conflating them would date a page written this year to a decade ago. And rows in that file are optional and nothing is derived, because the repository\u0026rsquo;s own history begins after the content did: a missing date leaves the field empty rather than recording a migration, on the principle that a precise wrong number is worse than an absent one, since only the wrong one gets believed.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e A re‑sync after a month of content edits now touches eight files of 186 instead of all of them, and the sitemap says something true. The general rule went into the metadata document alongside it, because the same trap applies to every field that has a fallback chain.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Brand \u0026 Marketing","Python","Testing \u0026 QA","Web Development","Brand, Marketing \u0026 SEO","DevOps \u0026 CI/CD Automation","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/portfolio/built-a-48-token-design-system-160/","url":"https://brand.engineer.company/portfolio/built-a-48-token-design-system-160/","title":"Built a 48‑token design system on the platform's own glass material and wrote the linter that rejects a magic number, a hardcoded font size, or an animation that ignores the reduce‑motion preference.","summary":"Forty-eight tokens describe the whole interface, the appearance follows the system, and none of the three erosions can be committed.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e An interface built by adding views accumulates values: a corner radius here, a fourteen‑point font there, a two‑tenths‑of‑a‑second animation somewhere else. Individually each is reasonable and collectively they are a design that cannot be changed, because there is no such thing as \u0026ldquo;the corner radius\u0026rdquo; to change — there are forty of them, slightly different, spread across fifty files.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The visual vocabulary had to be reduced to a named set, expressed against the platform\u0026rsquo;s own material rather than reinvented, and defended by a check so it stays reduced.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The system is forty‑eight tokens in six groups: spacing, typography, colour, radius, elevation and motion. Colour and material are built on the platform\u0026rsquo;s semantic colours and its glass material rather than fixed values, which is what makes the interface follow the system appearance, the accent colour and the contrast settings without any code that watches for them. Typography maps to the platform\u0026rsquo;s text styles so it scales with the user\u0026rsquo;s size preference instead of pinning a point value. The linter rejects three things: a numeric literal where a spacing or radius token belongs, a hardcoded font size anywhere, and an animation declared without honouring the reduce‑motion preference. The third is the one that would otherwise erode fastest, because an animation is added in a moment of polish and the preference check is the part that gets skipped — so the rule makes the animation itself impossible to write without it, rather than asking anyone to remember.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Forty‑eight tokens describe the whole interface, the appearance follows the system, and none of the three erosions can be committed. The limit is that a token system constrains consistency and not quality — everything now matches, and matching is not the same as being well designed, which is a judgement no linter in this project makes.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Design Systems \u0026 UI","Frontend Engineering","Testing \u0026 QA","UX / UI Design","Brand, Marketing \u0026 SEO","Frontend Development","UI/UX Design \u0026 Design Systems"]},{"id":"https://brand.engineer.company/portfolio/built-the-visual-identity-from-two-drawings-164/","url":"https://brand.engineer.company/portfolio/built-the-visual-identity-from-two-drawings-164/","title":"Built the company's icon and favicon sets from two hand‑made drawings, with every derived asset regenerated by script and a check that fails when a derived file was committed before its source.","summary":"Two drawings produce every published icon, regeneration is one command, and a stale asset fails the build.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e A small company\u0026rsquo;s visual identity is usually bought, generated, or assembled from stock, and the result is an identity that belongs to nobody. The alternative problem is worse: hand‑made artwork that exists only as exported files, where the source drawing is lost, the exports are edited directly, and within a year the versions in use disagree with each other and no original remains to settle it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The identity had to be drawn rather than sourced, and the pipeline from drawing to published asset had to be reproducible, so that every file on the site is derivable rather than kept.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Two hand‑made drawings sit at the root of the generated artwork — the company mark and the cog that became the favicon — and every icon the site publishes is produced from them. The mark is a raster drawing; the cog is a vector. From these, scripts produce every derived form: the favicon set in its required sizes, the touch icons, and the launch images at each device size. The generation is a task anyone can run, so the question \u0026ldquo;where did this file come from\u0026rdquo; has an answer that is a command. The staleness check is the piece that makes it hold: it compares the commit date of every derived asset against its source and fails when a derived file was committed first, which catches the exact failure this design exists to prevent — someone edits the source, forgets to regenerate, and the published site keeps showing artwork that no longer matches the original. The rule that derived files are never edited by hand is stated where the sources live.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Two drawings produce every published icon, regeneration is one command, and a stale asset fails the build. The trade‑off is a hard dependency on the toolchain: the derived files are committed so the site builds anywhere, but changing the identity requires the generation tooling to still work, and a source format that stops being readable takes the whole identity with it.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Brand \u0026 Marketing","Design Systems \u0026 UI","Testing \u0026 QA","UX / UI Design","Web Development","Brand, Marketing \u0026 SEO","UI/UX Design \u0026 Design Systems","Website Development \u0026 CMS"]},{"id":"https://brand.engineer.company/notes/solana-mobile-wallet-deeplinks/","url":"https://brand.engineer.company/notes/solana-mobile-wallet-deeplinks/","title":"Opening a dApp inside Solana mobile wallets","summary":"Why Phantom, Solflare and Backpack browse deep links fail on mobile, and the exact formats, trigger rules and fixes that make them work.","content_html":"\u003cp\u003eA React dApp built on \u003ccode\u003e@solana/wallet-adapter-react\u003c/code\u003e connects desktop wallets\nwithout trouble, but on a phone the same flow falls apart: the wallet has to\nopen the dApp inside its own in-app browser, and the deep links that should\nmake that happen quietly do not. Backpack lands on a \u0026ldquo;download the app\u0026rdquo; page;\nSolflare opens the app but never the site; every variant seems to fail. We took\nthe problem apart, and it turned out to be four separate problems wearing one\nsymptom.\u003c/p\u003e\n\u003ch2 id=\"the-four-problems\"\u003eThe four problems\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eThe Backpack link was malformed.\u003c/strong\u003e The only documented format is\n\u003ccode\u003ehttps://backpack.app/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e — a universal link with\nthe target URL in the path and a required \u003ccode\u003eref\u003c/code\u003e. A custom-scheme guess like\n\u003ccode\u003ebackpack://ul/v1/browse?url=...\u003c/code\u003e matches no route the app registers, so the\nuser ends on the wallet\u0026rsquo;s install page.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSolflare needs its universal link too:\u003c/strong\u003e\n\u003ccode\u003ehttps://solflare.com/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e, not the bare\n\u003ccode\u003esolflare://\u003c/code\u003e scheme. A bare scheme can launch the app without routing it —\nwhich is exactly \u0026ldquo;the app opens, but the site tab has to be opened by hand\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBoth parameters must be encoded.\u003c/strong\u003e \u003ccode\u003eurl\u003c/code\u003e is the full absolute dApp address\nand \u003ccode\u003eref\u003c/code\u003e is the requesting origin, each passed through \u003ccode\u003eencodeURIComponent\u003c/code\u003e.\nAn unencoded \u003ccode\u003e?\u003c/code\u003e or \u003ccode\u003e\u0026amp;\u003c/code\u003e in the target corrupts the parse, and the wallet\nopens on its home screen instead of the browser tab.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe trigger matters as much as the link.\u003c/strong\u003e Universal links only switch apps\non a navigation the operating system trusts — and they deliberately do\nnothing when pasted into the address bar, which is also how a perfectly\ncorrect link \u0026ldquo;fails\u0026rdquo; during testing.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-documented-formats\"\u003eThe documented formats\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003ePhantom: \u003ccode\u003ehttps://phantom.app/ul/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e — no \u003ccode\u003e/v1\u003c/code\u003e in this\none.\u003c/li\u003e\n\u003cli\u003eSolflare: \u003ccode\u003ehttps://solflare.com/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eBackpack: \u003ccode\u003ehttps://backpack.app/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOne pattern serves all three:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003econst WALLET_BROWSE = {\n  phantom: (url, ref) =\u0026gt;\n    `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n  solflare: (url, ref) =\u0026gt;\n    `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n  backpack: (url, ref) =\u0026gt;\n    `https://backpack.app/ul/v1/browse/${url}?ref=${ref}`,\n};\n\nfunction walletBrowseLink(\n  walletName,\n  targetUrl = window.location.href,\n) {\n  const build = WALLET_BROWSE[walletName.toLowerCase()];\n  if (!build) return null;\n  return build(\n    encodeURIComponent(targetUrl),\n    encodeURIComponent(window.location.origin),\n  );\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"triggering-the-link-so-ios-and-android-accept-it\"\u003eTriggering the link so iOS and Android accept it\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRender a real anchor, precomputed.\u003c/strong\u003e A plain\n\u003ccode\u003e\u0026lt;a href={walletBrowseLink('phantom')}\u0026gt;\u003c/code\u003e is the most reliable trigger on both\nplatforms.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIf it must be programmatic\u003c/strong\u003e, assign \u003ccode\u003ewindow.location.href\u003c/code\u003e synchronously\ninside the tap handler — no \u003ccode\u003eawait\u003c/code\u003e, no \u003ccode\u003efetch\u003c/code\u003e, no \u003ccode\u003esetTimeout\u003c/code\u003e first. After\nasynchronous work the gesture context is gone, and iOS falls back to the\nwallet\u0026rsquo;s website. Never \u003ccode\u003ewindow.open\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNever test by pasting into the address bar.\u003c/strong\u003e Universal links deliberately\ndo not fire there; test with a tapped link or a QR code scanned by the\ncamera.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMind the messenger webviews.\u003c/strong\u003e Opened inside Telegram\u0026rsquo;s or Instagram\u0026rsquo;s\nin-app browser, universal links are frequently swallowed and the wallet\u0026rsquo;s\nplain website loads instead. User-agent detection is heuristic at best, so\nalso give users a visible escape hatch: \u0026ldquo;open in Safari or Chrome, then\nconnect\u0026rdquo;.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-bigger-fix-on-android\"\u003eThe bigger fix on Android\u003c/h2\u003e\n\n\u003cp\u003eHand-rolled deep links are the iOS story. On Android, Solana Mobile\u0026rsquo;s Mobile\nWallet Adapter lets a dApp running in the mobile browser connect straight to\nthe installed wallet app, with no in-app-browser detour at all. Recent versions\nof \u003ccode\u003e@solana/wallet-adapter-react\u003c/code\u003e register the mobile adapter automatically, so\nupgrading the wallet-adapter packages can fix Android by itself. The target\narchitecture: Mobile Wallet Adapter on Android, browse universal links on iOS,\nwhere Apple allows no equivalent.\u003c/p\u003e\n\u003ch2 id=\"verifying-on-a-device\"\u003eVerifying on a device\u003c/h2\u003e\n\n\u003col\u003e\n\u003cli\u003eReal device, wallet installed, link opened from the system browser — not\nfrom a messenger.\u003c/li\u003e\n\u003cli\u003eTap a rendered link or scan a QR code; never paste into the address bar.\u003c/li\u003e\n\u003cli\u003eConfirm the wallet opens and the dApp loads in its in-app browser tab — the\nsecond half is the part that fails.\u003c/li\u003e\n\u003cli\u003eRepeat without the wallet installed: the universal link should degrade to\nthe wallet\u0026rsquo;s website. That page appearing while the app is installed means\nthe link or the trigger is still wrong.\u003c/li\u003e\n\u003cli\u003eThen test the messenger path, and add the \u0026ldquo;open in browser\u0026rdquo; hint if it\nfails there.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.phantom.com/phantom-deeplinks/deeplinks-ios-and-android\"\u003ePhantom: deep links on iOS and Android\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\"\u003eSolflare: the Browse deep link\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.backpack.app/deeplinks/other-methods/browse\"\u003eBackpack: the Browse deep link\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.solanamobile.com/mobile-wallet-adapter/mobile-apps\"\u003eSolana Mobile: Mobile Wallet Adapter\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","date_published":"2026-08-10T00:00:00Z","date_modified":"2026-09-13T01:45:50+02:00","language":"en"}]}