├── 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 `