diff --git a/astro.config.ts b/astro.config.ts index 543f820..8e32d15 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,10 +1,46 @@ -import { defineConfig } from "astro/config"; +import { defineConfig, fontProviders } from "astro/config"; import sitemap from "@astrojs/sitemap"; import tailwindcss from "@tailwindcss/vite"; +import { readFileSync, readdirSync } from "node:fs"; + +/** + * Last-modified date for each blog post URL, read from front matter (updatedDate, else pubDate). + * Content collections aren't available in the config, so this parses the Markdown directly. + */ +function blogLastModified() { + const dir = new URL("./src/content/blog/", import.meta.url); + const dates = new Map(); + for (const file of readdirSync(dir).filter((f) => f.endsWith(".md") && !f.startsWith("_"))) { + const frontMatter = readFileSync(new URL(file, dir), "utf8").split(/^---$/m)[1] ?? ""; + const date = (key: string) => frontMatter.match(new RegExp(`^${key}:\\s*"?([0-9-]+)`, "m"))?.[1]; + const lastmod = date("updatedDate") ?? date("pubDate"); + if (lastmod) dates.set(`/blog/${file.replace(/\.md$/, "")}/`, new Date(lastmod).toISOString()); + } + return dates; +} +const blogDates = blogLastModified(); export default defineConfig({ site: "https://www.heyal.co.uk", - integrations: [sitemap()], + integrations: [ + sitemap({ + serialize(item) { + const lastmod = blogDates.get(new URL(item.url).pathname); + return lastmod ? { ...item, lastmod } : item; + }, + }), + ], + fonts: [ + { + name: "Manrope", + cssVariable: "--font-manrope", + provider: fontProviders.google(), + weights: ["400 800"], + styles: ["normal"], + subsets: ["latin"], + fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"], + }, + ], markdown: { shikiConfig: { themes: { light: "github-light", dark: "github-dark" }, diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..8b7d37e Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-32.png b/public/favicon-32.png new file mode 100644 index 0000000..9ffda65 Binary files /dev/null and b/public/favicon-32.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..4e4c5cf Binary files /dev/null and b/public/logo.png differ diff --git a/public/og-default.jpg b/public/og-default.jpg new file mode 100644 index 0000000..a7602d3 Binary files /dev/null and b/public/og-default.jpg differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..436d3c7 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://www.heyal.co.uk/sitemap-index.xml diff --git a/src/assets/og-default.jpg b/src/assets/og-default.jpg new file mode 100644 index 0000000..a7602d3 Binary files /dev/null and b/src/assets/og-default.jpg differ diff --git a/src/assets/og-default.svg b/src/assets/og-default.svg new file mode 100644 index 0000000..2d39922 --- /dev/null +++ b/src/assets/og-default.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + Heyal Solutions + + + Make shipping + software boring + + Kubernetes, infrastructure as code and delivery pipelines + diff --git a/src/content.config.ts b/src/content.config.ts index b6e745a..1154902 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -47,6 +47,10 @@ const blog = defineCollection({ heroAlt: z.string().default(""), /** Personal posts are listed on /blog but not featured on the home page. */ personal: z.boolean().default(false), + /** Topics used to pick related posts and as schema.org keywords. Lowercase, e.g. "eks", "gitops". */ + tags: z.array(z.string()).default([]), + /** The service this post relates to; linked from the end of the post. */ + service: reference("services").optional(), /** Groups multi-part posts; parts are linked to each other on the post page. */ series: z.object({ id: z.string(), title: z.string(), part: z.number().int().positive() }).optional(), /** Post-specific note on what has changed since publication (Markdown not supported). */ diff --git a/src/content/blog/bootstrapping-private-eks-argocd.md b/src/content/blog/bootstrapping-private-eks-argocd.md index bc3c3dc..0faaab7 100644 --- a/src/content/blog/bootstrapping-private-eks-argocd.md +++ b/src/content/blog/bootstrapping-private-eks-argocd.md @@ -4,6 +4,8 @@ description: "Terraform for a private-endpoint EKS cluster, GitHub Actions with pubDate: 2026-10-17 heroImage: ../../assets/blog/generated/eks-private-argocd.jpg heroAlt: "A dashed, padlocked VPC boundary around a cluster of connected nodes, with a Git branch pulling into it" +tags: [kubernetes, eks, aws, argo-cd, gitops, terraform, github-actions] +service: infrastructure-and-delivery --- A Kubernetes API server on the public internet is an attack surface most teams don't need. In regulated environments diff --git a/src/content/blog/civo-my-journey.md b/src/content/blog/civo-my-journey.md index 50dea1e..90b733e 100644 --- a/src/content/blog/civo-my-journey.md +++ b/src/content/blog/civo-my-journey.md @@ -1,10 +1,11 @@ --- -title: "Civo Kubernetes KUBE100" -description: "What I have been up to on Civo's managed k3s service" +title: "Civo Kubernetes KUBE100: First Look at Managed k3s" +description: "What k3s is and how it differs from full Kubernetes, my experience on the beta of Civo's managed k3s service, contributing to its app marketplace, and other k3s tooling." pubDate: 2020-01-07 heroImage: ../../assets/blog/civo-card.jpg -heroAlt: "civo kube100 stickers" +heroAlt: "Civo KUBE100 stickers" changes: "The KUBE100 beta ended long ago and Civo Kubernetes is now generally available, the Civo CLI has been rewritten, OpenFaaS Cloud has been retired, and k3d's commands have changed (k3d cluster create)." +tags: [kubernetes, k3s, civo, open-source] --- ## The World's First Managed K3s diff --git a/src/content/blog/developing-openfaas.md b/src/content/blog/developing-openfaas.md index 047a1b5..86647d0 100644 --- a/src/content/blog/developing-openfaas.md +++ b/src/content/blog/developing-openfaas.md @@ -1,10 +1,11 @@ --- title: "How To Get Started Contributing to OpenFaaS" -description: "Get started contributing to OpenFaaS" +description: "A step-by-step guide to your first OpenFaaS contribution: setting up Go, Docker and Kubernetes, finding an issue, forking the repository, and opening a pull request." pubDate: 2020-01-04 heroImage: ../../assets/blog/of-contributing.jpg heroAlt: "A selection of Open Source stickers and my laptop" changes: "Several projects listed here (OpenFaaS Cloud, ofc-bootstrap and the open-source inlets) have since been retired, k3d's commands have changed (k3d cluster create), and Go modules have replaced GOPATH. Check the current OpenFaaS contributing guide before starting." +tags: [openfaas, serverless, open-source, go, kubernetes] --- ## New Year New You? diff --git a/src/content/blog/docker-bham-jan.md b/src/content/blog/docker-bham-jan.md index 1f856a1..16ce4c6 100644 --- a/src/content/blog/docker-bham-jan.md +++ b/src/content/blog/docker-bham-jan.md @@ -1,10 +1,11 @@ --- -title: "Docker Birmingham January 2020" -description: "Building docker images for multiple CPU Architectures" +title: "Docker Birmingham January 2020: Multi-Architecture Images with buildx" +description: "From my Docker Birmingham meetup talk: how Docker images and manifests handle CPU architectures, and building images for ARM and x86 with docker buildx." pubDate: 2020-01-08 heroImage: ../../assets/blog/pi.jpg -heroAlt: "Raspberry Pi computer" +heroAlt: "A Raspberry Pi single-board computer" changes: "buildx has since become a standard part of Docker and multi-platform builds no longer need experimental features, so the setup described here is simpler today." +tags: [docker, multi-arch, raspberry-pi, community] --- Every two months a group of us host a meetup focused on Docker. You can find [the meetup page here](https://www.meetup.com/Docker-Birmingham/). diff --git a/src/content/blog/inlets-pro.md b/src/content/blog/inlets-pro.md index 3783b14..05689fa 100644 --- a/src/content/blog/inlets-pro.md +++ b/src/content/blog/inlets-pro.md @@ -1,10 +1,11 @@ --- title: "Exploring NAT Traversal and Tunnels with Inlets and Inlets Pro" -description: "Give anything a public IP with inlets and inlets pro" +description: "How inlets tunnels give services behind NAT or a firewall a public IP, using inletsctl and the inlets-operator, and when you need inlets PRO for non-HTTP traffic." pubDate: 2020-01-20 heroImage: ../../assets/blog/inlets-headline.png -heroAlt: "inlets pro headline image" +heroAlt: "The inlets PRO logo" changes: "The open-source inlets project has since been retired and inlets is now a commercial product, so the open-source client, download URLs, licensing and some commands shown here no longer apply. See docs.inlets.dev for current instructions." +tags: [inlets, networking, tunnels, kubernetes] --- ## Inlets! What is it? diff --git a/src/content/blog/introducing-arkade.md b/src/content/blog/introducing-arkade.md index e97cdf6..ee3da90 100644 --- a/src/content/blog/introducing-arkade.md +++ b/src/content/blog/introducing-arkade.md @@ -3,8 +3,9 @@ title: "Introducing Arkade - The Kubernetes app installer" description: "A CLI with strongly-typed flags to install charts and apps to your cluster in one command." pubDate: 2020-02-28 heroImage: ../../assets/blog/arkade-headline.png -heroAlt: "arkade headline image" +heroAlt: "The arkade logo" changes: "arkade has grown well beyond installing apps (arkade get now downloads dozens of CLI tools), and its app list, flags and defaults have changed. Tiller belonged to Helm 2, which reached end of life in 2020." +tags: [arkade, kubernetes, helm, open-source] --- ## Arkade! What is it? diff --git a/src/content/blog/loft-sh.md b/src/content/blog/loft-sh.md index bf76720..176550d 100644 --- a/src/content/blog/loft-sh.md +++ b/src/content/blog/loft-sh.md @@ -1,10 +1,12 @@ --- title: "First look at loft.sh using GitOps" -description: "Installation and configuration of loft.sh with Flux" +description: "Using loft.sh for self-service Kubernetes namespaces and virtual clusters on a shared multi-tenant platform, installed and configured with Flux GitOps." pubDate: 2022-03-15 heroImage: ../../assets/blog/loft.jpg heroAlt: "Container in the sky" changes: "Loft has since refocused on vCluster (loft.sh now redirects to vcluster.com), so the product, Helm chart, CLI and docs referenced here have changed. Flux's documentation has also moved to fluxcd.io/flux." +tags: [kubernetes, gitops, flux, multi-tenancy, vcluster] +service: platform-engineering --- ## The problem diff --git a/src/content/blog/my-2019.md b/src/content/blog/my-2019.md index 4480928..b10db89 100644 --- a/src/content/blog/my-2019.md +++ b/src/content/blog/my-2019.md @@ -1,6 +1,6 @@ --- title: "My 2019 In Review - Hello Open Source!" -description: "I look back over my 2019 and share my highlights" +description: "Looking back at 2019: 75+ pull requests to OpenFaaS and OpenFaaS Cloud, k3sup, Birmingham meetups and events, and my plans for 2020." pubDate: 2019-12-30 heroImage: ../../assets/blog/lake.jpg heroAlt: "A lake with sun in the background" diff --git a/src/content/blog/openfaas-whats-it-about.md b/src/content/blog/openfaas-whats-it-about.md index 63bafc0..c3c5773 100644 --- a/src/content/blog/openfaas-whats-it-about.md +++ b/src/content/blog/openfaas-whats-it-about.md @@ -3,8 +3,9 @@ title: "OpenFaaS, what's it all about?" description: "Exploring serverless on Kubernetes. To roll your own or not?" pubDate: 2020-05-12 heroImage: ../../assets/blog/of-headline.png -heroAlt: "OpenFaaS Logo" +heroAlt: "The OpenFaaS logo" changes: "OpenFaaS Cloud has since been retired, and k3d's commands have changed (clusters are now created with k3d cluster create). OpenFaaS itself is still actively developed; see docs.openfaas.com for current installation steps." +tags: [openfaas, serverless, kubernetes] --- Serverless should let you focus on delivering features into production fast not fiddling with infrastructure. diff --git a/src/content/blog/unit-testing-helm-charts.md b/src/content/blog/unit-testing-helm-charts.md index f9615e7..87bb5e4 100644 --- a/src/content/blog/unit-testing-helm-charts.md +++ b/src/content/blog/unit-testing-helm-charts.md @@ -1,10 +1,12 @@ --- -title: "How to unit-test your helm charts with Golang" -description: "We explore unit testing Helm charts with Golang" +title: "How to Unit Test Helm Charts with Go" +description: "Write Go unit tests that render your Helm chart templates and assert on the Kubernetes manifests, so you can change charts with confidence. Includes a full example." pubDate: 2021-01-23 heroImage: ../../assets/blog/helm.jpg heroAlt: "A ship's wheel silhouetted against a sunset" changes: "The networking.k8s.io/v1beta1 Ingress API used in the example was removed in Kubernetes 1.22 (use networking.k8s.io/v1), the Helm Go APIs have moved on, and the OpenFaaS Cloud repository linked below is now archived. The helm-unittest plugin is a popular alternative to the approach shown here." +tags: [helm, testing, go, kubernetes] +service: infrastructure-and-delivery --- Learn how to write Golang unit tests for your Helm charts to keep quality high and make changes with confidence. diff --git a/src/data/site.ts b/src/data/site.ts index e247f07..b31098c 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -1,11 +1,16 @@ export const site = { name: "Heyal Solutions", + url: "https://www.heyal.co.uk", + /** Used as the full on the home page. */ + defaultTitle: "Kubernetes & Platform Engineering Consultancy | Heyal Solutions", legalName: "Heyal Solutions Ltd", description: "Cloud native consultancy. We design, build and fix the Kubernetes platforms, infrastructure and delivery pipelines engineering teams ship on.", email: "sales@heyal.co.uk", companyNumber: "12189247", registeredAddress: "Birmingham, UK", + locality: "Birmingham", + country: "GB", founder: "Alistair Hey", blog: "/blog/", github: "https://github.com/Waterdrips", diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 03d96b7..1332b40 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -3,7 +3,11 @@ import "../styles/global.css"; import Header from "../components/Header.astro"; import Footer from "../components/Footer.astro"; import CallToAction from "../components/CallToAction.astro"; +import { Font, getImage } from "astro:assets"; +import type { ImageMetadata } from "astro"; import { site } from "../data/site"; +import { graph, organization, website } from "../lib/seo"; +import defaultShareImage from "../assets/og-default.jpg"; interface Props { title?: string; @@ -11,11 +15,34 @@ interface Props { /** Show the closing "get in touch" band above the footer. */ cta?: boolean; ogType?: "website" | "article"; + /** Social share image, cropped to 1200x630. Falls back to the site-wide image. */ + image?: ImageMetadata; + imageAlt?: string; + /** Article timestamps, rendered as article:* Open Graph tags. */ + published?: Date; + modified?: Date; + /** Keep this page out of search results. */ + noindex?: boolean; + /** schema.org nodes for this page. The organization and website are always included. */ + schema?: object[]; } -const { title: pageTitle, description = site.description, cta = true, ogType = "website" } = Astro.props; -const title = pageTitle ? `${pageTitle} | ${site.name}` : `${site.name} | Cloud native platform consultancy`; +const { + title: pageTitle, + description = site.description, + cta = true, + ogType = "website", + image = defaultShareImage, + imageAlt = site.name, + published, + modified, + noindex = false, + schema = [], +} = Astro.props; +const title = pageTitle ? `${pageTitle} | ${site.name}` : site.defaultTitle; const canonical = new URL(Astro.url.pathname, Astro.site); +const shareImage = await getImage({ src: image, width: 1200, height: 630, fit: "cover", format: "jpg" }); +const shareImageUrl = new URL(shareImage.src, Astro.site); --- <!doctype html> @@ -37,19 +64,33 @@ const canonical = new URL(Astro.url.pathname, Astro.site); </script> <title>{title} + {noindex && } + + + + + - + - - - + + + + + {published && } + {modified && } + {ogType === "article" && } + + + + + +