Skip to content

Publishing a static website

This post describes the journey how to publish a static website for our employees. This could include documentation, API Specs, ...

Where did we came from

Previously, our documentation hub lived on GitHub Pages, and publishing was straightforward through a standard GitHub Actions workflow (an automated process that publishes updates).

Documentation Hub Image

The downside was access: only people with a GitHub license could view it, which excluded many colleagues. We wanted to keep the simple publishing experience while making the site available to every employee without needing a GitHub account.

Architecture at a glance

  • S3 bucket (private) as origin; CloudFront Origin Access Control (or Origin Access Identity) limits direct access
  • CloudFront distribution with:
    • Managed cache policy
    • Brotli/Gzip compression
  • SSL certificate and Route 53 DNS for the custom domain
  • Okta-backed authentication at the edge (Lambda@Edge) - tokens validated, access gated by cookies

This keeps the origin locked down, serves content quickly worldwide, and adds auth without changing how teams build their static sites.

CI/CD: from push to publish

A standardized GitHub Actions workflow:

  • Builds the site (any static generator)
    For the documentation hub, we use React Router, but any static page would work
  • Deploys infra changes via CDK
  • Syncs build artifacts to S3
  • Performs targeted CloudFront invalidations when needed

Teams integrate the construct and provide a small config block. Publishing is then triggered from their repository pipeline.

The StaticWebsite construct

To make this repeatable, we packaged the pattern in a CDK construct. It wires CloudFront, S3, Route 53, ACM, cache/security headers, and Okta authentication.

Example usage (TypeScript):

const website = new StaticWebsite(this, "website", {
  name: "website-name",
  domainName: "example.statista.dev",
  hostedZoneName: "example.statista.dev",
  sourceFolder: "/path/to/static/pages",
});

new route53.ARecord(this, "websiteAlias", {
  zone: website.hostedZone,
  target: website.recordTarget,
  recordName: "example.statista.dev",
});

An example CDK setup

The documentation hub is there as a reference if you want to publish a static page. The setup adds a staging/production environment setup which makes it slightly more complex that just a static page without additional environments.

Have a look at the repository and it's sources.

Semi-static pages

The construct allows to extend from it to change CDN configuration or add additional origins to the CDN.

A rough example how to do that would look like this:

class CustomStaticWebsite extends StaticWebsite {
  protected getDistributionProps(): cloudfront.DistributionProps {
    return {
      ...super.getDistributionProps(),
      additionalBehaviors: {
        origin: ... // e.g. an HTTP origin or an ALB
        edgeLambdas: [{
          // add these to ensure your origin is also protected with Okta
          {
            functionVersion: this.oktaAuthenticator.lambda.currentVersion,
            eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
          },
        }]
      }
    }
  }
}

The result

  • The construct is the product: a paved path for internal static sites
  • Teams get consistent security, caching, and auth by default
  • Publishing is fast, repeatable, and doesnโ€™t require bespoke infra work

If your team has a static site to publish internally, use the shared workflow, configure the construct, and ship.