Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55b9b6e
Added an API Reference component that renders the right API name base…
alistairmatthews Aug 27, 2026
f9f60d4
Modified pages in get-started to use new component.
alistairmatthews Aug 27, 2026
e24dac8
Fixed the WCAG AA audit error.
alistairmatthews Sep 1, 2026
8b3cf15
Required FQNs and removed spans from rendered output.
alistairmatthews Sep 2, 2026
d1bd33c
Updated to use the existing data-apphost-lang storage key instead of …
alistairmatthews Sep 2, 2026
b5a8040
Fixed glossary and resrouce-mcp-servers pages.
alistairmatthews Sep 2, 2026
851527d
Fixed custom-components.vitest.test failures.
alistairmatthews Sep 10, 2026
e21e825
Improve API reference resolution performance
IEvangelist Sep 10, 2026
610aa9d
Merge remote-tracking branch 'origin/main' into create-api-reference-…
alistairmatthews Sep 11, 2026
2026af9
Resolved broken lockfile error.
alistairmatthews Sep 11, 2026
93e40d1
Added an assertion that ApiReference doesn't expose any copy buttons.
alistairmatthews Sep 14, 2026
812626d
Added cover for pointer, keyboard, PivotSelector, query-string initia…
alistairmatthews Sep 14, 2026
eee71c5
Merge main into API reference component branch
IEvangelist Sep 22, 2026
52abc57
Refine API reference links, overload selection, and authoring guidance
IEvangelist Sep 22, 2026
0fcf30c
Refine API reference lists and tooltip navigation
IEvangelist Sep 22, 2026
43c3ca9
Merge release/13.6 and resolve obsolete glossary conflict
IEvangelist Sep 23, 2026
b469371
Cover API reference navigation across ClientRouter swaps
IEvangelist Sep 23, 2026
fa0a481
Integrated feedback from @Copilot.
alistairmatthews Sep 23, 2026
9677ac8
Address tooltip lifecycle and API reference spread feedback
IEvangelist Sep 23, 2026
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
53 changes: 53 additions & 0 deletions .agents/skills/doc-writer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,59 @@ Use standard Markdown links with absolute paths from the docs root:
For more information, see [Service Defaults](/fundamentals/service-defaults/).
```

### Inline API references

Use `ApiReference` selectively to connect an explanation to API reference documentation, not to turn every API mention into a link.

- In ordinary prose, use `<ApiReference />` only on the **first named mention of a given API in an article**, when naming that API helps explain the behavior or accompanying example. Do not introduce an API name just to add a reference link.
- After that first mention, do **not** repeat the component or API reference link in ordinary prose, including prose in callouts and later sections. Prefer descriptive prose such as "this method" or "the dependency configuration." If repeating the name is necessary for clarity, use unlinked inline code appropriate to the AppHost language being discussed.
- Apply the ordinary-prose limit **per API, per article**, not per section or language tab. A different API can have its own first reference.
- In **bulleted or numbered lists**, including language-specific API explanation lists, repeated `<ApiReference />` components are allowed. Treat equivalent API entries consistently, regardless of earlier mentions, rather than mixing linked components and unlinked API names.
- An optional API reference link in **See also** is also allowed.
- Keep API names in code samples as code; do not add reference markup inside code fences.
- Match the reference to the API actually used in the example. A PostgreSQL example calling `withPostgresMcp()` / `WithPostgresMcp()` must be introduced with `Aspire.Hosting.PostgresBuilderExtensions.WithPostgresMcp`, not the generic `WithMcpServer` API.

For example, introduce an API once:

```mdx
import ApiReference from '@components/ApiReference.astro';

