Wednesday, June 23, 2021

Little UX Guidelines

Problem and Audience

A good understanding of a system's UX design drives the architecture of the underlying software that implement that design. The CSS rules, the site map and navigation experience, and the javascript component hierarchy and state management all rely on the developer's mental model of the design she is implementing. Unfortunately, many software developers like myself struggle with UX design. I have held wrong ideas about the relationship between design and software - like believing that design is a separate less technical (less valuable) process from software development, or that an arbitrary design can be layered on top of a web site after it is built (I'll transition to hugo, and slap a nice hugo theme on the site; we'll just change the CSS; we'll build a skin-able system). In fact, it is difficult to build a web site with a consistent overall UX design implemented in a way that can evolve over time and support simple user customization (like a dark theme) while maintaining a comprehensible code base. The good thing about being bad at web design is that there are many opportunities to learn and improve. The bad thing about being bad at web design is that my site sucks - which is the only thing a user cares about.

Design and developer teams need to work together to agree on a mental model for a site's structure and behavior, then codify that model in UX guidelines. Implementing UX guidelines is an evolutionary process that yields artifacts like documentation explaining the high level concepts of the design, tutorials, how-tos, design tools, CSS baselines, component libraries, and SOP's for the processes that shape the teams' daily work.

The UX guidelines for a large organization can become a sprawling manifesto (like Google's material design or Apple's human interface guidelines), but it doesn't have to be complicated for small teams. The important task for the design and dev teams is to come up with a way to effectively communicate and record the ideas that connect design to development in UX guidelines, and agree on a contract that a design and its implementation must both conform to the guides. For example, if the UX guide defines three high level page elements (navigation, content, whitespace, and actions), then a designer should not introduce a new type of element (media player, user documentation, feedback form) without also working through a process to extend the UX guide and its surrounding tools. Anyway, that's my thinking as of this morning, and this document is a small beginning for littleware's UX guide.

The "Bla Guide" model may work well for managing the interaction between other teams as well. It is easy to imagine security, infrastructure and operations, hr, product management, and qa guidelines that are similar to UX guides in their complexity, tooling, and evolution. Inevitably we will need "guidelines for guidelines".

Littleware UX Guidelines

Elements of a page

The elements of a littleware web page may each be classified as either content, metadata, whitespace, or actions. The content is the information that the page wants to present to the user, or more generally where the page engages in a conversation with the user. The content of a blog post would be the blog's essay. The content of a feedback form would be the form. The content of a data dashboard would be the data presentation.

Actions are elements like buttons and forms that present a call to action to the visitor. The "Add to Basket" button on a product detail page is an action, and so is the "enter your e-mail to download our marketing pdf" form on a CRM teaser page. An action is usually a child element of an enclosing content block.

Metadata presents non-content information on topics like the site, page content, author, or publisher. The navigation elements in the page header are metadata, and so are the various "About us" links in the footer. Metadata should be easy to access and understand, but it should not distract from the content.

Whitespace is the empty space that separates content, metadata, and action blocks.

CSS variables for page elements

Littleware's base style helper defines a series of CSS properties (variables) and rules for rendering different elements of a page with a consistent color and font scheme based on the element type. A site may override these variables to define its own style.

Content regions should define style rules with the --lw-primary-text-color, --lw-primary-bg-color, and --lw-primary-font-family variables. We assume section (<section>) blocks hold content, and we define separate CSS classes to allow different background and border colors for different content blocks - lw-section-block1, lw-section-block2, etc. We decided that using a different background color (or even a gradient) for content sections was too distracting, so we use color in more subtle ways like applying it to the bottom border of content sections and the border of content tiles. Since CSS properties cascade in a cool way, the different lw-section-block... CSS classes can each define its own border color property (--lw-section-border-color: var(--lw-sec1-border-color);) that contained elements like tiles can leverage.

:root {
    --lw-primary-text-color: #222222;
    --lw-primary-bg-color: #fefefe;
    --lw-secondary-bg-color: #fafafa;
    --lw-primary-font-family: 'Oswald script=all rev=4', Verdana, sans-serif;
    --lw-sec1-border-color: #bb38b7;
    --lw-sec1-bg-gradient: linear-gradient(var(--lw-primary-bg-color), #fad7f6);
    --lw-sec2-border-color: #0bf749;
    --lw-sec2-bg-gradient: linear-gradient(var(--lw-primary-bg-color), #f1fff1);
    ...
}

...

section {
    font-family: var(--lw-primary-font-family);
    background-color: var(--lw-primary-bg-color);
    color: var(--lw-primary-text-color);
    padding: 10px 5px;
}

.lw-section-block1 {
    font-family: var(--lw-primary-font-family);
    --lw-section-border-color: var(--lw-sec1-border-color);
    border-bottom: thin solid var(--lw-section-border-color);
    min-height: 100px;
    background-color: var(--lw-primary-bg-color);
}

.lw-section-block1_gradient {
    background: var(--lw-sec1-bg-gradient);
    background-color: var(--lw-primary-bg-color);
}

...

/*--- rules for tiles ---- */

.lw-tile-container {
    display: flex;
    flex-wrap: wrap;
    background-color: var(--lw-whitespace-bg-color);
}

.lw-tile {
    width: 300px;
    height: 250px;
    padding: 10px;
    margin: 10px;
    border-radius: 5px;
    border: solid thin var(--lw-section-border-color);
    overflow: hidden;
    background-color: var(--lw-primary-bg-color);
}

The set of CSS rules for metadata-type elements has its own font-family and color scheme. A background color gradient helps distinguish metadata blocks from the content elements that a visitor should focus on.

:root {
    ...
    --lw-secondary-text-color: #777;
    --lw-secondary-bg-color: #fafafa;
    --lw-header-background-color: var(--lw-primary-bg-color);
    --lw-secondary-font-family: 'Noto Sans', sans-serif;
    --lw-nav-border-color: #0BDAF7;
    --lw-nav-bg-gradient: linear-gradient(var(--lw-header-background-color), #f0fdff);
    ...
}

...

h1,h2,h3,h4 {
    color: var(--lw-secondary-text-color);
    font-weight: normal;
    font-family: var(--lw-secondary-font-family);
    margin-top: 10px;
    margin-bottom: 10px;
}

header {
    font-family: var(--lw-secondary-font-family);
    background-color: var(--lw-secondary-bg-color);
    color: var(--lw-secondary-text-color);
}

footer {
    font-family: var(--lw-secondary-font-family);
    background-color: var(--lw-secondary-bg-color);
    color: var(--lw-secondary-text-color);
}

.lw-nav-block {
    font-family: var(--lw-secondary-font-family);
    border-bottom: thin solid var(--lw-nav-border-color);
    background-color: var(--lw-secondary-bg-color);
}

.lw-nav-block_gradient {
    background: var(--lw-nav-bg-gradient);
    background-color: var(--lw-secondary-bg-color);
}

...

Finally, the whitespace separating different content and metadata blocks has its own background color to clarify the page structure for the user.

:root {
    --lw-whitespace-bg-color: #f2f2f4;
    ...
}

...

body {
    ...
    background-color: var(--lw-whitespace-bg-color);
}

...
/*--- rules for tiles ---- */

.lw-tile-container {
    display: flex;
    flex-wrap: wrap;
    background-color: var(--lw-whitespace-bg-color);
}

...

The Rotating Hamburger and OG Javascript

We added a hamburger menu to the header of https://apps.frickjack.com to allow a visitor to easily navigate between the different parts of the site. I like the CSS animation that rotates the hamburger to an "X" when opening, then back to a hamburger when closing. We implement that hamburger and the other drop-down menus on the site with a lw-drop-down web component that wraps the purecss menu.

The lw-drop-down web component takes advantage of some of the drop-down and hamburger example code from the purecss web site. The sample code is written in an old-school jQuery style where the code keeps all its state in the DOM by tracking the custom CSS rules attached to different elements. For example, when the user clicks on the hamburger, the javascript event listener directly modifies the CSS rules attached to different DOM elements. Bootstrap is a popular framework with components that rely on this style of code.

We intend to refactor our lw-drop-down code to a more modern MVC (or component) style that tracks the UI state in javascript variables that drive a render template. For example, when a user clicks on the hamburger, a javascript event listener modifies the javascript variables that feed a template system that manipulates the DOM. React, Angular, Ember, and Vue follow this pattern.

Hugo Shortcodes for Content Tiles

Hugo shortcodes provide a mechanism to safely embed custom html into the markdown files that a hugo content author works with. We provide simple tilecanvas and tile shortcodes to allow an author to indicate that her content may be presented as tiles. The shortcodes are defined in the "littleware" hugo theme under the little-apps github repo.

tilecanvas:

<div class="lw-tile-container">
    {{ .Inner }}
</div>

tile:

<div class="lw-tile">
    {{ .Inner | markdownify }}
</div>

Summary

UX designers and software developers need to clearly communicate UX guidelines that establish a shared mental model for how to describe an implement the user experience.

Thursday, June 10, 2021

Jamstack Cloudformation

Problem and Audience

A simple clouformation template makes it easy to stamp out AWS infrastructure for a jamstack web site. A jamstack is a web site composed of static presentation (non-API) resources (html, css, javascript) assembled at build time - as opposed to a site that requires some kind of dynamic server side rendering of resources at request time. A nice feature of this architecture is that it allows a site to be served inexpensively from a serverless object storage system like S3. We manage https://apps.frickjack.com by copying presentation resources to an S3 bucket that acts as an origin for a cloudfront distribution.

We originally setup the infrastructure for https://apps.frickjack.com by clicking around the AWS web console (like this), but we want to tear down that infrastructure, and move to cloudformation managed infrastructure to realize the benefits of infrastructure as code including:

  • it is less work to manipulate infrastructure by editing json files than clicking through the web console
  • a cloudformation template allows us to deploy multiple copies of our architecture (for different products, test environments, etc) in a consistent way
  • cloudformation templates capture best practices and institutional conventions, and allow infrastructure to evolve over time
  • tracking the cloudformation template and parameters in git gives an audit trail
  • cloudformation makes automation easy

Jamstack Requirements

We have a handful of requirements for our jamstack infrastructure. First we want the S3 bucket to remain private and encrypted. Even though the content of the bucket is publicly accessible via the cloudfront CDN, making the origin bucket conform to standard S3 best practices simplifies compliance, since we do not need to note exceptions to organization policies that expect a bucket to be private and encrypted. The cloudfront CDN is granted access to the private bucket via a bucket policy that gives read permission to an origin access identity associated with the cloudfront distribution.

Most of our other requirements are addressed by adjusting knobs on our cloudfront configuration. For example, we want all http traffic to be redirected to https. We want to use an input parameter to our cloudformation stack to associate an alias domain with the distribution - we manage the DNS setup for the alias in another Route53 stack. We use another input parameter to feed the ARN of ourACM-managed TLS certificate. We configure cloudfront to require clients to use TLS 1.2 or better.

Finally, our little stack tools make it easy to follow the tagging conventions that we want to enforce across our infrastructure.

the little stack

We setup the following template (also in github) to start managing our jamstack infrastructure with cloudformation. The template takes advantage of the nunjucks extensions to cloudformation templates supported by our little stack automation.

One "gotcha" that we ran into was that we originally intended to setup a new stack with the apps.frickjack.com domain alias already in use by our live CDN, copy our web content to the new bucket (modify our codebuild CI pipeline configuration to do that for us), then update DNS to point the apps.frickjack.com domain at the new CDN. However, it turns out that cloudfront does not allow two distributions to have the same alias, so we setup our new CDN with a temporary alias, then took a few minutes of downtime while we removed the apps.frickjack.com alias from the old CDN, then updated our new stack.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Metadata": {
    "License": "Apache-2.0"
  },
  "Description":
    "Generalize AWS s3-cdn sample template from - https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/S3/S3_Website_With_CloudFront_Distribution.yaml",
  "Parameters": {
    "CertificateArn": {
      "Type": "String",
      "Description": "ACM Certificate ARN"
    },
    "DomainName": {
      "Type": "String",
      "Description": "The DNS name of the new cloudfront distro",
      "AllowedPattern": "(?!-)[a-zA-Z0-9-.]{1,63}(?<!-)",
      "ConstraintDescription": "must be a valid DNS zone name."
    },
    "BucketSuffix": {
      "Type": "String",
      "Description": "The suffix of the bucket name - prefix is account number",
      "AllowedPattern": "[a-zA-Z0-9-]{1,63}",
      "ConstraintDescription": "must be a valid S3-DNS name"
    }
  },
  "Resources": {
    "S3Bucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "AccessControl": "Private",
        "BucketName": { "Fn::Join": [ "-", [ { "Ref" : "AWS::AccountId" }, { "Ref": "BucketSuffix" } ] ] },
        "BucketEncryption": {
          "ServerSideEncryptionConfiguration" : [ 
            {
              "BucketKeyEnabled" : "true",
              "ServerSideEncryptionByDefault" : {
                "SSEAlgorithm": "AES256"
              }
            }
          ]
        },        
        "WebsiteConfiguration": {
          "IndexDocument": "index.html",
          "ErrorDocument": "error.html"
        },
        "Tags": [
          {{ stackTagsStr }}
        ]
      }
    },
    "CloudFrontOriginIdentity": {
      "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
      "Properties": {
        "CloudFrontOriginAccessIdentityConfig": {
          "Comment": "origin identity"
        }
      }
    },
    "BucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": { "Ref": "S3Bucket" },
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            { 
              "Effect": "Allow",
              "Principal": {
                "AWS": { "Fn::Sub": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginIdentity}" }
              },
              "Action": "s3:GetObject",
              "Resource": { "Fn::Sub": "arn:aws:s3:::${S3Bucket}/*" }
            }
          ]
        }
      }
    },
    "CdnDistribution": {
      "Type": "AWS::CloudFront::Distribution",
      "Properties": {
        "DistributionConfig": {
          "Aliases": [
            { "Ref": "DomainName" }
          ],
          "Origins": [
            { 
              "DomainName": { "Fn::Sub": "${S3Bucket}.s3.${AWS::Region}.amazonaws.com" },
              "Id": "S3-private-bucket",
              "S3OriginConfig": {
                "OriginAccessIdentity": { "Fn::Sub": "origin-access-identity/cloudfront/${CloudFrontOriginIdentity}" }
              }
            }
          ],
          "DefaultRootObject": "index.html",
          "Enabled": "true",
          "Comment": { "Ref": "DomainName" },
          "DefaultCacheBehavior": {
            "AllowedMethods": [ "GET", "HEAD", "OPTIONS" ],
            "CachedMethods": [ "GET", "HEAD", "OPTIONS" ],
            "TargetOriginId": "S3-private-bucket",
            "ForwardedValues": {
              "QueryString": "false",
              "Cookies": {
                "Forward": "none"
              }
            },
            "ViewerProtocolPolicy": "redirect-to-https"
          },
          "ViewerCertificate": {
            "AcmCertificateArn": { "Ref": "CertificateArn" },
            "SslSupportMethod": "sni-only",
            "MinimumProtocolVersion": "TLSv1.2_2019"
          }
        },
        "Tags": [
          { "Key": "Name", "Value": { "Ref": "DomainName" } },
          {{ stackTagsStr }}
        ]
      }
    }
  },
  "Outputs": {
    "CdnAliasDomain": {
      "Value": { "Fn::GetAtt": [ "CdnDistribution", "DomainName" ] },
      "Description": "The URL of the newly created website"
    },
    "BucketName": {
      "Value": { "Ref": "S3Bucket" },
      "Description": "Name of S3 bucket to hold website content"
    }
  }
}

