---
title: "Line permalinks"
description: "Give a code block line numbers that link to each line, so readers can share a link to the exact lines they mean. Shift-click to select multiple lines."
url: "https://ewels.github.io/starlight-codeblocks/features/line-permalinks/"
markdown: "https://ewels.github.io/starlight-codeblocks/features/line-permalinks.md"
section: "Link code"
site: "starlight-codeblocks documentation"
context: "This page is from the documentation of starlight-codeblocks. A Starlight plugin that adds focus, line states, annotations, API auto-linking and 22 more features to the code blocks of a site."
index: "https://ewels.github.io/starlight-codeblocks/llms.txt"
---

# Line permalinks

> Give a code block line numbers that link to each line, so readers can share a link to the exact lines they mean. Shift-click to select multiple lines.

A normal code block has no address for its lines. A reader who asks a question about one setting can only link to the page, or to the heading above the block. Line permalinks give a block line numbers, and each number is a link to its line.

````md
```yaml title="config.yml" id="cfg"
server:
  host: 0.0.0.0
  port: 8080
cache:
  dir: .cache
  max_age: 3600
logging:
  level: info
```
````

Click a line number to highlight that line. The address of the page changes to `#cfg-L2`, and the page does not move. Hold Shift and click a second number to highlight every line between the two, as `#cfg-L2-L4`. Open that address in a new tab, and the page scrolls to the lines and highlights them.

## Syntax

| Syntax | Where |
|---|---|
| `id="<id>"` | Code block fence line |

The `id` turns on line numbers for that block. It is also the `id` of the block in the page, so `#cfg` links to the whole block. Each block on a page needs its own `id`.

Pick an `id` from what the block contains, such as its file name. The `id` keeps links to the block working when you add other blocks to the page.

## Examples

### Numbers that match the file

A block that shows part of a file can start at the line number of that part, so the numbers match the file:

````md
```py title="app.py" id="app" startLineNumber=41
@app.get("/health")
def health():
    return {"status": "ok"}
```
````

### With hidden lines and a callout

This example combines line permalinks with hidden lines and an inline callout on purpose. The hidden-lines marker and the callout arrow line up with the code after the line numbers:

````md
```ts title="server.ts" id="server" hidden={1-2}
import { createServer } from 'node:http';
import { handler } from './handler.ts';

const port = Number(process.env.PORT ?? 3000);
// [!callout /listen/] Starts the server on the port.
createServer(handler).listen(port);
```
````

## Behaviour

- The links:
  - Each line number links to `#<id>-L<n>`, where `<n>` is the number that the reader sees. If the fence line (the first line of the code block, with the language) has `startLineNumber`, the numbers start there.
  - Clicking a number highlights that line and changes the address with `history.replaceState`. The page does not scroll, and the browser history gets no new entry.
  - Clicking a number with Shift held highlights the range from the last number you clicked, as `#<id>-L<a>-L<b>`.
- Opening a link:
  - When the page loads, or the address changes, the plugin highlights the lines in the address and scrolls them into view. If a line is a [hidden line](https://ewels.github.io/starlight-codeblocks/features/hidden-lines/), its marker opens. If a line is in a `<Tabs>` panel that is not selected, its tab opens.
- Keyboard and screen readers:
  - The line numbers are links, so you can reach them with the Tab key and press Enter to highlight a line. Shift and Enter together highlights a range. Screen readers announce each one as "Link to line N", and the highlighted numbers have `aria-current`.
- Copy:
  - Line numbers are not part of the copied text, and a manual selection of the code leaves them out.
- Warnings:
  - Two blocks with the same `id` on one page cause a build warning.
- Without JavaScript:
  - The links go to their line, without the highlight.

## Options

Line permalinks have no options. Turn the feature off for the whole site with `permalinks: false`:

```js title="astro.config.mjs"
codeblocks({
  permalinks: false,
});
```

With `permalinks: false`, the plugin ignores `id` on the fence line.

- [Configuration](https://ewels.github.io/starlight-codeblocks/configuration/): Learn how to set options for the whole site, or for a single code block.

## Limitations

- Line numbers take space on the left of every line. On a phone, a block with long lines scrolls sooner.
- The plugin highlights one range at a time on a page. A new highlight in any block removes the old highlight.
- An `id` must not match the `id` of a heading on the page. Heading ids come from the heading text, such as `#options`.
- The `starlight-links-validator` plugin reports links to a block or a line, such as `#cfg-L2`, as broken. Give it `exclude: linksValidatorExclude`, imported from `starlight-codeblocks`.

## Related

- [Line states](https://ewels.github.io/starlight-codeblocks/features/line-states/): mark a line as an error, a warning or a note in the source, for every reader.
- [Code mentions](https://ewels.github.io/starlight-codeblocks/features/code-mentions/): highlight lines from a phrase in the prose, without a line number.
- [Hidden lines](https://ewels.github.io/starlight-codeblocks/features/hidden-lines/): keep set-up lines out of view. Their line numbers still count.