For PostgreSQL, use <ApiReference name="Aspire.Hosting.PostgresBuilderExtensions.WithPostgresMcp" /> to expose MCP tools for a database.
```

Later in the same article's ordinary prose, refer to "the PostgreSQL MCP helper" rather than repeating the linked API name. In an API explanation list, repeat the component for consistent entries:

```mdx
- <ApiReference name="Aspire.Hosting.PostgresBuilderExtensions.WithPostgresMcp" /> exposes MCP tools for a database.
- <ApiReference name="Aspire.Hosting.ResourceBuilderExtensions.WithReference" /> connects resources.
```

#### Selecting an overload

Author `name`, `package`, and `parameterTypes` as explicit static props. The authoring validator does not evaluate spread objects: it reports `unsupported-spread` when a spread could supply or override any of these props, including optional ones. Remove the spread, or explicitly set all three props after the last spread so their values are known.

By default, `ApiReference` shows the method name **without `()`** and links to the method group. To discuss a specific overload, supply `parameterTypes` as a static array of its **complete declared C# parameter types**, in declaration order. Copy the types from the generated C# catalog, preserving namespaces, generic arguments, and nullability. Include the `this` receiver's type for an extension method, but not the `this` keyword. Use `[]` only for a declaration with no parameters.

```mdx
<ApiReference
name="Aspire.Hosting.ResourceBuilderExtensions.WithEnvironment"
parameterTypes={[
'Aspire.Hosting.ApplicationModel.IResourceBuilder<T>',
'string',
'string?',
]}
/>
```

This links to the exact C# overload and displays `WithEnvironment(string name, string? value)`. The extension receiver is used for selection but omitted from the visible call signature. In TypeScript mode, the component uses the generated TypeScript export's own name and parameters; several C# overloads may share one TypeScript dispatcher. Do not copy C# parameter types into a TypeScript signature or select overloads by ordinal position. A missing or ambiguous match is an authoring error, not permission to link to the first overload. Use `package` when the same API is declared in multiple packages.

Linked references reuse the code-block headers' C# and TypeScript icons from `material-icon-theme`, centered inside the code background. Do not add a separate icon or empty parentheses manually. `()` is shown only when an explicitly selected API genuinely has no call parameters.

The component derives its top-positioned tooltip from the resolved API's generated summary or description, including the selected overload when specified. Do not duplicate that description in an authored `title` prop. When no description exists, the tooltip identifies the API and language instead. Tooltip titles are capped at 160 characters, including an ASCII `...` suffix when shortened, preferably at a word boundary. This presentation limit also applies to diagnostic titles; full source descriptions, diagnostics, and visible API labels remain unchanged.

Title-based tooltips use the shared `src/frontend/src/scripts/tooltips.ts` lifecycle on initial load and after ClientRouter navigation. Keep API descriptions text-only with `data-tippy-allowhtml="false"`; do not add a competing per-component initializer.
The lifecycle is installed once per document; HMR disposal removes its listeners, restores titles, and destroys tooltip instances before the replacement module initializes.

### Reference NuGet Packages

Use the 📦 emoji with links:
Expand Down
1 change: 1 addition & 0 deletions src/frontend/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const buildConcurrency = Number(process.env.ASPIRE_BUILD_CONCURRENCY) || 4;

// https://astro.build/config
export default defineConfig({
cacheDir: './node_modules/.astro',
...(outDir ? { outDir } : {}),
vite: {
define: {
Expand Down
9 changes: 5 additions & 4 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@
"astro": "pnpm git-env && astro",
"test": "pnpm test:unit && pnpm test:e2e",
"test:all": "pnpm lint && pnpm test",
"test:unit": "vitest run --config vitest.config.ts",
"test:unit": "pnpm exec astro sync && vitest run --config vitest.config.ts",
"test:unit:api-reference": "vitest run --config vitest.config.ts tests/unit/api-reference.vitest.test.ts",
"test:unit:api-markdown": "vitest run --config vitest.config.ts tests/unit/api-markdown.vitest.test.ts",
"test:unit:ts-api": "vitest run --config vitest.config.ts tests/unit/api-reference-routes.vitest.test.ts tests/unit/browser-storage.vitest.test.ts tests/unit/locale-routes.vitest.test.ts tests/unit/ts-api-routes.vitest.test.ts tests/unit/ts-api-search.vitest.test.ts",
"test:unit:twoslash-types": "vitest run --config vitest.config.ts tests/unit/twoslash-types-generator.vitest.test.ts",
"test:unit:twoslash-blocks": "vitest run --config vitest.config.ts tests/unit/twoslash-blocks.vitest.test.ts",
"test:unit:contracts": "vitest run --config vitest.config.ts tests/unit/analytics-script-contracts.vitest.test.ts tests/unit/redirects.vitest.test.ts",
"test:unit:components": "vitest run --config vitest.config.ts tests/unit/custom-components.vitest.test.ts tests/unit/site-tour.vitest.test.ts",
"test:unit:components": "vitest run --config vitest.config.ts tests/unit/api-reference.vitest.test.ts tests/unit/custom-components.vitest.test.ts tests/unit/site-tour.vitest.test.ts",
"test:unit:docs": "vitest run --config vitest.config.ts tests/unit/filetree-format.vitest.test.ts",
"test:unit:structured-data": "vitest run --config vitest.structured-data.config.ts",
"test:unit:cli-config-schema": "vitest run --config vitest.config.ts tests/unit/cli-config-schema.vitest.test.ts",
Expand Down Expand Up @@ -82,7 +83,6 @@
"astro": "^7.2.8",
"astro-contributors": "^0.9.0",
"astro-expressive-code": "^0.44.1",
"astro-tooltips": "^0.6.2",
"hast-util-to-html": "^9.0.5",
"mdast-util-to-hast": "^13.2.1",
"mermaid": "^11.16.1",
Expand All @@ -97,7 +97,8 @@
"starlight-llms-txt": "^0.11.0",
"starlight-page-actions": "^0.7.0",
"starlight-plugin-icons": "^1.1.6",
"starlight-sidebar-topics": "^0.8.0"
"starlight-sidebar-topics": "^0.8.0",
"tippy.js": "6.3.7"
},
"devDependencies": {
"@axe-core/playwright": "^4.12.1",
Expand Down
13 changes: 3 additions & 10 deletions src/frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions src/frontend/src/components/ApiReference.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
import { resolveApiReference } from '@utils/api-reference';

interface Props {
/**
* Canonical fully qualified C# API member name, including the declaring type.
* Example: `Aspire.Hosting.PostgresBuilderExtensions.AddPostgres`.
*/
name: string;
/**
* Package identity used only when multiple packages declare the same FQN.
* Example: `Aspire.Hosting.JavaScript`.
*/
package?: string;
/**
* Complete declared C# parameter types, in order, including an extension
* method's receiver. Omit for a method-group link; [] selects zero parameters.
*/
parameterTypes?: string[];
}

function formatTitle(title: string | undefined): string | undefined {
if (!title || title.length <= 160) return title;

const prefix = title.slice(0, 157);
const text = /\s/.test(title[157]) ? prefix : prefix.replace(/\s+\S*$/, '');
return `${text.trimEnd()}...`;
}

const { name, package: packageName, parameterTypes } = Astro.props;
const resolution = await resolveApiReference(name, packageName, Astro.locals, parameterTypes);
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const csharpHref = resolution.csharp.path ? `${base}${resolution.csharp.path}` : undefined;
const typescriptHref = resolution.typescript.path
? `${base}${resolution.typescript.path}`
: undefined;
const diagnosticText = resolution.diagnostics.map((diagnostic) => diagnostic.message).join(' ');
const csharpDiagnostic = resolution.status === 'resolved' ? undefined : diagnosticText;
const typescriptDiagnostic = resolution.diagnostics.length > 0 ? diagnosticText : undefined;
---

<span class="api-reference not-content" data-status={resolution.status}>
{
csharpHref ? (
<a
class="ar-lang"
data-lang="csharp"
href={csharpHref}
aria-label={`${resolution.csharp.label} — C# API reference`}
title={formatTitle(
resolution.csharp.description ?? `${resolution.csharp.label} — C# API reference`
)}
data-tooltip-placement="top"
data-tippy-allowhtml="false"
>
<code>
<span class="ar-icon i-material-icon-theme:csharp" aria-hidden="true" />
<span>{resolution.csharp.label}</span>
</code>
</a>
) : (
<span
class="ar-lang"
data-lang="csharp"
title={formatTitle(csharpDiagnostic)}
data-tooltip-placement="top"
data-tippy-allowhtml="false"
>
<code>{resolution.csharp.label}</code>
</span>
)
}
{
typescriptHref ? (
<a
class="ar-lang"
data-lang="typescript"
href={typescriptHref}
aria-label={`${resolution.typescript.label} — TypeScript API reference`}
title={formatTitle(
resolution.typescript.description ??
`${resolution.typescript.label} — TypeScript API reference`
)}
data-tooltip-placement="top"
data-tippy-allowhtml="false"
>
<code>
<span class="ar-icon i-material-icon-theme:typescript" aria-hidden="true" />
<span>{resolution.typescript.label}</span>
</code>
</a>
) : (
<span
class="ar-lang"
data-lang="typescript"
title={formatTitle(typescriptDiagnostic)}
data-tooltip-placement="top"
data-tippy-allowhtml="false"
>
<code>{resolution.typescript.label}</code>
</span>
)
}
</span>

