Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 8 additions & 8 deletions .github/workflows/markdown-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Check links in Documentation
uses: JustinBeckwith/linkinator-action@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
paths: 'Documentation/**/*.md'
markdown: true
recurse: true
redirects: allow
statusCodes: '403:ok'
linksToSkip: '^(https?:\/\/)?(localhost|127\.0\.0\.1)(:\d+)?(\/|$)'
node-version: 23.x

# The same script contributors run locally, so that what CI checks and
# what verify-markdown.sh checks cannot drift apart.
- name: Check links in Documentation
run: ./Documentation/verify-links.sh
2 changes: 1 addition & 1 deletion Documentation/backend/chronicle/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Without this package, Arc and Chronicle are independent. With it:
| Topic | Description |
| ----- | ----------- |
| [Aggregates](aggregates/index.md) | Working with aggregate roots and event sourcing. |
| [Add event sourcing to an Arc slice](add-event-sourcing.md) | Move one database-backed slice to Chronicle while keeping its query and React screen in place. |
| [Add event sourcing to an Arc slice](add-event-sourcing.mdx) | Move one database-backed slice to Chronicle while keeping its query and React screen in place. |
| [Cratis Package](cratis-package.md) | The convenience package for Arc + Chronicle applications. |
| [React to an event](react-to-an-event.md) | Run side effects or follow-up commands from Chronicle events with reactors. |
| [Commands](commands/index.md) | Returning events from commands, event source id resolution, and concurrency scoping. |
Expand Down
2 changes: 1 addition & 1 deletion Documentation/backend/chronicle/react-to-an-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ A reactor may be called more than once for the same event — during replay or r
## See also

- [Returning Commands as Side Effects](./reactors/toc.yml) — execute commands as side effects from a reactor.
- [Add event sourcing to an Arc slice](./add-event-sourcing.md) — where reactors enter the Arc model.
- [Add event sourcing to an Arc slice](./add-event-sourcing.mdx) — where reactors enter the Arc model.
- [Return a result or an error](/arc/scenarios/return-a-result-or-error/) — what the command you execute can return.
2 changes: 1 addition & 1 deletion Documentation/backend/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Get an Arc backend up and building features. These first pages use a plain datab

Once you have a project:

- **[Your first command and query](./your-first-command.md)** — build a backend slice end to end: a command with `Handle()`, the read model it writes, and the live query that serves it.
- **[Your first command and query](./your-first-command.mdx)** — build a backend slice end to end: a command with `Handle()`, the read model it writes, and the live query that serves it.
- **[MongoDB integration](../mongodb/index.md)** — configure Arc over MongoDB collections and observable change streams.
- **[Entity Framework integration](../entity-framework/getting-started.md)** — configure Arc over DbContexts and observed DbSets.

Expand Down
2 changes: 1 addition & 1 deletion Documentation/backend/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ Arc meets the rest of your stack:
| [Open API](./open-api/index.md) | OpenAPI/Swagger generation. |
| [Code Analysis](./code-analysis/index.md) | Analyzers and fixers that catch mistakes at compile time. |

Building the UI on top? Head to the [frontend](../frontend/index.md), which consumes everything here
Building the UI on top? Head to the [frontend](../frontend/index.mdx), which consumes everything here
through the generated proxies.
2 changes: 1 addition & 1 deletion Documentation/frontend/react/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ those two with validation, scopes, and identity.
| [Identity](./identity.md) | Who the user is, and what they're allowed to see and do. |
| [Dialogs](./dialogs.md) | Consistent dialog handling for command and data-entry flows. |
| [Proxy Generation](../../backend/proxy-generation/index.md) | How the typed proxies you import here are generated from C#. |
| [Storybook](./storybook.md) | The Storybook for the components Arc exposes. |
| [Storybook](./storybook.mdx) | The Storybook for the components Arc exposes. |
| [Story Components](./stories) | Building good-looking, consistent stories. |

Prefer a structured, testable approach for complex screens? See [MVVM with React](../react.mvvm/index.md).
2 changes: 1 addition & 1 deletion Documentation/tutorial/books-and-relationships.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,4 @@ The `authorId` is context the form needs to be *valid* from the start, so it goe
- A **relationship as a foreign key + a filtered query**: `BooksForAuthor` reads exactly one author's catalog, live, with no join.
- A React screen that composes the two: a list of authors, each rendering its own live catalog.

