diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e7eb5f..ac48eecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ keeps reading that version for at least 24 months after a successor lands. ## [Unreleased] +### Docs: brand the documentation site + +The documentation site now matches the pghardstorage.org brand: the +website's navy + cyan palette (light and dark schemes), the wordmark in +the header and a light/dark home-page hero, favicon, typography tuning, +a branded footer with CYBERTEC links, and a right-hand mobile navigation +drawer. The home-page title was de-duplicated and made SEO-friendly, and +Open Graph + Twitter Card meta tags were added for social share previews. +All assets are repo-local (air-gapped posture); no new build dependencies. + ### Docs: publish the documentation site to GitHub Pages The docs CI built and validated the site but never published it. A diff --git a/docs/assets/pghardstorage_favicon.png b/docs/assets/pghardstorage_favicon.png new file mode 100644 index 00000000..6dc30715 Binary files /dev/null and b/docs/assets/pghardstorage_favicon.png differ diff --git a/docs/assets/pghardstorage_icon.png b/docs/assets/pghardstorage_icon.png new file mode 100644 index 00000000..65e130b9 Binary files /dev/null and b/docs/assets/pghardstorage_icon.png differ diff --git a/docs/assets/pghardstorage_logo_dark.png b/docs/assets/pghardstorage_logo_dark.png new file mode 100644 index 00000000..522cb0ef Binary files /dev/null and b/docs/assets/pghardstorage_logo_dark.png differ diff --git a/docs/assets/pghardstorage_logo_light.png b/docs/assets/pghardstorage_logo_light.png new file mode 100644 index 00000000..e665d692 Binary files /dev/null and b/docs/assets/pghardstorage_logo_light.png differ diff --git a/docs/index.md b/docs/index.md index 9d0cc767..c698dcd7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,24 @@ --- -title: pg_hardstorage -description: PostgreSQL backup, done right — agent + CLI documentation +# `title` drives only the /browser-tab text, not the on-page +# H1. It must NOT repeat the site_name ("pg_hardstorage"), or +# Material renders the duplicate "pg_hardstorage - pg_hardstorage". +# Keep it descriptive + keyword-front-loaded for SEO; Material +# appends " - pg_hardstorage" automatically. +title: PostgreSQL backup, WAL streaming & PITR +description: >- + pg_hardstorage is an open-source PostgreSQL backup agent and CLI: + continuous WAL streaming, point-in-time recovery, content-addressed + deduplication, envelope encryption, and signed manifests. PG 15+, + Apache 2.0. --- # pg_hardstorage +<p class="hero-logo" markdown> +![pg_hardstorage](assets/pghardstorage_logo_dark.png){.logo-on-light} +![pg_hardstorage](assets/pghardstorage_logo_light.png){.logo-on-dark} +</p> + > PostgreSQL backup, done right — agent + CLI for > resilient, compliant, content-addressed backup with native > WAL streaming, envelope encryption, and a built-in diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..3c7da253 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,249 @@ +/* pg_hardstorage documentation — brand theming. + * + * Why this file exists: the site must match the colours of + * the main pghardstorage.org website, which uses a bespoke + * navy + cyan palette that does NOT map onto any of MkDocs + * Material's named primary/accent colours. So mkdocs.yml + * sets `primary: custom` / `accent: custom` and the real + * values live here as Material's CSS custom properties. + * + * Air-gapped constraint: every value below is a literal + * colour or a repo-local asset. No webfont @import, no CDN + * reference — doc readers are often on networks that block + * external CDNs (see the `font: false` note in mkdocs.yml). + * + * Palette source of truth (mirrors the website's :root): + * --navy #060e18 --accent #3db8f5 + * --navy-light #0d1f33 --accent-bright #5ecbff + * --navy-mid #13304d --accent-light #8ee0ff + * --blue-dark #1a4a72 --accent-ink #176ba3 + * text-primary #0a1628 text-secondary #3d5068 + */ + +/* ------------------------------------------------------------------ * + * Brand palette as Material design tokens. + * + * Material derives most of the chrome (header, nav, buttons, + * links) from --md-primary-* and --md-accent-*. We pin them + * to the website palette here. Light mode uses navy chrome + * with cyan accents; dark mode (slate) keeps the same accent + * and lets Material's slate background carry the navy feel. + * ------------------------------------------------------------------ */ + +:root { + /* Header / nav chrome. */ + --md-primary-fg-color: #0d1f33; /* navy-light */ + --md-primary-fg-color--light: #13304d; /* navy-mid */ + --md-primary-fg-color--dark: #060e18; /* navy */ + /* Text that sits ON the primary (header title, icons). */ + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: rgba(255, 255, 255, 0.72); + + /* Links, hover, focus rings, active states. */ + --md-accent-fg-color: #3db8f5; /* accent */ + --md-accent-fg-color--transparent: rgba(61, 184, 245, 0.12); /* accent-glow */ +} + +/* Light scheme: cyan links that read on white. */ +[data-md-color-scheme="default"] { + --md-typeset-a-color: #176ba3; /* accent-ink — darker cyan for AA contrast on white */ +} + +/* Dark scheme (slate): brighter cyan + navy backgrounds so the + * doc body matches the website's dark hero, not Material's + * default near-black slate. */ +[data-md-color-scheme="slate"] { + --md-accent-fg-color: #5ecbff; /* accent-bright */ + --md-typeset-a-color: #5ecbff; + + --md-default-bg-color: #060e18; /* navy */ + --md-default-bg-color--light: #0d1f33; /* navy-light */ + --md-code-bg-color: #0d1f33; /* navy-light — code blocks lift off the page */ +} + +/* ------------------------------------------------------------------ * + * Typography. + * + * Air-gapped: no webfont download. We tune the system / + * Material default stack (Roboto when present, system-ui + * fallback) for a slightly tighter, more editorial read: + * larger line-height in body copy, heavier headings, and a + * monospace stack for code that degrades gracefully offline. + * ------------------------------------------------------------------ */ + +.md-typeset { + /* Comfortable reading measure + line height for long-form + * explanation pages. */ + line-height: 1.7; +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3 { + font-weight: 700; + letter-spacing: -0.01em; /* tightens large headings */ +} + +.md-typeset h1 { + color: var(--md-default-fg-color); +} + +/* Code: keep a robust offline monospace stack. */ +.md-typeset code, +.md-typeset pre { + font-family: "Roboto Mono", ui-monospace, "SFMono-Regular", + "Menlo", "Consolas", monospace; +} + +/* ------------------------------------------------------------------ * + * Header tweaks. + * + * The header brand is the full wordmark (assets/pghardstorage_logo_light.png, + * set in mkdocs.yml). We hide the redundant site-name text so + * the wordmark stands alone, and size the logo by HEIGHT so the + * wide 3.7:1 image scales proportionally into the narrow header. + * + * Two fixes over the naive `height: 1.5rem`: + * 1. Desktop was too small — a wordmark needs more height than + * a square icon to stay legible. Bumped to 2rem. + * 2. Mobile showed nothing — Material hides .md-logo on small + * screens (it normally falls back to the __title text, which + * we've suppressed). We force the logo visible again and + * cap its width so it can't crowd the hamburger / search. */ + +/* Desktop: size the wordmark by height; width follows. */ +.md-header__button.md-logo img { + height: 2rem; + width: auto; +} + +/* Hide the site-name text + the “return to home” topic next to + * the logo. site_name still drives the <title>, search index + * and meta tags — only the visible header text is suppressed. */ +.md-header__topic { + display: none; +} + +/* Mobile (Material's breakpoint is 76.1875em / ~1220px for the + * header layout shift). Re-show the logo Material would hide, + * shrink it slightly, and bound its width so the wide wordmark + * shares the row cleanly with the menu + search icons. */ +@media screen and (max-width: 76.1875em) { + .md-header__button.md-logo { + display: inline-flex; /* override Material's display:none */ + padding-right: 0.4rem; + } + .md-header__button.md-logo img { + height: 1.5rem; + max-width: 9rem; /* never push the menu/search off-row */ + object-fit: contain; + } +} + +/* ------------------------------------------------------------------ * + * Right-hand drawer (hamburger menu). + * + * DELIBERATE override of Material's stock behaviour: by default + * the hamburger sits immediately after the logo (left) and the + * nav drawer slides in from the LEFT. Glued to the wide + * wordmark it looked cramped, so we: + * 1. push the hamburger button to the far right of the header, + * 2. flip the primary drawer to sit on the right and slide in + * from the right, so icon-position and open-direction agree. + * + * Two non-obvious facts (verified against the built main.*.css): + * - Material's mobile breakpoint is 76.234375em, NOT 76.1875em + * (that one is for the navigation tabs). Match it exactly so + * the override engages over the same range as the drawer. + * - Material scopes the drawer's resting position with a + * [dir=ltr] prefix: `[dir=ltr] .md-sidebar--primary{left:-12.1rem}`. + * An unprefixed `.md-sidebar--primary{right:...}` has LOWER + * specificity and loses, so the drawer stays parked on the + * left (overlay shows, panel invisible). We therefore mirror + * Material's own [dir=rtl] right-hand rules under [dir=ltr]. + * - 12.1rem is today's drawer width; re-check if a future + * mkdocs-material bumps it. */ +@media screen and (max-width: 76.234375em) { + /* 1. Hamburger to the far right. The header is a flex row; + * ordering the button last floats it past the title gap to + * the right edge, mirroring the search/scheme icons' side. */ + .md-header__button.md-icon[for="__drawer"] { + order: 100; + margin-left: auto; /* eat the slack so it pins to the edge */ + } + + /* 2a. Park the hidden drawer off-screen on the RIGHT. [dir=ltr] + * prefix matches Material's own specificity so this actually + * wins over its `left:-12.1rem`. */ + [dir=ltr] .md-sidebar--primary { + left: auto; + right: -12.1rem; + } + + /* 2b. Slide it in from the right when the drawer toggle is + * checked (Material default for ltr is translateX(12.1rem); + * we use the negative magnitude, same as its rtl rule). */ + [dir=ltr] [data-md-toggle="drawer"]:checked ~ .md-container .md-sidebar--primary { + transform: translateX(-12.1rem); + } +} + +/* ------------------------------------------------------------------ * + * Home-page hero logo (light/dark swap). + * + * Material's header takes a single logo, so the wide + * light/dark wordmarks can't live there. Instead the home + * page (docs/index.md) carries a <p class="hero-logo"> block + * with BOTH images; CSS shows the right one per colour scheme. + * + * Usage in index.md (md_in_html is enabled): + * <p class="hero-logo" markdown> + * ![pg_hardstorage](assets/pghardstorage_logo_dark.png){.logo-on-light} + * ![pg_hardstorage](assets/pghardstorage_logo_light.png){.logo-on-dark} + * </p> + * ------------------------------------------------------------------ */ +.hero-logo img { + max-width: min(560px, 100%); + height: auto; + margin: 0.5rem 0 1.5rem; +} + +/* Show the dark wordmark on the light page, hide the light one. */ +[data-md-color-scheme="default"] .hero-logo .logo-on-dark { display: none; } +[data-md-color-scheme="default"] .hero-logo .logo-on-light { display: inline; } + +/* And the inverse on the dark page. */ +[data-md-color-scheme="slate"] .hero-logo .logo-on-light { display: none; } +[data-md-color-scheme="slate"] .hero-logo .logo-on-dark { display: inline; } + +/* ------------------------------------------------------------------ * + * Footer. + * + * Material's footer already inherits the primary (navy) chrome. + * Nudge the cyan accent into footer links so they match the + * body link colour rather than the muted footer default. */ +.md-footer-meta { + background-color: #060e18; /* navy — slightly darker than the header */ +} + +.md-footer-meta a:hover, +.md-footer-meta a:focus { + color: #5ecbff; /* accent-bright */ +} + +/* ------------------------------------------------------------------ * + * Buttons rendered via the `.md-button` class on landing pages. + * Give the primary call-to-action the website's cyan→teal + * gradient instead of a flat fill. */ +.md-typeset .md-button--primary { + background: linear-gradient(135deg, #3db8f5 0%, #22d3bb 100%); /* gradient-primary */ + border-color: transparent; + color: #060e18; +} + +.md-typeset .md-button--primary:hover, +.md-typeset .md-button--primary:focus { + background: linear-gradient(135deg, #5ecbff 0%, #3de8d0 100%); /* gradient-primary-h */ + border-color: transparent; + color: #060e18; +} diff --git a/mkdocs.yml b/mkdocs.yml index 1599f160..3a93b7cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,11 @@ site_dir: site # aware, native admonitions + tabs + code-copy buttons. theme: name: material + # Template overrides live in overrides/ — currently just + # main.html, which appends Open Graph + Twitter Card meta + # tags (social share previews) without the social plugin's + # Cairo/Pango build dependency. See overrides/main.html. + custom_dir: overrides features: # Single-page-app navigation: pre-fetches linked pages, # cuts perceived latency. @@ -52,18 +57,31 @@ theme: # Tab groups inside content (e.g. for one tutorial that # shows the same flow in CLI vs REST vs gRPC). - content.tabs.link + # Header brand: the full light wordmark (the header chrome is + # navy in BOTH colour schemes, so the light-on-dark wordmark + # reads correctly either way). The site-name text next to it + # is hidden via CSS (.md-header__topic) so the logo stands + # alone; site_name is still set for the <title>, search index + # and meta tags. favicon is the small square mark for the + # browser tab. All assets are repo-local (air-gapped posture). + logo: assets/pghardstorage_logo_light.png + favicon: assets/pghardstorage_favicon.png palette: + # primary/accent are `custom`: the real colour values are + # the website's bespoke navy + cyan palette, pinned as + # Material design tokens in stylesheets/extra.css (no + # named Material colour matches the brand). - media: "(prefers-color-scheme: light)" scheme: default - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/brightness-4 name: Switch to light mode @@ -123,6 +141,12 @@ markdown_extensions: permalink: true permalink_title: "Permalink to this section" +# Brand stylesheet: pins the website's navy + cyan palette +# onto Material's design tokens, tunes typography, and drives +# the home-page hero logo light/dark swap. Repo-local only. +extra_css: + - stylesheets/extra.css + plugins: - search: separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' @@ -306,11 +330,27 @@ extra: - icon: fontawesome/brands/github link: https://github.com/cybertec-postgresql/pg_hardstorage name: pg_hardstorage on GitHub + - icon: fontawesome/solid/globe + link: https://www.cybertec-postgresql.com/ + name: CYBERTEC PostgreSQL + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/cybertec-postgresql/ + name: CYBERTEC on LinkedIn +# Footer copyright + legal links. Imprint / privacy policy / +# accessibility live on the main pghardstorage.org site (the +# single source of truth) — we link out rather than duplicate +# legal text here. "Cookie settings" uses the bare +# #CCM.openWidget anchor on the CURRENT page: it opens the CCM19 +# consent widget once that script is present on this site (the +# CCM19 embed is added separately, outside this repo change). copyright: > Copyright © CYBERTEC PostgreSQL International GmbH · Licensed under Apache 2.0 · - <a href="#__consent">Change cookie settings</a> + <a href="https://www.pghardstorage.org/imprint">Imprint</a> · + <a href="https://www.pghardstorage.org/privacy-policy">Privacy policy</a> · + <a href="https://www.pghardstorage.org/accessibility">Accessibility</a> · + <a href="#CCM.openWidget">Cookie settings</a> # Strict mode is enabled at the CLI level (`mkdocs build # --strict`) by the Makefile — broken cross-link or missing diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 00000000..110c7dcb --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,61 @@ +{#- + Theme override: social / Open Graph + Twitter Card meta tags. + + Why a manual override instead of Material's `social` plugin: + that plugin renders card images at build time via Cairo/Pango, + which aren't installed in CI and would add system-level build + dependencies. These static tags need none — they reuse the + repo-local wordmark as the share image, so the air-gapped / + no-new-deps posture holds. + + Material exposes the `extrahead` block for exactly this; we + append to <head> without touching the rest of the template. + + Values are derived from page + site config so every page gets + a correct, specific card: + - og:title → "<page title> · pg_hardstorage" on sub-pages, + the bare page title on the home page (which + already front-loads keywords); avoids the + terse Material nav title ("Home", "FAQ") that + reads poorly in a share card + - og:description → the page meta description (falls back to + site_description) + - og:url / canonical → page.canonical_url (needs site_url, + which is set to https://docs.pghardstorage.org) + - og:image → the light wordmark, absolute URL +-#} +{% extends "base.html" %} + +{% block extrahead %} + {#- Page-specific title/description with sensible fallbacks. + Home page: use its (already descriptive) title as-is. + Sub-pages: append the site name so the card is self-identifying. -#} + {% if page and page.is_homepage %} + {#- On the home page Material sets page.title to the nav label + ("Home"); the descriptive SEO title lives in front matter, + readable as page.meta.title. -#} + {% set og_title = page.meta.title | default(page.title, true) | default(config.site_name, true) %} + {% elif page and page.title %} + {% set og_title = page.title ~ " · " ~ config.site_name %} + {% else %} + {% set og_title = config.site_name %} + {% endif %} + {% set og_desc = page.meta.description if page and page.meta and page.meta.description else config.site_description %} + {% set og_image = config.site_url ~ "assets/pghardstorage_logo_light.png" %} + + {#- Open Graph (Slack, LinkedIn, Facebook, Teams). -#} + <meta property="og:type" content="website"> + <meta property="og:site_name" content="{{ config.site_name }}"> + <meta property="og:title" content="{{ og_title }}"> + <meta property="og:description" content="{{ og_desc }}"> + {% if page and page.canonical_url %} + <meta property="og:url" content="{{ page.canonical_url }}"> + {% endif %} + <meta property="og:image" content="{{ og_image }}"> + + {#- Twitter / X large-image card. -#} + <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:title" content="{{ og_title }}"> + <meta name="twitter:description" content="{{ og_desc }}"> + <meta name="twitter:image" content="{{ og_image }}"> +{% endblock %}