<style>
.api-reference {
display: inline;
}

.ar-lang {
display: none;
}

:global(html[data-apphost-lang='typescript']) .ar-lang[data-lang='typescript'] {
display: inline;
}

:global(html:not([data-apphost-lang='typescript'])) .ar-lang[data-lang='csharp'] {
display: inline;
}

a.ar-lang {
color: var(--sl-color-text-accent);
text-decoration: none;
}

.ar-icon {
display: block;
flex: 0 0 1em;
width: 1em;
height: 1em;
}

.ar-lang code {
display: inline-flex;
align-items: center;
gap: 0.3em;
max-width: 100%;
box-sizing: border-box;
vertical-align: middle;
font-family: var(--sl-font-mono, ui-monospace, SFMono-Regular, monospace);
font-size: var(--sl-text-body);
font-weight: 400;
line-height: 1.5;
white-space: normal;
color: inherit;
background-color: var(--aspire-inline-code-bg);
border: 0;
padding: 0.08em 0.3em;
border-radius: 0.25rem;
overflow-wrap: anywhere;
}

.ar-lang code > span {
min-width: 0;
}

a.ar-lang:is(:hover, :focus-visible) code {
background-color: color-mix(in srgb, var(--aspire-inline-code-bg), currentColor 15%);
}
</style>
30 changes: 2 additions & 28 deletions src/frontend/src/components/starlight/Head.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import { Tooltips } from 'astro-tooltips';
import 'tippy.js/dist/tippy.css';
import DefaultHead from '@astrojs/starlight/components/Head.astro';
import { ClientRouter } from 'astro:transitions';
import AccessibleCodeButtons from '../AccessibleCodeButtons.astro';
Expand Down Expand Up @@ -574,34 +574,8 @@ function computeSourceUrl() {
import '@scripts/webmcp';
</script>

<Tooltips
interactive={false}
allowHTML={true}
delay={[0, 0]}
duration={[0, 0]}
hideOnClick={true}
animation="scale"
onClickOutside={function (instance, event) {
if (instance) {
instance.hide();
}
}}
/>

<script is:inline>
(function () {
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
const activeElement = document.activeElement;
if (activeElement && activeElement._tippy) {
activeElement._tippy.hide();
}
}
});
})();
</script>

<script>
import '@scripts/tooltips';
import '@scripts/twoslash-hover';
</script>

Expand Down
Loading
Loading