Summary

A simple clouformation template makes it easy to stamp out AWS infrastructure for a jamstack web site.

Monday, June 07, 2021

route53 and cloudformation

Problem and Audience

Managing route53 records with cloudformation is a good idea for the same reasons that tracking other resources with cloudformation (or terraform or whatever) is better than clicking around in the web console - namely:

  • it is less work to manipulate route53 records by editing json files than clicking through the web console
  • tracking the cloudformation template and parameters in git (or whatever code repository) gives an audit trail
  • cloudformation makes automation easy

We setup the following cloudformation template to start managing our simple route53 zones with cloudformation. The template takes advantage of the nunjucks extensions to cloudformation templates supported by our little stack automation.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "DomainName": {
      "Type": "String",
      "Description": "the domain name"
    }
  },
  "Resources": {
    "HostedZone": {
      "Type" : "AWS::Route53::HostedZone",
      "Properties" : {
          "HostedZoneTags" : [ 
            {{ stackTagsStr }}
          ],
          "Name" : { "Ref": "DomainName" }
        }
    }

    {% if stackVariables.aliasList.length %}
    ,
      {% for item in stackVariables.aliasList %}
      "AliasA{{ item.resourceName }}": {
        "Type" : "AWS::Route53::RecordSet",
        "Properties" : {
            "AliasTarget" : {
              "DNSName" : "{{ item.target }}",
              "HostedZoneId": "{{ item.hostedZoneId }}"
            },
            "Comment" : "{{ item.comment }}",
            "HostedZoneId" : { "Ref": "HostedZone" },
            "Name" : "{{ item.domainName }}",
            "Type" : "A"
          }
      },
      "AliasAaaa{{ item.resourceName }}": {
        "Type" : "AWS::Route53::RecordSet",
        "Properties" : {
            "AliasTarget" : {
              "DNSName" : "{{ item.target }}",
              "HostedZoneId": "{{ item.hostedZoneId }}"
            },
            "Comment" : "{{ item.comment }}",
            "HostedZoneId" : { "Ref": "HostedZone" },
            "Name" : "{{ item.domainName }}",
            "Type" : "AAAA"
          }
      }

      {% if not loop.last %} , {% endif %}
      {% endfor %}
    {% endif %}

    {% if stackVariables.mxConfig %}
    ,
    "MX": {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
          "Comment" : "mx mail config",
          "HostedZoneId" : { "Ref": "HostedZone" },
          "Name" : { "Ref": "DomainName" },
          "ResourceRecords" : {{ stackVariables.mxConfig.resourceRecords | dump }},
          "TTL" : "900",
          "Type" : "MX"
        }
    }
    {% endif %}

    {% if stackVariables.cnameList.length %}
    ,
    {% for item in stackVariables.cnameList %}
    "Cname{{item.resourceName}}": {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
          "Comment" : "{{ item.comment }}",
          "HostedZoneId" : { "Ref": "HostedZone" },
          "Name" : "{{ item.domainName }}",
          "ResourceRecords" : [ "{{ item.target }}" ],
          "TTL" : "900",
          "Type" : "CNAME"
        }
    }
    {% if not loop.last %} , {% endif %}
    {% endfor %}

    {% endif %}

    {% if stackVariables.txtList.length %}
    ,
    {% for item in stackVariables.txtList %}
    "Txt{{item.resourceName}}": {
      "Type" : "AWS::Route53::RecordSet",
      "Properties" : {
          "Comment" : "{{ item.comment }}",
          "HostedZoneId" : { "Ref": "HostedZone" },
          "Name" : { "Ref": "DomainName" },
          "ResourceRecords" : [ {{ item.txtValue | dump }} ],
          "TTL" : "900",
          "Type" : "TXT"
        }
    }
    {% if not loop.last %} , {% endif %}
    {% endfor %}

    {% endif %}

  },

  "Outputs": {
    "NameServers": {
      "Description": "hosted zone nameservers",
      "Value": { "Fn::Join": [",", { "Fn::GetAtt": [ "HostedZone", "NameServers" ] }] }
    }
  }
}

