diff --git a/astro.config.mjs b/astro.config.mjs index ef7af29..eafb922 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -19,10 +19,25 @@ export default defineConfig({ title: 'Stackyard', description: 'A calm, customizable dashboard for your services, designed to be useful without becoming another wall of data.', - logo: { src: './public/img/stackyard-mark.svg', alt: 'Stackyard' }, + logo: { + light: './public/img/stackyard-wordmark-light.svg', + dark: './public/img/stackyard-wordmark-dark.svg', + alt: 'Stackyard', + replacesTitle: true, + }, favicon: '/favicon.svg', - customCss: ['./src/styles/docs.css'], + customCss: ['./src/styles/fonts.css', './src/styles/docs.css'], head: [ + { + tag: 'link', + attrs: { + rel: 'preload', + as: 'font', + type: 'font/woff2', + href: '/fonts/inter-latin.woff2', + crossorigin: true, + }, + }, { tag: 'link', attrs: { rel: 'manifest', href: '/manifest.webmanifest' } }, { tag: 'meta', attrs: { name: 'theme-color', content: '#0d1117' } }, { tag: 'meta', attrs: { property: 'og:image', content: `${SITE}/img/og.jpg` } }, diff --git a/public/fonts/inter-LICENSE.txt b/public/fonts/inter-LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/public/fonts/inter-LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/inter-latin.woff2 b/public/fonts/inter-latin.woff2 new file mode 100644 index 0000000..e57a265 Binary files /dev/null and b/public/fonts/inter-latin.woff2 differ diff --git a/public/img/stackyard-wordmark-dark.svg b/public/img/stackyard-wordmark-dark.svg new file mode 100644 index 0000000..33a4b53 --- /dev/null +++ b/public/img/stackyard-wordmark-dark.svg @@ -0,0 +1 @@ + diff --git a/public/img/stackyard-wordmark-light.svg b/public/img/stackyard-wordmark-light.svg new file mode 100644 index 0000000..b146d70 --- /dev/null +++ b/public/img/stackyard-wordmark-light.svg @@ -0,0 +1 @@ + diff --git a/scripts/check-dist.mjs b/scripts/check-dist.mjs index 62af94c..62cf778 100644 --- a/scripts/check-dist.mjs +++ b/scripts/check-dist.mjs @@ -6,7 +6,7 @@ import { relative, join } from 'node:path'; const DIST = 'dist'; const SRC_ROOTS = ['src', 'scripts', 'public/api', 'public/icons', 'public/img']; const ROOT_FILES = ['README.md', 'astro.config.mjs', 'package.json', '.gitignore']; -const ASSET_EXT = /\.(svg|png|jpe?g|webp|ico|css|js|json|txt|xml|webmanifest)$/i; +const ASSET_EXT = /\.(svg|png|jpe?g|webp|ico|css|js|json|txt|xml|webmanifest|woff2)$/i; const BANNED = /claude|anthropic|generated by|ai-generated|chatgpt|openai|copilot/i; const read = (f) => readFileSync(f, 'utf8'); diff --git a/src/content/docs/docs/advanced-configuration.md b/src/content/docs/docs/advanced-configuration.md index bdd26bc..3719ea6 100644 --- a/src/content/docs/docs/advanced-configuration.md +++ b/src/content/docs/docs/advanced-configuration.md @@ -15,7 +15,7 @@ Every variable is optional. The defaults are what the container ships with. | `SOCKET_PROXY_URL` | unset | A Docker socket proxy, for container health monitoring. | | `TRUST_PROXY` | unset | Believe `X-Forwarded-Proto`, so a request through a TLS-terminating proxy gets a `Secure` cookie. | | `TRUSTED_PROXY` | unset | Where a front proxy sits, so nginx can resolve the real client for rate limiting. | -| `SESSION_MAX_AGE_DAYS` | `12h` | Idle session lifetime before re-login. Accepts a fraction. A session in use is extended. | +| `SESSION_MAX_AGE_DAYS` | `0.5` | Idle session lifetime in days before re-login, so 12 hours by default. Accepts a fraction. A session in use is extended. | | `PASSWORD_HASH_MEMORY` | `16mib` | Memory per password hash. One of `8mib`, `16mib`, `32mib`, `64mib`, `128mib`. | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error`. `warn` and `error` behave the same. The General settings page also sets this, and that wins once the config has loaded. | | `DEMO_MODE` | unset | Run as a read-only public showcase. | diff --git a/src/content/docs/docs/first-setup.md b/src/content/docs/docs/first-setup.md index fba8f5d..41539c7 100644 --- a/src/content/docs/docs/first-setup.md +++ b/src/content/docs/docs/first-setup.md @@ -19,8 +19,8 @@ The password is a gate between local users, not an authentication layer. See [Se | Section | What it covers | | --- | --- | -| **General** | Server identity and behaviour: title, host IP, logging, password, Docker health checks, and config [import and export](/docs/import-export/backup-and-restore/). | -| **Appearance** | How the dashboard looks: wallpaper, labels, theme, language. See [Customization](/docs/customization/). | +| **General** | Server identity and behaviour: title, host IP, language, logging, password, Docker health checks, and config [import and export](/docs/import-export/backup-and-restore/). | +| **Appearance** | How the dashboard looks: wallpaper, labels, and theme. See [Customization](/docs/customization/). | | **Dashboard** | What is on the dashboard: apps, widgets, folders, and their order. See [Adding services](/docs/adding-services/). | | **About** | Version, update notice, and links to the project. | diff --git a/src/content/docs/docs/settings-reference.md b/src/content/docs/docs/settings-reference.md index 0ffe20e..173a436 100644 --- a/src/content/docs/docs/settings-reference.md +++ b/src/content/docs/docs/settings-reference.md @@ -19,6 +19,12 @@ At the top, before any group: | Description | Free text shown alongside the title. | | Host IP | Your server's own address. Setting it lets badges and widgets reach services on that IP, which the SSRF guard would otherwise block. See [Security](/docs/security/). | +### Language + +| Setting | What it does | +| --- | --- | +| Language | The interface language. English, Persian, Simplified Chinese, Spanish, German or French. Persian flips the whole layout to right to left. See [Customization](/docs/customization/#language). | + ### Monitoring | Setting | What it does | diff --git a/src/lib/repo.ts b/src/lib/repo.ts index 01aee64..231943a 100644 --- a/src/lib/repo.ts +++ b/src/lib/repo.ts @@ -34,3 +34,35 @@ export function readAppVersion(): string | null { return null; } } + +export type Heading = { depth: number; slug: string; text: string }; + +export function slugify(text: string): string { + return text + .toLowerCase() + .replace(/<[^>]+>/g, '') + .replace(/[^\p{L}\p{N}]+/gu, '-') + .replace(/^-+|-+$/g, ''); +} + +/* marked emits headings with no id, so nothing on a generated page is + linkable and Starlight's table of contents renders empty. */ +export function withHeadingIds(html: string, depths = [2, 3]): { html: string; headings: Heading[] } { + const headings: Heading[] = []; + const seen = new Map(); + const out = html.replace( + /]*)>([\s\S]*?)<\/h\1>/g, + (match, level: string, attrs: string, inner: string) => { + const depth = Number(level); + if (!depths.includes(depth) || /\bid=/.test(attrs)) return match; + const text = inner.replace(/<[^>]+>/g, '').trim(); + const base = slugify(text) || `section-${headings.length + 1}`; + const n = seen.get(base) ?? 0; + seen.set(base, n + 1); + const slug = n ? `${base}-${n}` : base; + headings.push({ depth, slug, text }); + return `${inner}`; + }, + ); + return { html: out, headings }; +} diff --git a/src/pages/docs/accessibility.astro b/src/pages/docs/accessibility.astro index fbb2751..eb50919 100644 --- a/src/pages/docs/accessibility.astro +++ b/src/pages/docs/accessibility.astro @@ -1,16 +1,20 @@ --- import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import { marked } from 'marked'; -import { readRepoFile, stripTitle, absolutiseLinks } from '../../lib/repo'; +import { readRepoFile, stripTitle, absolutiseLinks, withHeadingIds } from '../../lib/repo'; const REPO = 'https://github.com/SandObserver/stackyard'; const raw = readRepoFile('ACCESSIBILITY.md'); -const html = raw - ? await marked.parse(absolutiseLinks(stripTitle(raw))) - : `

The accessibility statement is read from the Stackyard repository at build time, and that - checkout was not available for this build. Read it on - GitHub.

`; +const parsed = raw + ? withHeadingIds(String(await marked.parse(absolutiseLinks(stripTitle(raw))))) + : { + html: `

The accessibility statement is read from the Stackyard repository at build time, and + that checkout was not available for this build. Read it on + GitHub.

`, + headings: [], + }; +const { html, headings } = parsed; ---

This page is generated from ACCESSIBILITY.md in the Stackyard repository, so it diff --git a/src/pages/docs/changelog.astro b/src/pages/docs/changelog.astro index 46d7bc3..6bda8f2 100644 --- a/src/pages/docs/changelog.astro +++ b/src/pages/docs/changelog.astro @@ -1,7 +1,8 @@ --- import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import { marked } from 'marked'; -import { readRepoFile, stripTitle, absolutiseLinks } from '../../lib/repo'; +import { readRepoFile, stripTitle, absolutiseLinks, slugify } from '../../lib/repo'; +import type { Heading } from '../../lib/repo'; const REPO = 'https://github.com/SandObserver/stackyard'; const raw = readRepoFile('CHANGELOG.md'); @@ -12,6 +13,7 @@ type Section = { meta: string; body: string; unreleased: boolean; + slug: string; }; let intro = ''; @@ -35,6 +37,7 @@ if (raw) { meta: dash ? dash[2] : '', body, unreleased: /unreleased/i.test(heading), + slug: `release-${slugify((dash ? dash[1] : heading).replace(/^\[|\]$/g, ''))}`, }); } } else { @@ -45,14 +48,23 @@ const unreleased = sections.find((s) => s.unreleased) ?? null; const released = sections.filter((s) => !s.unreleased); const latest = released[0] ?? null; const earlier = released.slice(1); + +const headings: Heading[] = failed + ? [] + : [ + ...(latest ? [{ depth: 2, slug: latest.slug, text: `${latest.label} (latest)` }] : []), + ...(unreleased ? [{ depth: 2, slug: 'unreleased', text: 'Unreleased' }] : []), + ...(earlier.length ? [{ depth: 2, slug: 'earlier-releases', text: 'Earlier releases' }] : []), + ...earlier.map((s) => ({ depth: 3, slug: s.slug, text: s.label })), + ]; --- {failed ? (

@@ -68,7 +80,7 @@ const earlier = released.slice(1); {latest && (

-

+

{latest.label} Latest release {latest.meta && {latest.meta}} @@ -78,7 +90,7 @@ const earlier = released.slice(1); )} {unreleased && ( -
+
Unreleased Not in a published version yet @@ -89,9 +101,9 @@ const earlier = released.slice(1); {earlier.length > 0 && ( <> -

Earlier releases

+

Earlier releases

{earlier.map((s) => ( -
+
{s.label} {s.meta && {s.meta}} @@ -106,3 +118,19 @@ const earlier = released.slice(1); )} + + diff --git a/src/pages/docs/development.astro b/src/pages/docs/development.astro index 0f7ad8d..e2b8f7a 100644 --- a/src/pages/docs/development.astro +++ b/src/pages/docs/development.astro @@ -1,16 +1,20 @@ --- import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import { marked } from 'marked'; -import { readRepoFile, stripTitle, absolutiseLinks } from '../../lib/repo'; +import { readRepoFile, stripTitle, absolutiseLinks, withHeadingIds } from '../../lib/repo'; const REPO = 'https://github.com/SandObserver/stackyard'; const raw = readRepoFile('CONTRIBUTING.md'); -const html = raw - ? await marked.parse(absolutiseLinks(stripTitle(raw))) - : `

The contributing guide is read from the Stackyard repository at build time, and that - checkout was not available for this build. Read it on - GitHub.

`; +const parsed = raw + ? withHeadingIds(String(await marked.parse(absolutiseLinks(stripTitle(raw))))) + : { + html: `

The contributing guide is read from the Stackyard repository at build time, and that + checkout was not available for this build. Read it on + GitHub.

`, + headings: [], + }; +const { html, headings } = parsed; const reference = [ ['architecture.md', 'Architecture', 'How a request is served, and where config lives.'], @@ -25,6 +29,7 @@ const reference = [ title: 'Development', description: 'How to work on Stackyard, taken from the repository at build time.', }} + headings={[...headings, { depth: 2, slug: 'reference', text: 'Reference' }]} >

This page is generated from CONTRIBUTING.md in the Stackyard repository, so it diff --git a/src/pages/index.astro b/src/pages/index.astro index dd32e58..7b00134 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,6 @@ --- import { readAppVersion } from '../lib/repo'; +import '../styles/fonts.css'; const version = readAppVersion(); @@ -54,11 +55,12 @@ const schema = {