Two features, related, both live. We keep *saying* "it stays live" — next we'll make that concrete and watch a screen update itself the instant the data changes. [Let's make it live →](./real-time)
Two features, related, both live. We keep *saying* "it stays live" — next we'll make that concrete and watch a screen update itself the instant the data changes. [Let's make it live →](./real-time.mdx)
2 changes: 1 addition & 1 deletion Documentation/tutorial/first-slice.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,4 @@ In one folder, read top to bottom:

That's a complete vertical slice, backend to browser, over a plain database. The next feature will be another folder just like it.

There's one problem, though: right now a librarian can register an author with a blank name, or the same author twice, and nothing stops them. A real app has to say no. [Let's make it trustworthy →](./validation)
There's one problem, though: right now a librarian can register an author with a blank name, or the same author twice, and nothing stops them. A real app has to say no. [Let's make it trustworthy →](./validation.mdx)
12 changes: 6 additions & 6 deletions Documentation/tutorial/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ An Arc project running locally with a database. The [Get started](/arc/backend/g

## The tour

1. **[Your first full-stack slice](./first-slice)** — register an author from C# all the way to a live React screen, fully typed.
2. **[Make it trustworthy](./validation)** — reject bad input with a validator and a uniqueness rule, and show the reason in the form.
3. **[Relate your slices](./books-and-relationships)** — add books that belong to an author, and read them back.
4. **[Make it live](./real-time)** — observable queries that update the screen the moment the data changes.
5. **[Decide who can do what](./authorization)** — lock the catalog down with role-based authorization.
1. **[Your first full-stack slice](./first-slice.mdx)** — register an author from C# all the way to a live React screen, fully typed.
2. **[Make it trustworthy](./validation.mdx)** — reject bad input with a validator and a uniqueness rule, and show the reason in the form.
3. **[Relate your slices](./books-and-relationships.mdx)** — add books that belong to an author, and read them back.
4. **[Make it live](./real-time.mdx)** — observable queries that update the screen the moment the data changes.
5. **[Decide who can do what](./authorization.mdx)** — lock the catalog down with role-based authorization.

Each chapter ends where the next begins. By the end you'll have a real full-stack feature — and the model to build your own. Ready? [Let's build the first slice →](./first-slice)
Each chapter ends where the next begins. By the end you'll have a real full-stack feature — and the model to build your own. Ready? [Let's build the first slice →](./first-slice.mdx)
2 changes: 1 addition & 1 deletion Documentation/tutorial/real-time.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ That's the whole switch between "load once" and "stay live." You choose per quer
- A clear picture of **why your screens update themselves** — observable queries are standing subscriptions, database to browser.
- The one-line difference between a **one-shot** query and a **live** one — the return type, nothing else.

The catalog is live. There's just one thing left before it's a real back office: right now *anyone* can register authors and add books. Let's decide who's allowed. [Lock it down →](./authorization)
The catalog is live. There's just one thing left before it's a real back office: right now *anyone* can register authors and add books. Let's decide who's allowed. [Lock it down →](./authorization.mdx)
2 changes: 1 addition & 1 deletion Documentation/tutorial/validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,4 @@ One rule on the value type, one rule on the command, and both ends of the app ho
- A `RegisterAuthorValidator` **business rule** that checks existing state in the database, with a unique index behind it for the hard guarantee.
- A React form that surfaces both — client-side and server-side — with no extra frontend code.

Our author is trustworthy now. But an author with no books is a lonely thing. Next we'll give them a catalog — a second feature that *relates* to this one. [Let's add books →](./books-and-relationships)
Our author is trustworthy now. But an author with no books is a lonely thing. Next we'll give them a catalog — a second feature that *relates* to this one. [Let's add books →](./books-and-relationships.mdx)
85 changes: 85 additions & 0 deletions Documentation/verify-links.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash

# Link Verification Script
# Verifies the links in the documentation of this repository, and is used both
# by verify-markdown.sh and by the Markdown Verification workflow so the two
# cannot drift apart.
#
# What is verified here:
# - relative links between pages in this repository
# - external http(s) links
#
# What is not verified here:
# Links written as site-absolute paths (/arc/..., /chronicle/...) address the
# aggregated documentation site, where each product's Documentation folder is
# mounted under its own prefix. They cannot resolve against a single
# repository and are verified when that site is built. They are skipped by
# rule rather than by name, so a new prefix needs no change here.

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Pinned so that a new major version cannot silently change what is scanned.
LINKINATOR_VERSION="8.0.2"