Summary

Managing route53 zones with cloudformation is the right thing to do.

Wednesday, June 02, 2021

Porting https://apps.frickjack.com to hugo

Problem and Audience

A web site may be architected in various ways: from a simple collection of static html, javascript, and css files behind a web server; to a site administered by a content management system; to a web application built on custom server or client side software.

The appropriate design for a particular site is the one that best balances the requirements of the site's different stakeholders. For example, the marketing team may primarily view the site as one part of customer relationship management (CRM). The customer support team might want to publish documentation to the site, or provide tools for a customer to request support. The product team may want the site to provide access to the product's user console application.

Each stakeholder may need to update the site in different ways. The marketing and customer support teams may require a simple mechanism to submit edits for review and publication. The product development team may want to build and test code updates with a CICD pipeline. Neither of those teams may be well versed in graphic design.

apps.frickjack.com and hugo

We just completed a project to transition https://apps.frickjack.com to the hugo static site generator. The https://apps.frickjack.com property acts both as my personal site and as a sandbox for experimenting with the littleware software stack. It is a static multi-page site served from an S3 bucket with a few small javascript web applications and some early integrations with web API's.

The hugo transition allowed us to move the content and theme management for https://apps.frickjack.com from an idiosynchratic templating system to the well documented and community supported process that hugo implements. Hugo's theme design also pushed us to think about what we want the site to provide to its visitors, and whether the landing page clearly conveys those use cases. For example, https://www.salesforce.com/ has a straight forward explanation of what the company is, "the #1 CRM ...", and a call to action "sign up for your free account".

The content management process is still developer oriented in that site updates are managed via github pull requests, and a codebuild CI job updates the site, but the content markdown and theme templates are now managed in their own hugo directory hierarchy. The site's github repo includes more details at https://github.com/frickjack/little-apps/blob/master/Notes/howto/devTest.md.

Summary

We transitioned https://apps.frickjack.com to the hugo static site generator to further decouple the site's content and theme management from the javascript code implementing the dynamic services and applications on the site. We also reorganized the site to better support the experiences we want the site to provide to visitors.