zed/docs/theme/index.hbs
morgankrey 10504e3ce1
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Improve docs AI readiness (#59577)
Context

This PR makes the Zed docs easier for AI tools, search crawlers, and
users to consume without changing the visible docs content. The current
production docs are primarily optimized for browser navigation. They do
not expose first-class Markdown URLs, an `llms.txt` index, page-level
copy affordances, or machine-readable freshness metadata that let users
and agents grab clean, current page content.

Changes

- Generate Markdown copies for docs pages during the mdBook postprocess
step, including `/docs/index.md` as an alias for Getting Started.
- Generate `/docs/llms.txt` from the mdBook chapter list, grouped by
`SUMMARY.md` sections and annotated with page frontmatter descriptions.
- Generate `/docs/sitemap.xml` with `<lastmod>` values for every docs
page.
- Emit machine-readable freshness metadata in HTML via `last-modified`
and `article:modified_time` meta tags.
- Generate Cloudflare Pages `_redirects` for `.html`, extensionless, and
`.md` redirect variants, with channel-aware docs destinations.
- Add discovery hints for agents and crawlers: `rel="llms.txt"`,
`rel="alternate" type="text/markdown"`, and a short generated `llms.txt`
directive in copied Markdown pages.
- Update the docs proxy so `Accept: text/markdown`, `/docs.md`, and
direct `.md` requests can resolve to the generated Markdown artifacts.
- Move primary docs content earlier in the HTML source while preserving
the visible layout, so crawlers and agent scorers encounter the article
before sidebar chrome.
- Move the existing copy-as-Markdown control from the top navigation
into the page-title row, using the generated Markdown alternate link as
the source of truth.
- Split AI-discovery artifact generation out of
`docs_preprocessor/src/main.rs` into a focused module.

Best Practices Adopted

- Use `llms.txt` as a concise navigation index, not a dump of full page
content.
- Link to absolute, canonical Markdown URLs from `llms.txt`.
- Preserve the docs hierarchy in `llms.txt` instead of emitting a flat
sitemap-like list.
- Include short per-link descriptions from existing metadata rather than
inventing summaries.
- Keep `llms.txt`, Markdown copies, sitemap data, redirects, and
freshness metadata generated from the same mdBook source to avoid drift.
- Advertise Markdown alternates with standard HTML metadata and
same-origin URLs.
- Support both explicit Markdown URLs and content negotiation for
clients that prefer Markdown.
- Keep browser copy behavior pointed at generated Markdown alternate
links instead of duplicating route inference in JavaScript.
- Keep the copy-as-Markdown affordance in the page title row without
duplicating header chrome controls.

Validation

- `cargo check -p docs_preprocessor`
- `cargo test -p docs_preprocessor`
- `./script/clippy -p docs_preprocessor`
- `mdbook build ./docs --dest-dir=../target/deploy/docs/`
- `node --check docs/theme/plugins.js`
- `pnpm dlx prettier@3.5.0 docs/theme/plugins.js --check`
- `git diff --check`
- Local artifact checks confirmed generated Markdown pages, `llms.txt`,
`sitemap.xml` lastmod values, HTML freshness metadata, Markdown
alternate links, redirect targets, and preprocessed action/keybinding
tags resolve as expected.
- Worker URL rewrite mock covered `/docs/`, `/docs.md`,
`/docs/index.md`, extensionless docs routes, `.html` routes,
`/docs/llms.txt`, and `/docs/sitemap.xml`.
- High-effort adversarial subagent review found blockers around
channel-aware redirects, shallow-checkout date fallback, file size,
duplicated Markdown path inference, and process spawning. Those were
addressed.

Remaining Notes

- Local `python -m http.server` does not emulate Cloudflare Pages pretty
URLs, `_redirects`, or the docs-proxy Worker, so full local `afdocs`
still cannot prove content negotiation end to end.
- Existing production remains unchanged until this PR is deployed
through the docs workflow.
- Production baseline `npx afdocs check https://zed.dev/docs/ --fixes
--verbose` still reports the original failures before this PR is
deployed: 12 passed, 8 failed, 3 skipped.

Release Notes:

- Improved docs AI-readiness by adding machine-readable discovery,
Markdown access, and freshness metadata.

---------

Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2026-07-08 20:54:51 +00:00

564 lines
24 KiB
Handlebars

<!DOCTYPE HTML>
<html lang="{{ language }}" dir="{{ text_direction }}">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<style>
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.05s;
}
</style>
<!-- Theme initialization - must run before any CSS loads to prevent flicker -->
<script>
(function() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
var html = document.documentElement;
html.setAttribute('data-theme', theme);
html.setAttribute('data-color-scheme', theme);
html.className = theme;
})();
</script>
<title>{{ title }}</title>
<link rel="llms.txt" href="{{ path_to_root }}llms.txt">
{{#if is_print }}
<meta name="robots" content="noindex">
{{/if}}
#noindex#
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="description" content="#description#">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
{{#if print_enable}}
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
{{#if copy_fonts}}
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
{{/if}}
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<meta name="amplitude-key" content="#amplitude_key#" />
<meta name="consent-io-instance" content="#consent_io_instance#" />
</head>
<body class="no-js">
<div id="body-container">
<div class="noise-pattern" style="background-image: url('https://cdn.zed.dev/images/noise.png');"></div>
<!-- Provide site root to javascript -->
<script>
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
// Mark as JS-enabled
document.body.classList.remove('no-js');
document.body.classList.add('js');
</script>
<div style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;">
Agent documentation index: <a href="{{ path_to_root }}llms.txt">llms.txt</a>. Markdown versions are available for docs pages.
</div>
<header class="header-bar">
<div class="left-container">
<a href="/" class="logo-nav">
<img src="https://zed.dev/logo_icon.webp" class="icon-logo-img" alt="Zed Industries" style="height: 26px;">
</a>
<button id="sidebar-toggle" class="icon-button ib-hidden-desktop" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar" aria-expanded="false">
<i class="fa fa-bars"></i>
</button>
</div>
{{#if search_enabled}}
<button id="search-toggle" class="search-button" type="button" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="icon fa fa-search"></i>
<span class="search-content-desktop">
<span class="placeholder">Search docs…</span>
<kbd>S</kbd>
</span>
<span class="search-content-mobile">
<span class="placeholder">Search…</span>
</span>
</button>
{{/if}}
<div class="right-container">
<button id="theme-toggle" class="icon-button" type="button" title="Change Theme" aria-label="Change Theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="dark">Dark</button></li>
</ul>
<button id="copy-markdown-toggle" class="icon-button ib-hidden-mobile" type="button" title="Copy Page as Markdown" aria-label="Copy page as markdown">
<i class="fa fa-copy"></i>
</button>
<a class="download-button" href="https://zed.dev/download" title="Download Zed" aria-label="Download Zed">
Download
</a>
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
{{/if}}
</div>
</header>
<div id="page-wrapper" class="page-wrapper">
{{#if search_enabled}}
<div class="search-container">
<div id="search-wrapper" class="search-modal hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search…" aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
</div>
{{/if}}
<div class="page">
<div id="content" class="content">
<main>
{{{ content }}}
<div class="footer-buttons">
{{#previous}}
<a rel="prev" href="{{ path_to_root }}{{link}}" class="footer-button" title="{{title}}">
<i class="fa fa-angle-left"></i>
{{title}}
</a>
{{/previous}}
{{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="footer-button" title="{{title}}">
{{title}}
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
<footer class="footer">
<a href="/" class="logo-nav">
<img
src="https://zed.dev/logo_icon.webp"
class="footer-logo"
alt="Zed Industries"
/>
</a>
<span class="footer-separator">•</span>
<a class="footer-link" href="https://zed.dev"
>Back to Site</a
>
<span class="footer-separator">•</span>
<a
class="footer-link"
href="https://zed.dev/releases"
>Releases</a
>
<span class="footer-separator">•</span>
<a
class="footer-link"
href="https://zed.dev/roadmap"
>Roadmap</a
>
<span class="footer-separator">•</span>
<a
class="footer-link"
href="https://github.com/zed-industries/zed"
>GitHub</a
>
<span class="footer-separator">•</span>
<a
class="footer-link"
href="https://zed.dev/blog"
>Blog</a
>
<span class="footer-separator">•</span>
<button
id="c15t-manage-consent-btn"
class="footer-link"
>
Manage Site Cookies
</button>
</footer>
</main>
<div class="toc-container">
<nav class="pagetoc"></nav>
</div>
<!-- Immediately detect if page has headins that are not h1 to prevent flicker -->
<script>
(function() {
var tocContainer = document.querySelector('.toc-container');
var headers = document.querySelectorAll('.header');
var hasNonH1Headers = false;
for (var i = 0; i < headers.length; i++) {
var parent = headers[i].parentElement;
if (parent && !parent.tagName.toLowerCase().startsWith('h1')) {
hasNonH1Headers = true;
break;
}
}
if (hasNonH1Headers) {
tocContainer.classList.add('has-toc');
} else {
tocContainer.classList.add('no-toc');
}
})();
</script>
</div>
</div>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
{{#toc}}{{/toc}}
</div>
<div style="display: none;" id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Mobile sidebar toggle -->
<script>
(function() {
var sidebarToggle = document.getElementById('sidebar-toggle');
var sidebar = document.getElementById('sidebar');
sidebarToggle.addEventListener('click', function() {
var isOpen = document.body.classList.toggle('sidebar-open');
sidebarToggle.setAttribute('aria-expanded', isOpen);
sidebar.setAttribute('aria-hidden', !isOpen);
});
// Close sidebar when clicking a link inside it (on mobile)
sidebar.addEventListener('click', function(e) {
if (e.target.tagName === 'A' && window.innerWidth < 620) {
document.body.classList.remove('sidebar-open');
sidebarToggle.setAttribute('aria-expanded', 'false');
sidebar.setAttribute('aria-hidden', 'true');
}
});
})();
</script>
<!-- Search backdrop handlers -->
<script>
(function() {
var searchWrapper = document.getElementById('search-wrapper');
var searchContainer = document.querySelector('.search-container');
var searchResults = document.getElementById('searchresults');
if (!searchWrapper || !searchContainer) return;
searchContainer.addEventListener('click', function(e) {
if (e.target === searchContainer) {
searchWrapper.classList.add('hidden');
}
});
if (searchResults) {
searchResults.addEventListener('click', function(e) {
if (e.target.tagName === 'A' || e.target.closest('a')) {
searchWrapper.classList.add('hidden');
}
});
}
})();
</script>
<!-- Insert section spacers and apply collapsed state before scroll restoration to prevent layout shift/flicker -->
<script>
(function() {
var chapterList = document.querySelector('#sidebar ol.chapter');
if (!chapterList) return;
var collapsedSections = [];
try {
var stored = sessionStorage.getItem('sidebar-collapsed-sections');
if (stored) {
collapsedSections = JSON.parse(stored);
}
} catch (e) {}
var partTitles = chapterList.querySelectorAll('li.part-title');
var previousPartTitle = null;
partTitles.forEach(function(partTitle, index) {
partTitle._sectionName = partTitle.textContent.trim();
// Insert a spacer before this part-title (except for the first one)
if (index > 0) {
var spacer = document.createElement('li');
spacer.className = 'section-spacer';
partTitle.parentNode.insertBefore(spacer, partTitle);
if (previousPartTitle) {
previousPartTitle._spacerAfter = spacer;
}
}
var isCollapsed = collapsedSections.includes(partTitle._sectionName);
if (isCollapsed) {
// Hide all siblings until next part-title
var sibling = partTitle.nextElementSibling;
while (sibling && !sibling.classList.contains('part-title')) {
sibling.classList.add('section-hidden');
sibling = sibling.nextElementSibling;
}
// Hide the spacer after this section (will be set on next iteration)
partTitle._isCollapsed = true;
}
// If previous section was collapsed, hide its spacer
if (previousPartTitle && previousPartTitle._isCollapsed && previousPartTitle._spacerAfter) {
previousPartTitle._spacerAfter.classList.add('section-hidden');
}
previousPartTitle = partTitle;
});
// Handle the last section's spacer if it was collapsed
if (previousPartTitle && previousPartTitle._isCollapsed && previousPartTitle._spacerAfter) {
previousPartTitle._spacerAfter.classList.add('section-hidden');
}
})();
</script>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
</div>
{{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
{{/if}}
{{#if playground_line_numbers}}
<script>
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script>
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
<script src="{{ path_to_root }}mark.min.js"></script>
<script src="{{ path_to_root }}searcher.js"></script>
<script>
(function () {
// Check for focused search result and bring into the view
const ensureVisible = () => {
const focused = document.querySelector("#searchresults li.focus");
if (focused) {
focused.scrollIntoView({
block: "nearest",
inline: "nearest"
});
}
};
// 1. Listen for arrow key events
// 2. Wait for DOM to update
// 3. Call envsureVisible
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
requestAnimationFrame(ensureVisible);
}
});
})();
</script>
{{/if}}
<script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script>
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
});
});
</script>
{{else}}
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
{{/if}}
{{/if}}
<!-- c15t Consent Banner -->
<div id="c15t-banner" style="display: none;">
<div>
<p id="c15t-description">
Zed uses cookies to improve your experience and for marketing. Read <a href="https://zed.dev/cookie-policy">our cookie policy</a> for more details.
</p>
</div>
<div id="c15t-configure-section" style="display: none">
<div>
<label for="c15t-toggle-necessary"
>Strictly Necessary</label
>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-necessary"
checked
disabled
/>
<span class="c15t-slider"></span>
</label>
</div>
<div>
<label for="c15t-toggle-measurement">Analytics</label>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-measurement"
/>
<span class="c15t-slider"></span>
</label>
</div>
<div>
<label for="c15t-toggle-marketing">Marketing</label>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-marketing"
/>
<span class="c15t-slider"></span>
</label>
</div>
</div>
<div id="c15t-footer">
<button
id="c15t-configure-btn"
class="c15t-button icon"
title="Configure"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 7h-9" />
<path d="M14 17H5" />
<circle cx="17" cy="17" r="3" />
<circle cx="7" cy="7" r="3" />
</svg>
</button>
<div>
<button id="c15t-decline" class="c15t-button">
Reject all
</button>
<button id="c15t-accept" class="c15t-button primary">
Accept all
</button>
</div>
</div>
</div>
</div>
</body>
</html>