├── src
├── env.d.ts
├── Schema.astro
├── Pagination.astro
├── Navigation.astro
├── NavigationList.astro
├── Breadcrumbs.astro
├── NavigationItem.astro
├── schemas.ts
└── utils.ts
├── .prettierignore
├── tsconfig.json
├── .github
├── renovate.json
└── workflows
│ ├── renovate.yml
│ └── release.yml
├── .prettierrc.cjs
├── .gitignore
├── .changeset
├── config.json
└── README.md
├── index.ts
├── package.json
├── CHANGELOG.md
├── README.md
└── pnpm-lock.yaml
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | pnpm-lock.yaml
3 | dist
4 | node_modules
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "branchPrefix": "renovate/",
3 | "dryRun": true,
4 | "username": "renovate-release",
5 | "gitAuthor": "Renovate Bot ",
6 | "platform": "github"
7 | }
8 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require.resolve('prettier-plugin-astro')],
3 | printWidth: 120,
4 | semi: false,
5 | singleQuote: true,
6 | overrides: [
7 | {
8 | files: '*.astro',
9 | options: {
10 | parser: 'astro',
11 | },
12 | },
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # dependencies
5 | node_modules/
6 |
7 | # logs
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | pnpm-debug.log*
12 |
13 |
14 | # environment variables
15 | .env
16 | .env.production
17 |
18 | # macOS-specific files
19 | .DS_Store
20 |
21 | # Local Netlify folder
22 | .netlify
23 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
3 | "changelog": ["@changesets/cli/changelog", { "repo": "tony-sull/astro-navigation" }],
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | // Do not write code directly here, instead use the `src` folder!
2 | // Then, use this file to export everything you want your user to access.
3 |
4 | import Breadcrumbs from './src/Breadcrumbs.astro'
5 | import Navigation from './src/Navigation.astro'
6 | import Pagination from './src/Pagination.astro'
7 | import Schema from './src/Schema.astro'
8 | export type { WebPage } from './src/utils.js'
9 |
10 | export default Navigation
11 | export { Breadcrumbs, Pagination, Schema }
12 |
--------------------------------------------------------------------------------
/src/Schema.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Thing } from 'schema-dts'
3 | import { ldToString } from './schemas.js'
4 |
5 | export interface Props {
6 | /** Adds indentation, white space, and line break characters to JSON-LD output. {@link JSON.stringify} */
7 | space?: string | number
8 | json: Thing | Thing[]
9 | }
10 |
11 | const { json, space } = Astro.props
12 |
13 | const children = ldToString(json, space)
14 | ---
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.github/workflows/renovate.yml:
--------------------------------------------------------------------------------
1 | name: Renovate
2 | on:
3 | schedule:
4 | # The "*" (#42, asterisk) character has special semantics in YAML, so this
5 | # string has to be quoted.
6 | - cron: '0/15 * * * *'
7 | jobs:
8 | renovate:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2.0.0
13 | - name: Self-hosted Renovate
14 | uses: renovatebot/github-action@v32.118.0
15 | with:
16 | configurationFile: .github/renovate.json
17 | token: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/src/Pagination.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes, HTMLTag } from 'astro/types'
3 | import { findPaginationEntries } from './utils.js'
4 |
5 | export interface Props extends HTMLAttributes<'ul'> {
6 | as?: HTMLTag
7 | nextLabel: string
8 | prevLabel: string
9 | }
10 |
11 | const { as: Component = 'ol', nextLabel, prevLabel, ...attrs } = Astro.props
12 |
13 | const { next, prev } = findPaginationEntries(Astro.url.pathname)
14 | ---
15 |
16 |
17 | {
18 | prev && (
19 |
20 | {prev.title}
21 |
22 | )
23 | }
24 | {
25 | next && (
26 |
27 | {next.title}
28 |
29 | )
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/Navigation.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes, HTMLTag } from 'astro/types'
3 | import NavigationList from './NavigationList.astro'
4 | import { fetchPage, findNavigationEntries } from './utils.js'
5 | import Schema from './Schema.astro'
6 |
7 | export interface Props extends HTMLAttributes<'nav'> {
8 | as?: HTMLTag
9 | showExcerpts?: boolean
10 | itemAttrs?: Omit
11 | }
12 |
13 | const { as: Component = 'ol', showExcerpts = false, itemAttrs, ...attrs } = Astro.props
14 |
15 | const currentPage = fetchPage(Astro.url.pathname)
16 | const entries = findNavigationEntries()
17 | ---
18 |
19 | {currentPage && }
20 |
21 |
--------------------------------------------------------------------------------
/src/NavigationList.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Props as NavigationProps } from './Navigation.astro'
3 | import NavigationItem from './NavigationItem.astro'
4 | import type { Entry } from './utils.js'
5 |
6 | export interface Props extends Omit {
7 | entries: Entry[]
8 | disableCurrent?: boolean
9 | }
10 |
11 | const { as: Component = 'ol', entries, showExcerpts = false, disableCurrent = false, itemAttrs, ...attrs } = Astro.props
12 | ---
13 |
14 |
15 | {
16 | entries.map((entry) => (
17 |
24 | ))
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/src/Breadcrumbs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { BreadcrumbList, ListItem } from 'schema-dts'
3 | import type { Props as NavProps } from './NavigationList.astro'
4 | import { findBreadcrumbEntries } from './utils.js'
5 | import NavigationList from './NavigationList.astro'
6 | import Schema from './Schema.astro'
7 |
8 | export interface Props extends Omit {}
9 |
10 | const attrs = Astro.props
11 |
12 | const entries = findBreadcrumbEntries(Astro.url.pathname)
13 |
14 | const breadcrumbs: BreadcrumbList = {
15 | '@type': 'BreadcrumbList',
16 | itemListElement: entries.map((entry, i) => {
17 | let item: ListItem = {
18 | '@type': 'ListItem',
19 | position: i + 1,
20 | item: {
21 | '@id': new URL(entry.url, Astro.site).toString(),
22 | name: entry.title,
23 | },
24 | }
25 |
26 | return item
27 | }),
28 | }
29 | ---
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/NavigationItem.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes, HTMLTag } from 'astro/types'
3 | import NavigationList from './NavigationList.astro'
4 | import type { Props as NavProps } from './NavigationList.astro'
5 | import type { Entry } from './utils.js'
6 |
7 | export interface Props extends Entry, Omit, 'title' | 'children'> {
8 | as?: HTMLTag
9 | showExcerpt?: boolean
10 | disableCurrent?: boolean
11 | listAttrs: Omit
12 | }
13 |
14 | const {
15 | as: Component = 'li',
16 | title,
17 | url,
18 | children = [],
19 | showExcerpt,
20 | disableCurrent = false,
21 | listAttrs,
22 | ...attrs
23 | } = Astro.props
24 |
25 | const current = url === Astro.url.pathname
26 | ---
27 |
28 |
29 | {
30 | current && disableCurrent ? (
31 | {title}
32 | ) : (
33 |
34 | {title}
35 |
36 | )
37 | }
38 | {children.length > 0 && }
39 |
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-navigation",
3 | "description": "A plugin for creating hierarchical navigation in Astro projects. Supports breadcrumbs too!",
4 | "version": "0.4.0",
5 | "type": "module",
6 | "exports": {
7 | ".": "./index.ts"
8 | },
9 | "author": "tony-sull (https://twitter.com/tonysull_co)",
10 | "files": [
11 | "src",
12 | "index.ts"
13 | ],
14 | "keywords": [
15 | "astro-component",
16 | "withastro",
17 | "navigation"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/tony-sull/astro-navigation.git"
22 | },
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/tony-sull/astro-navigation/issues"
26 | },
27 | "homepage": "https://github.com/tony-sull/astro-navigation#readme",
28 | "scripts": {
29 | "lint": "prettier -w --plugin-search-dir=. ."
30 | },
31 | "dependencies": {
32 | "dependency-graph": "^0.11.0",
33 | "schema-dts": "^1.1.0"
34 | },
35 | "devDependencies": {
36 | "@changesets/cli": "^2.25.2",
37 | "astro": "^1.6.2",
38 | "prettier": "^2.7.1",
39 | "prettier-plugin-astro": "^0.7.0",
40 | "typescript": "^4.8.4"
41 | },
42 | "peerDependencies": {
43 | "astro": "^1.6.2"
44 | },
45 | "packageManager": "pnpm@7.9.5"
46 | }
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # astro-navigation
2 |
3 | ## 0.4.0
4 |
5 | ### Minor Changes
6 |
7 | - 384f316: Upgrades to the latest version of Astro and uses the new astro/types interfaces
8 |
9 | ### Patch Changes
10 |
11 | - fbd6bb8: Fixing formatting error of readme file in npm
12 |
13 | ## 0.3.0
14 |
15 | ### Minor Changes
16 |
17 | - a4f11af: feat: adds support for .astro pages :rocket:
18 |
19 | ## 0.2.1
20 |
21 | ### Patch Changes
22 |
23 | - f5192e2: fix: fixes dependency graph
24 |
25 | ## 0.2.0
26 |
27 | ### Minor Changes
28 |
29 | - 3b7ca3d: fixes a mismatch between "name" and "title" frontmatter naming.
30 |
31 | ### Patch Changes
32 |
33 | - ee4fd2b: chore: linter fixes
34 | - 51c0395: `@type` is now defaulted to "WebPage" when not provided in a page's frontmatter
35 | - d44cff3: Fixes a link in the package.json metadata
36 | - 881aef2: fix: navigation.permalink frontmatter was being ignored
37 |
38 | ## 0.1.2
39 |
40 | ### Patch Changes
41 |
42 | - 0f8d703: Updates package metadata for NPM deployments
43 |
44 | ## 0.1.1
45 |
46 | ### Patch Changes
47 |
48 | - 1e98d8a: Always include the WebPage ld+json schema in
49 |
50 | ## 0.1.0
51 |
52 | ### Minor Changes
53 |
54 | - 21ec11c: Updates the components to handle globbing `/src/content/pages` internally, no more passing your own `Astro.glob()` results!
55 | - e1076b9: Adds a new `` component for next/previous links
56 |
--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------
1 | import type { Graph, Thing, WithContext } from 'schema-dts'
2 |
3 | type JsonValueScalar = string | boolean | number
4 | type JsonValue = JsonValueScalar | Array | { [key: string]: JsonValue }
5 | type JsonReplacer = (_: string, value: JsonValue) => JsonValue | undefined
6 |
7 | const ESCAPE_ENTITIES = Object.freeze({
8 | '&': '&',
9 | '<': '<',
10 | '>': '>',
11 | '"': '"',
12 | "'": ''',
13 | })
14 | const ESCAPE_REGEX = new RegExp(`[${Object.keys(ESCAPE_ENTITIES).join('')}]`, 'g')
15 | const ESCAPE_REPLACER = (t: string): string => ESCAPE_ENTITIES[t as keyof typeof ESCAPE_ENTITIES]
16 |
17 | /**
18 | * A replacer for JSON.stringify to strip JSON-LD of illegal HTML entities
19 | * per https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements
20 | */
21 | const safeJsonLdReplacer: JsonReplacer = (() => {
22 | // Replace per https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements
23 | // Solution from https://stackoverflow.com/a/5499821/864313
24 | return (_: string, value: JsonValue): JsonValue | undefined => {
25 | switch (typeof value) {
26 | case 'object':
27 | // Omit null values.
28 | if (value === null) {
29 | return undefined
30 | }
31 |
32 | return value // JSON.stringify will recursively call replacer.
33 | case 'number':
34 | case 'boolean':
35 | case 'bigint':
36 | return value // These values are not risky.
37 | case 'string':
38 | return value.replace(ESCAPE_REGEX, ESCAPE_REPLACER)
39 | default: {
40 | // We shouldn't expect other types.
41 | isNever(value)
42 |
43 | // JSON.stringify will remove this element.
44 | return undefined
45 | }
46 | }
47 | }
48 | })()
49 |
50 | // Utility: Assert never
51 | function isNever(_: never): void {}
52 |
53 | function withContext(thing: T): WithContext {
54 | return {
55 | '@context': 'https://schema.org',
56 | ...(thing as Object),
57 | } as WithContext
58 | }
59 |
60 | function asGraph(things: Thing[]): Graph {
61 | return {
62 | '@context': 'https://schema.org',
63 | '@graph': things,
64 | }
65 | }
66 |
67 | export function ldToString(json: Thing | Thing[], space?: number | string) {
68 | const ld = Array.isArray(json) ? asGraph(json) : withContext(json)
69 |
70 | return JSON.stringify(ld, safeJsonLdReplacer, space)
71 | }
72 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | pull_request:
8 | paths-ignore:
9 | - '.vscode/**'
10 |
11 | # Automatically cancel in-progress actions on the same branch
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
14 | cancel-in-progress: true
15 |
16 | defaults:
17 | run:
18 | shell: bash
19 |
20 | jobs:
21 | # Lint can run in parallel with Build.
22 | # We also run `yarn install` with the `--prefer-offline` flag to speed things up.
23 | # Lint can run in parallel with Build.
24 | # We also run `yarn install` with the `--prefer-offline` flag to speed things up.
25 | lint:
26 | name: Lint
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Check out repository
30 | uses: actions/checkout@v3
31 |
32 | - name: Setup PNPM
33 | uses: pnpm/action-setup@v2.2.1
34 |
35 | - name: Setup Node
36 | uses: actions/setup-node@v3
37 | with:
38 | node-version: 16
39 | cache: 'pnpm'
40 |
41 | - name: Install dependencies
42 | run: pnpm install
43 |
44 | - name: Status
45 | run: git status
46 |
47 | # Lint autofix cannot run on forks, so just skip those! See https://github.com/wearerequired/lint-action/issues/13
48 | - name: Lint (External)
49 | if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login != github.repository_owner }}
50 | run: pnpm lint
51 |
52 | # Otherwise, run lint autofixer
53 | - name: Lint
54 | if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.repository_owner }}
55 | uses: wearerequired/lint-action@v1.10.0
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | with:
59 | prettier: true
60 | auto_fix: true
61 | git_name: github-actions[bot]
62 | git_email: github-actions[bot]@users.noreply.github.com
63 | commit_message: 'chore(lint): ${linter} fix'
64 | github_token: ${{ secrets.GITHUB_TOKEN }}
65 | neutral_check_on_warning: true
66 |
67 | # Changelog can only run _after_ Build and Test.
68 | # We download all `dist/` artifacts from GitHub to skip the build process.
69 | changelog:
70 | name: Changelog PR or Release
71 | if: ${{ github.ref_name == 'main' }}
72 | needs: [lint]
73 | runs-on: ubuntu-latest
74 | steps:
75 | - uses: actions/checkout@v3
76 |
77 | - name: Setup PNPM
78 | uses: pnpm/action-setup@v2.2.1
79 |
80 | - name: Setup Node
81 | uses: actions/setup-node@v3
82 | with:
83 | node-version: 16
84 | cache: 'pnpm'
85 | env:
86 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
87 |
88 | - name: Install dependencies
89 | run: pnpm install
90 |
91 | - name: Create Release Pull Request or Publish
92 | id: changesets
93 | uses: changesets/action@v1
94 | with:
95 | publish: pnpm exec changeset publish
96 | commit: '[ci] release'
97 | title: '[ci] release'
98 | env:
99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
100 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # astro-navigation
2 |
3 | A plugin for creating hierarchical navigation in Astro projects. Supports breadcrumbs and next/previous pagination too!
4 |
5 | > Full docs coming soon!
6 |
7 | ## Basic usage
8 |
9 | This packages adds three components useful for building hierarchical navigation in [Astro](https://astro.build). Just write your Markdown and MDX pages in `src/content/pages` and you're all set!
10 |
11 | `` builds a sorted hierarchical navigation menu, `` adds an SEO-friendly breadcrumb list for the current page, and `` adds next/previous navigation links.
12 |
13 | ## `` component
14 |
15 | This is the main component, building the HTML for a sorted navigation menu. Include a bit of frontmatter on each `.md` or `.mdx` page and the component will handle sorting and nesting pages automatically.
16 |
17 | The `` component will build the list itself but leaves rendering a `