# linkinator serves the scanned files from a local web server, so every internal
# link resolves to http://127.0.0.1:<port>/<path>. Skipping what falls outside
# /Documentation/ therefore skips exactly the site-absolute paths this
# repository cannot resolve, and nothing else. The pattern must never match the
# crawl root itself - a pattern that did is what turned this check into a no-op.
SITE_ABSOLUTE_LINKS='^https?://(localhost|127\.0\.0\.1):[0-9]+/(?!Documentation/)'

cd "$ROOT_DIR"

if ! command -v npx &> /dev/null; then
echo "Error: npx is not installed. Please install Node.js and npm."
exit 1
fi

echo "Checking links in Documentation..."
echo "This may take a few minutes to check all links..."
echo ""

set +e
OUTPUT=$(npx --yes "linkinator@$LINKINATOR_VERSION" \
"Documentation/**/*.md" \
"Documentation/**/*.mdx" \
--markdown \
--recurse \
--directory-listing \
--verbosity error \
--status-code "403:ok" \
--skip "$SITE_ABSOLUTE_LINKS" 2>&1)
LINKINATOR_EXIT_CODE=$?
set -e

echo "$OUTPUT"

# How many links were actually looked at. A check that scans nothing reports
# success while verifying nothing, which reads exactly like a passing check.
SCANNED=$(echo "$OUTPUT" | grep -oE 'canned [0-9]+ link' | grep -oE '[0-9]+' | tail -1)

echo ""

if [ -z "$SCANNED" ]; then
echo "✗ Link verification could not determine how many links were scanned."
echo " linkinator reported no scan summary - the file globs most likely matched nothing."
exit 1
fi

if [ "$SCANNED" -eq 0 ]; then
echo "✗ Link verification scanned zero links."
echo " Nothing was checked, so this run proves nothing about the links in Documentation."
exit 1
fi

if [ $LINKINATOR_EXIT_CODE -ne 0 ]; then
echo "✗ Link verification failed - $SCANNED links scanned."
exit $LINKINATOR_EXIT_CODE
fi

echo "✓ Link verification passed - $SCANNED links scanned."
echo " Site-absolute links (/arc/..., /chronicle/...) resolve only on the aggregated"
echo " documentation site and are verified when that site is built."
28 changes: 13 additions & 15 deletions Documentation/verify-markdown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ echo "Markdown Verification"
echo "=========================================="
echo ""

# Check if running from repository root or Documentation folder
if [ "$(basename "$PWD")" = "Documentation" ]; then
cd ..
fi
# Both steps address paths from the repository root, whether this script is run
# from there or from the Documentation folder.
cd "$ROOT_DIR"

echo "Working directory: $PWD"
echo ""
Expand All @@ -32,8 +31,13 @@ if ! command -v npx &> /dev/null; then
exit 1
fi

npx markdownlint-cli2 "Documentation/**/*.md"
LINT_EXIT_CODE=$?
# Each step captures its own exit code and the run continues, so that a failing
# lint step does not leave the state of the links unreported.
if npx --yes markdownlint-cli2 "Documentation/**/*.md"; then
LINT_EXIT_CODE=0
else
LINT_EXIT_CODE=$?
fi

echo ""
if [ $LINT_EXIT_CODE -eq 0 ]; then
Expand All @@ -48,17 +52,11 @@ echo "=========================================="
echo "Step 2: Running link verification..."
echo "=========================================="
echo ""
echo "This may take a few minutes to check all links..."
echo ""

npx linkinator "Documentation/**/*.md" --markdown --recurse --verbosity error --status-code "403:ok" --skip "^(https?:\\/\\/)?(localhost|127\\.0\\.0\\.1)(:\\d+)?(\\/|$)"
LINK_EXIT_CODE=$?

echo ""
if [ $LINK_EXIT_CODE -eq 0 ]; then
echo "✓ Link verification passed!"
if "$SCRIPT_DIR/verify-links.sh"; then
LINK_EXIT_CODE=0
else
echo "✗ Link verification failed with exit code $LINK_EXIT_CODE"
LINK_EXIT_CODE=$?
fi
echo ""

Expand Down
Loading