Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export default function Layout({ children, frontmatter }) {
<span className="header-divider" aria-hidden="true" />
<nav className="header-nav" aria-label="Main navigation">
<Link href="/getting-started/quickstart" className="header-nav-link">Docs</Link>
<Link href="/tools/scaffold" className="header-nav-link">Tools</Link>
<Link href="/tools/hypercerts-agent-skills" className="header-nav-link">Tools</Link>
</nav>
<div style={{ flex: 1 }} />
<button
Expand Down
1 change: 0 additions & 1 deletion components/SearchDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const QUICK_LINK_PATHS = [
'/getting-started/quickstart',
'/core-concepts/what-is-hypercerts',
'/core-concepts/hypercerts-core-data-model',
'/tools/scaffold',
'/architecture/overview',
'/reference/glossary',
];
Expand Down
4 changes: 4 additions & 0 deletions lib/generate-search-index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { readdirSync, statSync, readFileSync, writeFileSync } = require("fs");
const { join, relative } = require("path");
const { hiddenPaths } = require("./page-visibility.json");
const {
loadExternalDocsContent,
parseMarkdownFrontmatter,
Expand All @@ -9,6 +10,7 @@ const {
const PAGES_DIR = join(__dirname, "..", "pages");
const OUTPUT = join(__dirname, "..", "public", "search-index.json");
const MAX_BODY_LENGTH = 5000;
const HIDDEN_PATHS = new Set(hiddenPaths);

function walkDir(dir) {
const results = [];
Expand Down Expand Up @@ -111,6 +113,8 @@ async function main() {
const rel = "/" + relative(PAGES_DIR, file).replace(/\.md$/, "");
const path = rel === "/index" ? "/" : rel;

if (HIDDEN_PATHS.has(path)) continue;

const frontmatter = parseMarkdownFrontmatter(localContent, relative(PAGES_DIR, file));
const title = getStringFrontmatterValue(frontmatter, "title");
const description = getStringFrontmatterValue(frontmatter, "description");
Expand Down
4 changes: 3 additions & 1 deletion lib/generate-sitemap.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const fs = require('fs');
const path = require('path');
const { hiddenPaths } = require('./page-visibility.json');

const SITE_URL = 'https://docs.hypercerts.org';
const HIDDEN_PATHS = new Set(hiddenPaths);

function getPages(dir, base = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
Expand All @@ -26,7 +28,7 @@ function getPages(dir, base = '') {

function generateSitemap() {
const pagesDir = path.join(__dirname, '..', 'pages');
const pages = getPages(pagesDir);
const pages = getPages(pagesDir).filter((page) => !HIDDEN_PATHS.has(page));

const today = new Date().toISOString().split('T')[0];

Expand Down
3 changes: 0 additions & 3 deletions lib/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,9 @@ export const navigation = [
{
section: "Tools",
children: [
{ title: "Scaffold Starter App", path: "/tools/scaffold" },
{ title: "Hypercerts CLI", path: "/tools/hypercerts-cli" },
{ title: "Agent Skills", path: "/tools/hypercerts-agent-skills" },
{ title: "Hyperindex", path: "/tools/hyperindex" },
{ title: "Hyperscan", path: "/tools/hyperscan" },
{ title: "Hyperboards", path: "/tools/hyperboards" },
{ title: "Labelers", path: "/tools/labelers" },
],
},
Expand Down
7 changes: 7 additions & 0 deletions lib/page-visibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"hiddenPaths": [
"/tools/hypercerts-cli",
"/tools/scaffold",
"/tools/hyperboards"
]
}
2 changes: 1 addition & 1 deletion pages/architecture/account-and-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ See [ePDS (extended PDS)](/architecture/epds) for integration examples, handle m

For scripts, CLI tools, and server-side automation, use app passwords instead of your main password. App passwords are scoped credentials that can be revoked independently.

Create one in your account settings at [certified.app](https://certified.app). Give it a descriptive name (e.g., "CI/CD pipeline" or "Local development"). If a credential is compromised, revoke just that app password — your main account stays secure. The [Hypercerts CLI](/tools/hypercerts-cli) uses app passwords for authentication.
Create one in your account settings at [certified.app](https://certified.app). Give it a descriptive name (e.g., "CI/CD pipeline" or "Local development"). If a credential is compromised, revoke just that app password — your main account stays secure.

{% callout type="warning" %}
Never commit app passwords to version control. Use environment variables or a secrets manager.
Expand Down
4 changes: 1 addition & 3 deletions pages/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const { session } = await client.callback(new URLSearchParams(callbackQuery));
const agent = new Agent(session);
```

See the [AT Protocol OAuth documentation](https://atproto.com/specs/oauth) for full details on client metadata, session storage, and keyset configuration. For further info on how to set up OAuth you can check out [AT Protos node.js implementation tutorial](https://atproto.com/guides/oauth-cli-tutorial) or the [scaffold app](/tools/scaffold).
See the [AT Protocol OAuth documentation](https://atproto.com/specs/oauth) for full details on client metadata, session storage, and keyset configuration. For a step-by-step Node.js example, see the [AT Protocol OAuth tutorial](https://atproto.com/guides/oauth-cli-tutorial).

## Create your first hypercert

Expand Down Expand Up @@ -347,8 +347,6 @@ Your hypercert is now created and stored on your PDS. The completion screen show
![Completion screen showing the finished hypercert](/images/scaffold/finalized-cert.png)
*The hypercert is created and stored on your PDS.*

For full details on the Scaffold app — including self-hosting, environment setup, and extending the codebase — see the [Scaffold app documentation](/tools/scaffold).

## Next steps

Third parties can now [evaluate your hypercert](/getting-started/working-with-evaluations) by creating evaluation records and measurements on their own PDS.
1 change: 0 additions & 1 deletion pages/tools/hyperscan.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,4 @@ Use the Agent API when you want a tool, bot, or coding agent to understand the c

- [Hyperindex](/tools/hyperindex) — indexer used to query and serve indexed Hypercerts data
- [Labelers](/tools/labelers) — ATProto label services used for quality and trust signals
- [Hypercerts CLI](/tools/hypercerts-cli) — command-line tool for creating and inspecting Hypercerts records
- [Introduction to Lexicons](/lexicons/introduction-to-lexicons) — schemas behind the records shown in Hyperscan
69 changes: 69 additions & 0 deletions test/hidden-tool-visibility.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const { existsSync, readFileSync, readdirSync } = require('node:fs');
const { join, relative } = require('node:path');
const test = require('node:test');

const ROOT = join(__dirname, '..');
const PAGES_DIR = join(ROOT, 'pages');
const HIDDEN_PATHS = [
'/tools/hypercerts-cli',
'/tools/scaffold',
'/tools/hyperboards',
];
const HIDDEN_PAGE_FILES = new Set(HIDDEN_PATHS.map((path) => `pages${path}.md`));

function walkMarkdownFiles(directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory() ? walkMarkdownFiles(path) : path.endsWith('.md') ? [path] : [];
});
}

test('keeps the hidden tool page source files', () => {
for (const file of HIDDEN_PAGE_FILES) {
assert.equal(existsSync(join(ROOT, file)), true, `${file} must remain available for direct routes`);
}
});

test('excludes hidden tool pages from the generated search index', () => {
execFileSync(process.execPath, ['lib/generate-search-index.js'], { cwd: ROOT });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Provide the external-docs-content prerequisite before running this test.

execFileSync invokes the search-index generator directly, but CI reports that it crashes with missing external-docs-content.json; the hidden-route assertion is never reached. Add test setup/fixtures for that prerequisite, or invoke the repository’s documented preparation step before generating the index.

🧰 Tools
🪛 GitHub Actions: Docs CI / 0_Test and build documentation.txt

[error] 29-30: Test failed: 'excludes hidden tool pages from the generated search index' (ERR_TEST_FAILURE) due to missing external docs content file.

🪛 GitHub Actions: Docs CI / Test and build documentation

[error] 29-30: Test failed: 'excludes hidden tool pages from the generated search index' due to missing external-docs-content.json (generate-search-index crashed with ERR_TEST_FAILURE).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/hidden-tool-visibility.test.js` around lines 29 - 30, Update the setup
around the hidden-tool visibility test and its execFileSync call to ensure
external-docs-content.json is available before invoking
lib/generate-search-index.js. Use the repository’s documented preparation step
or add the required test fixture, while preserving the existing hidden-page
exclusion assertion.

Source: Pipeline failures

const index = JSON.parse(readFileSync(join(ROOT, 'public/search-index.json'), 'utf8'));
const indexedHiddenPaths = index
.map((page) => page.path)
.filter((path) => HIDDEN_PATHS.includes(path));

assert.deepEqual(indexedHiddenPaths, []);
});

test('excludes hidden tool pages from the generated sitemap', () => {
execFileSync(process.execPath, ['lib/generate-sitemap.js'], { cwd: ROOT });
const sitemap = readFileSync(join(ROOT, 'public/sitemap.xml'), 'utf8');

for (const path of HIDDEN_PATHS) {
assert.doesNotMatch(sitemap, new RegExp(`<loc>[^<]+${path}</loc>`));
}
});

test('does not link to hidden tool pages from visible documentation or navigation', () => {
const authoredFiles = [
join(ROOT, 'components/Layout.js'),
join(ROOT, 'components/SearchDialog.js'),
join(ROOT, 'lib/navigation.js'),
...walkMarkdownFiles(PAGES_DIR).filter((file) => {
const repoPath = relative(ROOT, file).split('\\').join('/');
return !HIDDEN_PAGE_FILES.has(repoPath);
}),
];

for (const file of authoredFiles) {
const content = readFileSync(file, 'utf8');
for (const path of HIDDEN_PATHS) {
assert.equal(
content.includes(path),
false,
`${relative(ROOT, file)} still links to ${path}`,
);
}
}
});
Loading