diff --git a/Makefile b/Makefile index 0fb1def..9179b93 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,20 @@ -.PHONY: launch +.PHONY: launch cover + launch: @echo "Launching the application..." - npm run dev \ No newline at end of file + npm run dev + +# Rebuild static/cover.png (the og:image) from tools/og-card.html. +# +# The card is copied into public/ and served over HTTP rather than opened as a +# file:// URL, so the Jost webfont and the logo are same-origin — woff2 is +# CORS-gated, and a cross-origin miss fails silently to the fallback font. +cover: + npm run build + cp tools/og-card.html public/og-card.html + @python3 -m http.server 8899 --directory public >/dev/null 2>&1 & \ + srv=$$!; sleep 1; \ + node tools/og-shot.mjs http://127.0.0.1:8899/og-card.html static/cover.png 1200 630; \ + kill $$srv + rm -f public/og-card.html + @echo "static/cover.png regenerated" diff --git a/assets/js/custom.js b/assets/js/custom.js index c5525d0..167ea72 100644 --- a/assets/js/custom.js +++ b/assets/js/custom.js @@ -1 +1,55 @@ -// Put your custom JS code here +// Put your custom JS code here + +// Refresh the homepage star counts from /.netlify/functions/stars. +// +// The counts are already rendered at build time, so this only corrects them +// between rebuilds. Every failure path leaves the build-time values in place: +// running `hugo server` without `netlify dev` has no function to call, and a +// visitor may be offline. +// +// Cards are deliberately not re-sorted. This runs after first paint, so +// reordering would visibly shuffle them. The order therefore reflects the last +// deploy, and only looks wrong if two projects swap rank between deploys. +(function () { + function format(count) { + return count >= 1000 ? (count / 1000).toFixed(1) + "k" : String(count); + } + + function refreshStars() { + var badges = document.querySelectorAll(".feature-stars[data-repo]"); + if (!badges.length) { + return; + } + + fetch("/.netlify/functions/stars") + .then(function (response) { + if (!response.ok) { + throw new Error("stars endpoint responded " + response.status); + } + return response.json(); + }) + .then(function (stars) { + for (var i = 0; i < badges.length; i++) { + var badge = badges[i]; + var count = stars[badge.dataset.repo.toLowerCase()]; + if (typeof count !== "number") { + continue; + } + + badge.querySelector("[data-stars-count]").textContent = format(count); + badge.title = count + " stars on GitHub"; + // Reveals the badge on cards whose build-time lookup came back empty. + badge.classList.remove("feature-stars-pending"); + } + }) + .catch(function () { + // Keep the build-time counts. + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", refreshStars, { once: true }); + } else { + refreshStars(); + } +})(); diff --git a/assets/scss/common/_custom.scss b/assets/scss/common/_custom.scss index f7c1361..84d7d34 100644 --- a/assets/scss/common/_custom.scss +++ b/assets/scss/common/_custom.scss @@ -1 +1,291 @@ -// Put your custom SCSS code here +// Put your custom SCSS code here + +// Shrink the top-right navbar icons (social links + color mode toggler) +#socialMenu .social-link svg, +#buttonColorMode svg { + width: 20px; + height: 20px; +} + +// Skip link: the first focusable thing on the page, hidden until it has focus. +// +// Not Bootstrap's `.visually-hidden-focusable`, so this does not depend on that +// utility surviving purgecss. Clipped rather than `display: none`, because a +// display-none element cannot receive focus at all. +.skip-link { + position: absolute; + top: 0; + left: 0; + z-index: 1080; // above the sticky navbar ($zindex-sticky is 1020) + padding: 0.625rem 1rem; + background: $primary; + color: #fff; + border-bottom-right-radius: 0.375rem; + text-decoration: none; + + &:not(:focus) { + // The standard visually-hidden recipe: out of sight, still in the a11y tree. + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } + + &:focus { + color: #fff; + } +} + +// Keep the footer at the bottom of the viewport on short pages +// (the homepage does not fill the screen otherwise). +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +// Flex items default to `min-width: auto`, so wide content (code blocks, +// tables) could stretch the page instead of scrolling inside its own box +// now that `body` is a flex container. +body > * { + min-width: 0; +} + +.footer { + margin-top: auto; + + // Doks sizes footer items at $font-size-base (16px) from `md` up, which is + // heavy for a copyright line under an 18px body. No media query needed to + // override it: same specificity, and this file is imported last. + li { + font-size: 0.8125rem; + } +} + +// Scale the homepage headline with the viewport rather than wrapping it. +.home h1.home-title { + // `clamp()` passes through to CSS; Sass would try to evaluate `min()` + // itself and fail on the mixed rem/vw units. + font-size: clamp(1.5rem, 3.4vw, 3rem); + // Doks pulls the home headline up by 1rem, which eats into the hero padding. + margin-top: 0; + margin-bottom: 1.75rem; +} + +// One line, but only where one line fits. Below `sm` the headline would have to +// drop to ~15px to fit 320px on a single line, so it wraps to two legible lines +// instead — `nowrap` there ran off the right edge and scrolled the page +// sideways. +@include media-breakpoint-up(sm) { + .home h1.home-title { + white-space: nowrap; + } +} + +// The one piece of prose on the page. Constrained because a full-width measure +// under a centred headline reads as a caption rather than a statement. +.home .lead { + max-width: 40rem; + margin-inline: auto; + opacity: 0.75; +} + +// The headline carries the top of the page alone, so give it room to sit in. +// The slack comes out of the dead band above the footer, which the short +// homepage leaves over anyway. +.home { + .section.container-fluid { + // Scaled, not fixed: the headline itself scales with the viewport, and a + // flat 5.5rem left 88px of air above a 20px headline on a phone. + padding-top: clamp(2.5rem, 7vw, 5.5rem); + padding-bottom: clamp(2rem, 5.5vw, 4.5rem); + } + + .section-features { + padding-top: 0; + } +} + +// Brand mark in the navbar (top left), next to the site title +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + + &::before { + content: ""; + display: inline-block; + flex: 0 0 auto; + width: 32px; + height: 32px; + background: url("images/logo.png") no-repeat center / contain; + border-radius: 50%; + } +} + +// Project cards on the homepage. +// +// Flex rather than grid or Bootstrap's row/col: seven cards never fill a row +// evenly, and flex-wrap centres the short final row instead of leaving a hole +// on one side. The explicit basis keeps every card the same width, which +// `flex-grow` would not — the last row would stretch wider than the rows above. +$feature-gap: 1.25rem; + +// Names the grid without shouting: smaller than the headline, uppercase and +// tracked out so it reads as a label for the section rather than a second title +// competing with the hero. +// +// Deliberately not called `.section-title` — the theme already uses that for the +// "Related posts" heading on blog and legal pages (doks-core _posts.scss:55), and +// this would have restyled those into tiny uppercase labels. +.feature-grid-title { + margin-top: 0; + margin-bottom: 1.5rem; + text-align: center; + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + // Not lower: at 13px this is normal-size text for WCAG, so it needs 4.5:1. + // 0.5 measured 3.01:1 and failed; 0.7 measures 5.4:1. + opacity: 0.7; +} + +.feature-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: $feature-gap; + // It is a `ul` now, for the item count screen readers announce. Strip the + // list affordances the browser adds; the cards are the visual affordance. + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.feature-card { + flex: 0 0 100%; +} + +@include media-breakpoint-up(sm) { + .feature-card { + // Two per row: subtract the one gap between them. + flex-basis: calc(50% - #{$feature-gap * 0.5}); + } +} + +@include media-breakpoint-up(lg) { + .feature-card { + // Four per row: three gaps spread across four cards. + flex-basis: calc(25% - #{$feature-gap * 0.75}); + } +} + +.feature-card { + padding: 1.5rem; + // A warm tint rather than neutral grey: at these low alphas it barely reads + // as colour, but it stops the cards from looking like grey wireframes. + background: rgba($primary, 0.02); + border: 1px solid rgba($primary, 0.14); + border-radius: 0.75rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + + // `h3` rather than `h2`: the grid now sits under its own section heading, so + // the card titles are one level deeper in the outline. + h3 { + // Doks puts a 2rem top margin on every heading, which leaves a gap + // between the card's top edge and its title. + margin-top: 0; + margin-bottom: 0.5rem; + display: flex; + align-items: baseline; + gap: 0.5rem; + font-size: 1.125rem; + + a { + color: inherit; + text-decoration: none; + } + } + + // Decorative, and `aria-hidden` in the markup. `flex: 0 0 auto` stops a + // wide emoji from being squeezed by a long project name. + .feature-icon { + flex: 0 0 auto; + font-size: 1rem; + line-height: 1; + } + + p { + margin-bottom: 0; + font-size: 1rem; + opacity: 0.78; + } + + &:hover { + border-color: rgba($primary, 0.6); + box-shadow: 0 6px 20px rgba($primary, 0.12); + transform: translateY(-2px); + + h3 a { + color: $primary; + } + } +} + +// The donation call to action. Deliberately unboxed: a panel here competed with +// the project cards for attention and left a large empty band across the page. +// A plain centred line separates it from the grid on whitespace alone. +.feature-cta { + // Separated by whitespace alone — no rule and no box. The gap is a little + // wider than the grid's own row gap so the invitation still reads as the end + // of the page rather than an eighth card that fell out of the row. + margin-top: 3rem; + text-align: center; + + .feature-cta-title { + display: block; + font-weight: 600; + color: $primary; + } + + p { + margin: 0.125rem 0 0; + font-size: 0.9375rem; + opacity: 0.7; + } +} + +// GitHub star count, right-aligned on the card's title line +.feature-stars { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-left: auto; + flex: 0 0 auto; + font-size: 0.8125rem; + font-weight: 500; + + svg { + // A gold star, as GitHub draws it. The muting sits on the number instead of + // the whole badge, so the colour is not washed out by an opacity of its own. + color: #e3a008; + // Nudge the star onto the text baseline + margin-bottom: -0.125rem; + } + + // The count stays in the body colour: yellow text this small would be hard to + // read on white, and the star alone carries the colour. + [data-stars-count] { + opacity: 0.65; + } +} + +// No count yet: the build-time lookup failed and assets/js/custom.js has not +// filled this in (or cannot reach the endpoint). Hide it rather than show a +// bare star with no number. +.feature-stars-pending { + display: none; +} diff --git a/assets/scss/common/_variables-custom.scss b/assets/scss/common/_variables-custom.scss index 7302032..e6c285a 100644 --- a/assets/scss/common/_variables-custom.scss +++ b/assets/scss/common/_variables-custom.scss @@ -1,2 +1,7 @@ -// Put your custom SCSS variables here -$primary: #F2A884; +// Put your custom SCSS variables here + +// The brand orange, sampled from the logo. The previous value (#F2A884) was a +// pale tint of it that only managed 1.96:1 contrast against white — well below +// the 4.5:1 WCAG AA needs for text — so links and accents barely registered and +// the site read as monochrome. This hits 4.56:1. +$primary: #d93c0d; diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index 7987c73..41f0755 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -21,7 +21,10 @@ defaultContentLanguageInSubdir = false enable = true [outputs] - home = ["HTML", "RSS", "searchIndex"] + # SITEMAP on `home` is what produces /sitemap.xml. The Doks starter left it off, + # so the root sitemap was never generated even though robots.txt advertises it — + # only the per-section /blog/sitemap.xml existed. + home = ["HTML", "RSS", "searchIndex", "SITEMAP"] section = ["HTML", "RSS", "SITEMAP"] [outputFormats.searchIndex] @@ -48,6 +51,20 @@ defaultContentLanguageInSubdir = false [caches.getjson] dir = ":cacheDir/:project" maxAge = -1 # "30m" + # GitHub star counts (resources.GetRemote). These are only the seed values for + # first paint and the card ordering — assets/js/custom.js replaces the numbers + # from /.netlify/functions/stars on every visit, so what a reader sees is at + # most an hour old regardless of when the site last built. A TTL here just + # stops repeated deploys from spending the 60 requests/hour unauthenticated + # API budget; Netlify persists this cache between builds. + [caches.getresource] + dir = ":cacheDir/:project" + maxAge = "12h" + +# Allow reading GITHUB_TOKEN so CI can raise the GitHub API rate limit when +# fetching star counts. Repeats Hugo's defaults, which this setting replaces. +[security.funcs] + getenv = ['^HUGO_', '^CI$', '^GITHUB_TOKEN$'] [taxonomies] contributor = "contributors" diff --git a/config/_default/module.toml b/config/_default/module.toml index d82136f..9e5022e 100644 --- a/config/_default/module.toml +++ b/config/_default/module.toml @@ -1,87 +1,88 @@ -# mounts -## archetypes -[[mounts]] - source = "node_modules/@thulite/doks-core/archetypes" - target = "archetypes" - -[[mounts]] - source = "archetypes" - target = "archetypes" - -## assets -[[mounts]] - source = "node_modules/@thulite/core/assets" - target = "assets" - -[[mounts]] - source = "node_modules/@thulite/images/assets" - target = "assets" - -[[mounts]] - source = "node_modules/@thulite/doks-core/assets" - target = "assets" - -[[mounts]] - source = "node_modules/@tabler/icons/icons" - target = "assets/svgs/tabler-icons" - -[[mounts]] - source = "assets" - target = "assets" - -## content -[[mounts]] - source = "content" - target = "content" - -## data -[[mounts]] - source = "node_modules/@thulite/doks-core/data" - target = "data" - -[[mounts]] - source = "data" - target = "data" - -## i18n -[[mounts]] - source = "node_modules/@thulite/doks-core/i18n" - target = "i18n" - -[[mounts]] - source = "i18n" - target = "i18n" - -## layouts -[[mounts]] - source = "node_modules/@thulite/core/layouts" - target = "layouts" - -[[mounts]] - source = "node_modules/@thulite/seo/layouts" - target = "layouts" - -[[mounts]] - source = "node_modules/@thulite/images/layouts" - target = "layouts" - -[[mounts]] - source = "node_modules/@thulite/doks-core/layouts" - target = "layouts" - -[[mounts]] - source = "node_modules/@thulite/inline-svg/layouts" - target = "layouts" - -[[mounts]] - source = "layouts" - target = "layouts" - -## static -[[mounts]] - source = "node_modules/@thulite/doks-core/static" - target = "static" - -[[mounts]] - source = "static" - target = "static" +# mounts +## archetypes +[[mounts]] + source = "node_modules/@thulite/doks-core/archetypes" + target = "archetypes" + +[[mounts]] + source = "archetypes" + target = "archetypes" + +## assets +[[mounts]] + source = "node_modules/@thulite/core/assets" + target = "assets" + +[[mounts]] + source = "node_modules/@thulite/images/assets" + target = "assets" + +[[mounts]] + source = "node_modules/@thulite/doks-core/assets" + target = "assets" + +[[mounts]] + source = "node_modules/@tabler/icons/icons" + target = "assets/svgs/tabler-icons" + +[[mounts]] + source = "assets" + target = "assets" + +## content +[[mounts]] + source = "content" + target = "content" + +## data +[[mounts]] + source = "node_modules/@thulite/doks-core/data" + target = "data" + +[[mounts]] + source = "data" + target = "data" + +## i18n +[[mounts]] + source = "node_modules/@thulite/doks-core/i18n" + target = "i18n" + +[[mounts]] + source = "i18n" + target = "i18n" + +## layouts +[[mounts]] + source = "layouts" + target = "layouts" + +[[mounts]] + source = "node_modules/@thulite/core/layouts" + target = "layouts" + +[[mounts]] + source = "node_modules/@thulite/seo/layouts" + target = "layouts" + +[[mounts]] + source = "node_modules/@thulite/images/layouts" + target = "layouts" + +[[mounts]] + source = "node_modules/@thulite/doks-core/layouts" + target = "layouts" + +[[mounts]] + source = "node_modules/@thulite/inline-svg/layouts" + target = "layouts" + + +## static +[[mounts]] + source = "node_modules/@thulite/doks-core/static" + target = "static" + +[[mounts]] + source = "static" + target = "static" diff --git a/config/_default/params.toml b/config/_default/params.toml index 2f53f2b..61262ae 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -1,13 +1,15 @@ # Hugo -title = "My Docs" -description = "Congrats on setting up a new Doks project!" +title = "InftyAI" +description = "Exploring the ∞ possibilities of AI" images = ["cover.png"] # mainSections mainSections = ["docs"] [social] - twitter = "getdoks" + # Drives , i.e. who X credits when the site is shared. + # Was "getdoks", the theme author's handle, left over from the starter. + twitter = "InftyAI" # Doks (@thulite/doks-core) [doks] @@ -48,7 +50,12 @@ mainSections = ["docs"] toTopButton = false # false (default) or true breadcrumbTrail = false # false (default) or true headlineHash = true # true (default) or false - scrollSpy = true # true (default) or false + # Off, because there is nothing to spy on: scrollspy highlights the current + # heading inside `#toc`, and no page on this site renders a table of contents. + # Leaving it on had one real effect — `tabindex="0"` on ``, which made the + # body itself the first Tab stop and pushed the skip link to second. Turn this + # back on if a docs section with a TOC is ever added. + scrollSpy = false # true (default) or false # Multilingual multilingualMode = false # false (default) or true @@ -60,7 +67,7 @@ mainSections = ["docs"] # UX headerBar = false # true (default) or false - backgroundDots = true # true (default) or false + backgroundDots = false # true (default) or false # Homepage sectionFooter = false # false (default) or true diff --git a/config/production/hugo.toml b/config/production/hugo.toml index 25b6683..553ebd6 100644 --- a/config/production/hugo.toml +++ b/config/production/hugo.toml @@ -1,2 +1,11 @@ -# Overrides for production environment -baseurl = "/" +# Overrides for production environment + +# No `baseurl` override here on purpose. The Doks starter shipped `baseurl = "/"`, +# which silently beat the real URL in config/_default and left every absolute URL +# broken in production: `og:url` and `og:image` came out relative (Open Graph +# requires absolute), and robots.txt advertised a relative `Sitemap:`. The +# canonical value lives in config/_default/hugo.toml and should stay the only +# place it is written down. +# +# config/next keeps `baseurl = "/"` deliberately — branch and preview deploys +# have their own hostnames and must not claim the production URL. diff --git a/content/_index.md b/content/_index.md index 376f504..a5d1428 100644 --- a/content/_index.md +++ b/content/_index.md @@ -1,13 +1,18 @@ ---- -title: "Exploring the ∞ possibilities of AI" -description: "" -# lead: "" -date: 2023-09-07T16:33:54+02:00 -lastmod: 2023-09-07T16:33:54+02:00 -draft: false -seo: - title: "InftyAI ∞" # custom title (optional) - description: "Exploring the ∞ possibilities of AI" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - robots: "" # custom robot tags (optional) ---- +--- +title: "Exploring the ∞ possibilities of AI" +# Shown under the headline. Placeholder copy in your voice — edit freely; it is +# the only prose on the page, so it is what tells a first-time visitor what +# InftyAI actually does. +lead: "We build open source infrastructure for AI." +description: "InftyAI is an open source community building infrastructure for AI: LLM inference on Kubernetes, GPU orchestration, model routing, and agent sandboxes." +date: 2023-09-07T16:33:54+02:00 +lastmod: 2023-09-07T16:33:54+02:00 +draft: false +seo: + title: "InftyAI ∞" # custom title (optional) + # Deliberately not a copy of the title — search snippets and social cards + # previously showed the same eight words twice. + description: "InftyAI is an open source community building infrastructure for AI: LLM inference on Kubernetes, GPU orchestration, model routing, and agent sandboxes." + canonical: "" # custom canonical URL (optional) + robots: "" # custom robot tags (optional) +--- diff --git a/data/projects.yaml b/data/projects.yaml new file mode 100644 index 0000000..6bbd603 --- /dev/null +++ b/data/projects.yaml @@ -0,0 +1,55 @@ +# Homepage project cards. `repo` is used to look up the GitHub star count at +# build time; cards are rendered in descending order of stars. +# +# `icon` is decorative only — it is rendered `aria-hidden`, so the project name +# has to carry the meaning on its own. Keeping it out of `description` leaves the +# prose clean and gives every card the same visual anchor beside its title. +projects: + - name: Awesome-LLMOps + icon: "🎉" + repo: InftyAI/Awesome-LLMOps + url: https://awesome-llmops.inftyai.com + description: "An awesome & curated list of best LLMOps tools." + + - name: llmaz + icon: "☸️" + repo: InftyAI/llmaz + url: https://llmaz.inftyai.com/ + description: "Easy, advanced inference platform for large language models on Kubernetes." + + - name: Alphatrion + icon: "⚒️" + repo: InftyAI/alphatrion + url: https://github.com/InftyAI/alphatrion + description: "The open-source framework for LLM experiments and agent orchestration." + + - name: AMRS + icon: "🧬" + repo: InftyAI/amrs + url: https://github.com/InftyAI/amrs + description: "The adaptive model routing system for exploration and exploitation." + + - name: SandD + icon: "🤖" + repo: InftyAI/sandd + url: https://github.com/InftyAI/sandd + description: "A lightweight sandbox daemon for secure agent execution in isolated environments." + + - name: Nebula + icon: "🎮" + repo: InftyAI/Nebula + url: https://github.com/InftyAI/Nebula + description: "The control plane for GPUaaS." + + - name: MLX + icon: "🦀" + repo: InftyAI/mlx + url: https://github.com/InftyAI/mlx + description: "Rust bindings for Apple's MLX framework." + +# Rendered below the grid as plain text — not a project, and not a link, so it +# has no `repo` (no star count) and no `url`. +cta: + name: "Donation Plan" + icon: "✨" + description: "Got a brilliant idea? Let's build something extraordinary together." diff --git a/functions/stars.mjs b/functions/stars.mjs new file mode 100644 index 0000000..5513a9c --- /dev/null +++ b/functions/stars.mjs @@ -0,0 +1,73 @@ +// Current GitHub star counts for the InftyAI org, keyed by lowercased +// `owner/repo`. +// +// The homepage renders star counts at build time; this endpoint lets the +// browser refresh them without waiting for a rebuild. It exists as a function +// rather than a direct browser fetch for three reasons: it is same-origin, so +// the `connect-src 'self'` CSP in netlify.toml needs no loosening; it can hold +// a token server-side; and the CDN caches its response, so GitHub sees roughly +// one request an hour regardless of traffic. + +const ORG = "InftyAI"; + +// One org-wide request instead of one per project, so adding a project to +// data/projects.yaml needs no change here. 100 is the page-size ceiling; the +// org would need to outgrow that before pagination mattered. +const UPSTREAM = `https://api.github.com/orgs/${ORG}/repos?per_page=100&type=public`; + +export default async () => { + const headers = { + Accept: "application/vnd.github+json", + "User-Agent": "inftyai-website", + }; + + // Optional: raises the rate limit from 60 to 5,000 requests an hour. Set it + // in Netlify's environment variables if the unauthenticated budget ever runs + // short. + const token = process.env.GITHUB_TOKEN; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + let repos; + try { + const upstream = await fetch(UPSTREAM, { headers }); + if (!upstream.ok) { + return failed(`GitHub responded ${upstream.status}`); + } + repos = await upstream.json(); + } catch (err) { + return failed(err.message); + } + + // Lowercased because the casing GitHub reports does not always match the + // slugs in data/projects.yaml (`InftyAI/SandD` vs `InftyAI/sandd`). + const stars = {}; + for (const repo of repos) { + stars[repo.full_name.toLowerCase()] = repo.stargazers_count; + } + + return new Response(JSON.stringify(stars), { + headers: { + "Content-Type": "application/json", + // Set explicitly so the catch-all `Cache-Control` in netlify.toml cannot + // pin this response for a year. + "Cache-Control": "public, max-age=600", + "Netlify-CDN-Cache-Control": + "public, s-maxage=3600, stale-while-revalidate=86400", + }, + }); +}; + +// The page falls back to its build-time counts on any failure, so the body is +// only ever read by a human debugging. `no-store` keeps a transient GitHub +// outage from being cached for an hour. +function failed(reason) { + return new Response(JSON.stringify({ error: reason }), { + status: 503, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }); +} diff --git a/hugo_stats.json b/hugo_stats.json index a7a6bcb..170baff 100644 --- a/hugo_stats.json +++ b/hugo_stats.json @@ -13,17 +13,18 @@ "g", "h1", "h2", + "h3", "h5", "head", "header", "html", - "img", "input", "kbd", "label", "li", "line", "link", + "main", "meta", "noscript", "p", @@ -43,25 +44,16 @@ "classes": [ "DocSearch-Label", "active", - "bg-dots", "blog", "blog-header", - "blur-up", - "border-0", "btn", "btn-close", - "btn-cta", - "btn-lg", "btn-link", - "btn-primary", "card", "card-body", "card-list", "categories", "col-lg-10", - "col-lg-12", - "col-lg-5", - "col-lg-8", "col-lg-9", "col-md-12", "col-xl-8", @@ -78,6 +70,13 @@ "d-md-none", "d-none", "error404", + "feature-card", + "feature-cta", + "feature-cta-title", + "feature-grid", + "feature-grid-title", + "feature-icon", + "feature-stars", "flex-column", "flex-grow-1", "flex-lg-row", @@ -87,33 +86,25 @@ "form-control-lg", "fs-5", "h-auto", - "h4", "h5", "home", + "home-title", "icon", "icon-tabler", "icon-tabler-brand-discord", "icon-tabler-brand-github", - "icon-tabler-brand-slack", "icon-tabler-brand-x", "icon-tabler-menu", "icon-tabler-search", "icon-tabler-x", - "icons-tabler-outline", - "img-fluid", - "img-simple", "justify-content-between", "justify-content-center", - "justify-content-start", - "lazyloaded", "lead", "list", "list-inline", "list-inline-item", "list-view", - "ls-is-cached", "mb-0", - "mb-2", "me-2", "me-auto", "me-lg-3", @@ -131,7 +122,6 @@ "ms-3", "ms-auto", "mt-3", - "mt-n3", "mx-2", "mx-auto", "my-3", @@ -151,12 +141,9 @@ "order-lg-4", "p-0", "p-2", - "pb-3", "privacy", "px-0", - "px-4", "query-no-results", - "rounded-circle", "row", "search-form", "search-input", @@ -169,8 +156,8 @@ "section", "section-features", "section-md", - "section-sm", "single", + "skip-link", "social-link", "status", "sticky-top", @@ -180,8 +167,6 @@ "taxonomy", "text-center", "text-decoration-none", - "text-lg-end", - "text-lg-start", "text-muted", "text-reset", "title", @@ -190,6 +175,7 @@ "wrap" ], "ids": [ + "content", "offcanvasNavMain", "offcanvasNavMainLabel", "query", diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..73c0065 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,40 @@ +{{/* Overrides @thulite/doks-core/layouts/_default/baseof.html. Three changes, all + in the body; everything else is kept byte-for-byte so theme updates stay easy + to diff against: + + 1. A `
` landmark. The theme shipped none, so assistive technology had no + way to skip the navbar and jump to the content. + 2. A skip link as the first focusable element, which needs (1) to point at. + 3. `role="document"` dropped from `.wrap`. It is meant for reading-mode content + inside web *applications*; on an ordinary page it adds nothing and competes + with the real landmarks. + + `
` has to wrap the `sidebar-prefooter` and `sidebar-footer` blocks too, + not just `main`: the homepage renders its project grid into sidebar-prefooter, + and blog/legal pages render "Related posts" into sidebar-footer. Both are page + content, and neither can be wrapped from inside its own block. */}} + + + {{ partial "head/head" . }} + {{ partial "head/body-class" . }} + + + {{ partial "header/header" . }} +
+
+
+ {{ if and (eq site.Params.doks.containerBreakpoint "fluid") (or (not (in .Site.Params.mainSections .Type)) (.IsNode)) }}
{{ end }} + {{ block "main" . }}{{ end }} + {{ if and (eq site.Params.doks.containerBreakpoint "fluid") (or (not (in .Site.Params.mainSections .Type)) (.IsNode)) }}
{{ end }} +
+
+ {{ block "sidebar-prefooter" . }}{{ end }} + {{ block "sidebar-footer" . }}{{ end }} +
+ {{ partial "footer/footer" . }} + {{ partial "footer/script-footer" . }} + {{ if eq site.Params.doks.toTopButton true -}} + {{ partial "footer/to-top" . }} + {{ end }} + + diff --git a/layouts/_default/home.html b/layouts/_default/home.html index 66cbbce..c4678c8 100644 --- a/layouts/_default/home.html +++ b/layouts/_default/home.html @@ -1,26 +1,12 @@ {{ define "main" }} -
-
- Alt_text -
-
- -
-
-
-

{{ .Title }}

-
-
-

{{ .Params.lead | safeHTML }}

- Source - On GitHub - {{ .Content }} -
-
+{{/* No `py-*` utility: Bootstrap marks those `!important`, so the hero spacing + could not then be set from _custom.scss. */}} +
+

{{ .Title }}

+ {{ with .Params.lead -}} +

{{ . | safeHTML }}

+ {{ end -}} + {{ .Content }}
{{ end }} @@ -31,81 +17,86 @@

{{ .Title }}

{{ end -}} {{ if eq $.Site.Language.LanguageName "English" }} -
-
-
- -
-

Awesome-LLMOps

-

🎉 An awesome & curated list of best LLMOps tools. -

-
- -
- - logo - -

☸️ Easy, advanced inference platform for large language models on - Kubernetes.

-
- -
- - logo - -

⚒️ The open-source framework for LLM experiments and agent orchestration.

-
- -
-

AMRS

-

🧬 The adaptive model routing system for exploration and exploitation. -

-
-
-

SandD

-

🤖 A lightweight sandbox daemon for secure agent execution in isolated environments. -

-
- -
-

Nebula

-

🎮 The control plane for GPUaaS. -

-
- -
-

MLX

-

🦀 Rust bindings for Apple's MLX framework. -

-
+{{/* Look up the star count for each project, then rank by it. A failed + lookup (offline, rate limited) leaves that card without a star badge + rather than failing the build. */}} +{{ $headers := dict "Accept" "application/vnd.github+json" }} +{{ with getenv "GITHUB_TOKEN" -}} + {{ $headers = merge $headers (dict "Authorization" (printf "Bearer %s" .)) }} +{{ end -}} +{{ $ranked := slice }} +{{ range site.Data.projects.projects -}} + {{ $stars := -1 }} + {{ $url := printf "https://api.github.com/repos/%s" .repo }} + {{ with try (resources.GetRemote $url (dict "headers" $headers)) -}} + {{ with .Err -}} + {{ warnf "projects: no star count for %s (%s)" $url . }} + {{ else with .Value -}} + {{ $stars = (unmarshal .Content).stargazers_count | default 0 }} + {{ else -}} + {{ warnf "projects: no response for %s" $url }} + {{ end -}} + {{ end -}} + {{ $ranked = $ranked | append (merge . (dict "stars" $stars)) }} +{{ end -}} +{{ $cards := sort $ranked "stars" "desc" }} -
-

✨ Donation - Plan

-

🚀 Got a brilliant idea? Let's build something extraordinary together. -

-
+
+
+ {{/* Names the grid, so the seven project headings hang off a section + instead of directly off the page's h1. */}} +

Projects

+ {{/* A list, because that is what seven sibling projects are: screen + readers announce the count and allow item-by-item navigation. + Flexbox rather than Bootstrap's row/col — it needs no breakpoint + classes, no `h-100`, and no gutter overrides (Doks ships neither the + `.g-*` utilities nor a non-zero `--bs-gutter-y`). */}} +
    + {{ range $cards -}} +
  • +

    + {{ with .icon }}{{ end }} + {{ .name }} + {{/* Rendered for every project, even when the build-time + lookup failed, so assets/js/custom.js can fill it in + from /.netlify/functions/stars. A card with no count + yet is hidden by `.feature-stars-pending` until then. */}} + {{ if .repo -}} + {{ $stars := .stars -}} + {{ $known := and $stars (ge $stars 0) -}} + + {{/* Filled rather than outlined: at 14px a solid star + carries the colour, an outline barely shows it. */}} + {{ if $known }}{{ if ge $stars 1000 }}{{ printf "%.1fk" (div (float $stars) 1000) }}{{ else }}{{ $stars }}{{ end }}{{ end }} + + {{ end -}} +

    +

    {{ .description }}

    +
  • + {{ end -}} +
+ {{/* An invitation, not a project — kept out of the grid so it does not + read as one more repository. */}} + {{ with site.Data.projects.cta -}} +
+ + {{ with .icon }} {{ end }}{{ .name }} + +

{{ .description }}

+ {{ end -}}
{{ end }} {{ end }} {{ define "sidebar-footer" }} -{{ if site.Params.doks.sectionFooter -}} -
-
-
-

Start building with Doks today

- {{ i18n "get-started" }} -
-
-
-{{ end -}} -{{ end }} \ No newline at end of file +{{ end }} diff --git a/layouts/index.html b/layouts/index.html deleted file mode 100644 index 6e1430f..0000000 --- a/layouts/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/layouts/partials/footer/footer.html b/layouts/partials/footer/footer.html new file mode 100644 index 0000000..fc03b26 --- /dev/null +++ b/layouts/partials/footer/footer.html @@ -0,0 +1,16 @@ +{{/* Overrides the Doks footer, which splits into a left-aligned column for the + footer menu and a right-aligned one for `site.Params.footer`. Only the menu + is populated here, so the two columns are collapsed into one centered list. + Both sources are still rendered, so setting `params.footer` later works. */}} +
+
+
    + {{ range .Site.Menus.footer -}} +
  • {{ .Name }}
  • + {{- end }} + {{- with site.Params.footer }} +
  • {{ . | safeHTML }}
  • + {{- end }} +
+
+
diff --git a/layouts/sitemap.xml b/layouts/sitemap.xml new file mode 100644 index 0000000..7ac87c9 --- /dev/null +++ b/layouts/sitemap.xml @@ -0,0 +1,30 @@ +{{/* Overrides @thulite/seo/layouts/sitemap.xml, which ranges over `.Pages`. On the + home page that is the *child* pages only, so the homepage itself — the most + important URL on this site — was left out of /sitemap.xml. Ranging over + `site.Pages` and filtering by kind includes it while still keeping the empty + taxonomy list pages (categories, tags, contributors) out. */}} +{{ printf "" | safeHTML }} + + {{ $pages := where site.Pages "Kind" "in" (slice "home" "section" "page") }} + {{ range where $pages "Sitemap.Disable" "ne" true }} + {{- if .Permalink -}} + + {{ .Permalink }}{{ if not .Lastmod.IsZero }} + {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} + {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + {{ .Sitemap.Priority }}{{ end }}{{ if .IsTranslated }}{{ range .Translations }} + {{ end }} + {{ end }} + + {{- end -}} + {{ end }} + diff --git a/static/cover.png b/static/cover.png new file mode 100644 index 0000000..cf0e7a8 Binary files /dev/null and b/static/cover.png differ diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..6f07aea Binary files /dev/null and b/static/images/logo.png differ diff --git a/tools/og-card.html b/tools/og-card.html new file mode 100644 index 0000000..9fe6891 --- /dev/null +++ b/tools/og-card.html @@ -0,0 +1,121 @@ + + + + + + + + +
+ +

Exploring the ∞
possibilities of AI

+

We build open source infrastructure for AI.

+
inftyai.com
+
+ + diff --git a/tools/og-shot.mjs b/tools/og-shot.mjs new file mode 100644 index 0000000..06e6a91 --- /dev/null +++ b/tools/og-shot.mjs @@ -0,0 +1,47 @@ +// Headless-Chrome screenshot at an exact pixel size, waiting for webfonts. +// Drives Chrome over CDP directly so this needs no Puppeteer/Playwright +// dependency — the browser is already on the machine. +// +// Usage: node tools/og-shot.mjs +// Normally invoked by `make cover`; see tools/og-card.html. +const [url, out, w, h] = process.argv.slice(2); +const { execFile } = await import('node:child_process'); +const fs = await import('node:fs/promises'); + +const CHROME = process.env.CHROME + || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const port = 9333 + Math.floor(Math.random() * 500); +const proc = execFile(CHROME, [ + '--headless=new', `--remote-debugging-port=${port}`, '--no-first-run', + '--user-data-dir=/tmp/cdp-og-' + port, '--hide-scrollbars', 'about:blank', +]); + +let page; +for (let i = 0; i < 60 && !page; i++) { + try { page = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find((t) => t.type === 'page'); } catch {} + if (!page) await new Promise((r) => setTimeout(r, 200)); +} +const ws = new WebSocket(page.webSocketDebuggerUrl); +await new Promise((r) => (ws.onopen = r)); +let id = 0; const pending = new Map(); +ws.onmessage = (m) => { const x = JSON.parse(m.data); if (x.id && pending.has(x.id)) { pending.get(x.id)(x.result); pending.delete(x.id); } }; +const send = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })); }); +const ev = (e) => send('Runtime.evaluate', { returnByValue: true, awaitPromise: true, expression: e }).then((r) => r.result && r.result.value); + +await send('Page.enable'); +await send('Runtime.enable'); +await send('Emulation.setDeviceMetricsOverride', { width: +w, height: +h, deviceScaleFactor: 1, mobile: false }); +await send('Page.navigate', { url }); +await new Promise((r) => setTimeout(r, 1200)); +// Fonts and the logo have to be in before the pixels are read, or the capture +// races the swap and lands on the fallback face. +await ev(`document.fonts.ready.then(()=>'ok')`); +await ev(`Promise.all([...document.images].map(i=>i.complete?1:new Promise(r=>{i.onload=i.onerror=r}))).then(()=>'ok')`); +console.log('fonts loaded :', await ev(`[...document.fonts].filter(f=>f.status==='loaded').map(f=>f.family+' '+f.weight).join(', ')`)); +console.log('h1 lines :', await ev(`(()=>{const e=document.querySelector('h1');const cs=getComputedStyle(e);return Math.round(e.getBoundingClientRect().height/parseFloat(cs.lineHeight))+' line(s), '+Math.round(e.getBoundingClientRect().width)+'px wide'})()`)); +console.log('overflow :', await ev(`document.documentElement.scrollWidth+'x'+document.documentElement.scrollHeight`)); + +const { data } = await send('Page.captureScreenshot', { format: 'png', clip: { x: 0, y: 0, width: +w, height: +h, scale: 1 }, captureBeyondViewport: true }); +await fs.writeFile(out, Buffer.from(data, 'base64')); +console.log('wrote', out); +ws.close(); proc.kill(); process.exit(0);