| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -37,6 +37,23 @@ if (import.meta.server) { | |||
| 37 | 37 | }) | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | + /** Prose routes with a markdown twin. */ | ||
| 41 | + const PROSE = /^\/(?:$|fonts$|compare$|api$|about$|contact$|privacy$|docs)/ | ||
| 42 | + | ||
| 43 | + const canonical = computed(() => new URL(route.path, origin).href) | ||
| 44 | + | ||
| 45 | + if (import.meta.server) { | ||
| 46 | + useHead({ | ||
| 47 | + link: [{ rel: 'canonical', href: () => canonical.value }], | ||
| 48 | + }) | ||
| 49 | + | ||
| 50 | + // <https://acceptmarkdown.com>: the same document, negotiable through `Accept` or the suffix. | ||
| 51 | + if (PROSE.test(route.path)) { | ||
| 52 | + const markdown = route.path === '/' ? '/index.md' : `${route.path.replace(/\/$/, '')}.md` | ||
| 53 | + useHead({ link: [{ rel: 'alternate', type: 'text/markdown', href: new URL(markdown, origin).href }] }) | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + | ||
| 40 | 57 | const main = useTemplateRef<HTMLElement>('main') | |
| 41 | 58 | ||
| 42 | 59 | // Reset focus to the main landmark after a client-side navigation, which otherwise leaves the | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,8 +9,9 @@ const { data: credits } = await useFetch<ContributorsResponse>('/api/v1/contribu | |||
| 9 | 9 | }) | |
| 10 | 10 | ||
| 11 | 11 | const contributors = computed(() => credits.value?.contributors ?? []) | |
| 12 | - // The API sorts by commit count, so the first entry leads. | ||
| 13 | - const top = computed(() => contributors.value[0]!) | ||
| 12 | + | ||
| 13 | + /** Named above: danielroe, qwerzl, florian-lefebvre. Zero when GitHub is rate-limited. */ | ||
| 14 | + const others = computed(() => Math.max(0, contributors.value.length - 3)) | ||
| 14 | 15 | </script> | |
| 15 | 16 | ||
| 16 | 17 | <template> | |
@@ -24,47 +25,24 @@ const top = computed(() => contributors.value[0]!) | |||
| 24 | 25 | <NuxtLink to="/compare">compare</NuxtLink> · | |
| 25 | 26 | <NuxtLink to="/docs">docs</NuxtLink> · | |
| 26 | 27 | <NuxtLink to="/api">api</NuxtLink> · | |
| 28 | + <NuxtLink to="/about">about</NuxtLink> · | ||
| 29 | + <NuxtLink to="/contact">contact</NuxtLink> · | ||
| 30 | + <NuxtLink to="/privacy">privacy</NuxtLink> · | ||
| 27 | 31 | <a href="https://github.com/unjs/unifont">source</a> · | |
| 28 | - <a href="https://npmjs.com/package/unifont">npm</a> | ||
| 32 | + <a href="https://npmjs.com/package/unifont">npm</a> · | ||
| 33 | + <a href="/llms.txt">llms.txt</a> · | ||
| 34 | + <a href="/openapi.json">openapi.json</a> | ||
| 29 | 35 | </p> | |
| 30 | - <div | ||
| 31 | - v-if="contributors.length" | ||
| 32 | - class="credits" | ||
| 33 | - > | ||
| 34 | - <!-- A link per avatar is a link and a list item per person once the styles are gone, so the | ||
| 35 | - row is decorative and the sentence below carries the same information as text. --> | ||
| 36 | - <div | ||
| 37 | - class="credits__row" | ||
| 38 | - aria-hidden="true" | ||
| 39 | - > | ||
| 40 | - <img | ||
| 41 | - v-for="person in contributors" | ||
| 42 | - :key="person.login" | ||
| 43 | - class="credits__avatar" | ||
| 44 | - :src="person.avatar" | ||
| 45 | - alt="" | ||
| 46 | - width="28" | ||
| 47 | - height="28" | ||
| 48 | - loading="lazy" | ||
| 49 | - decoding="async" | ||
| 50 | - > | ||
| 51 | - </div> | ||
| 52 | - <p class="credits__summary"> | ||
| 53 | - {{ contributors.length }} | ||
| 54 | - {{ contributors.length === 1 ? 'person has' : 'people have' }} | ||
| 55 | - landed a commit, {{ top.login }} the most with {{ top.contributions }} | ||
| 56 | - {{ top.contributions === 1 ? 'commit' : 'commits' }}. | ||
| 57 | - <a href="https://github.com/unjs/unifont/graphs/contributors">all contributors</a> | ||
| 58 | - </p> | ||
| 59 | - </div> | ||
| 60 | 36 | ||
| 61 | 37 | <p class="colophon__line colophon__line--fine"> | |
| 62 | 38 | made with <span | |
| 63 | 39 | class="heart" | |
| 64 | 40 | role="img" | |
| 65 | 41 | aria-label="love" | |
| 66 | 42 | >♥</span> by | |
| 67 | - <a href="https://roe.dev">Daniel Roe</a> and contributors · | ||
| 43 | + <a href="https://roe.dev">danielroe</a>, <a href="https://github.com/qwerzl">qwerzl</a><template v-if="others">, </template><template v-else> and </template><a href="https://florian-lefebvre.dev/">florian-lefebvre</a><template v-if="others"> | ||
| 44 | + and <a href="https://github.com/unjs/unifont/graphs/contributors">{{ others }} other contributor{{ others === 1 ? '' : 's' }}</a> | ||
| 45 | + </template> · | ||
| 68 | 46 | MIT · font data belongs to its foundries and is licensed by them, not by us | |
| 69 | 47 | </p> | |
| 70 | 48 | </div> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,122 @@ | |||
| 1 | + <script setup lang="ts"> | ||
| 2 | + import type { MarkdownDocumentProps } from '@comark/vue' | ||
| 3 | + import { MarkdownDocument } from '@comark/vue' | ||
| 4 | + import { clientContent } from '~/composables/client-content' | ||
| 5 | + | ||
| 6 | + const props = defineProps<{ slug: string }>() | ||
| 7 | + | ||
| 8 | + const { data: page } = await useAsyncData(() => `page:${props.slug}`, () => clientContent.get(`/${props.slug}`)) | ||
| 9 | + | ||
| 10 | + if (!page.value) { | ||
| 11 | + throw createError({ statusCode: 404, statusMessage: 'No such page.' }) | ||
| 12 | + } | ||
| 13 | + | ||
| 14 | + const components = { | ||
| 15 | + pre: resolveComponent('ProsePre'), | ||
| 16 | + table: resolveComponent('ProseTable'), | ||
| 17 | + th: resolveComponent('ProseTh'), | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + const title = computed(() => (page.value?.data.title as string | undefined) ?? 'unifont') | ||
| 21 | + const description = computed(() => page.value?.data.description as string | undefined) | ||
| 22 | + | ||
| 23 | + // `ContentFile` carries the `nodes` the renderer wants, but the two types are declared in | ||
| 24 | + // different packages and do not structurally line up. | ||
| 25 | + const document = computed(() => page.value as MarkdownDocumentProps['value']) | ||
| 26 | + | ||
| 27 | + usePageSeo({ title: () => title.value, description: () => description.value }) | ||
| 28 | + </script> | ||
| 29 | + | ||
| 30 | + <template> | ||
| 31 | + <article class="page"> | ||
| 32 | + <header class="page__head"> | ||
| 33 | + <h1 class="page__title"> | ||
| 34 | + {{ title }} | ||
| 35 | + </h1> | ||
| 36 | + <p | ||
| 37 | + v-if="description" | ||
| 38 | + class="page__lede" | ||
| 39 | + > | ||
| 40 | + {{ description }} | ||
| 41 | + </p> | ||
| 42 | + </header> | ||
| 43 | + | ||
| 44 | + <div class="page__body"> | ||
| 45 | + <MarkdownDocument | ||
| 46 | + :value="document" | ||
| 47 | + :components="components" | ||
| 48 | + /> | ||
| 49 | + </div> | ||
| 50 | + </article> | ||
| 51 | + </template> | ||
| 52 | + | ||
| 53 | + <style scoped> | ||
| 54 | + .page { | ||
| 55 | + max-width: 46rem; | ||
| 56 | + margin-inline: auto; | ||
| 57 | + padding: var(--space-xl) var(--page-pad) var(--space-2xl); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + .page__head { | ||
| 61 | + padding-bottom: var(--space-lg); | ||
| 62 | + border-bottom: var(--rule-heavy) solid var(--color-ink); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + .page__title { | ||
| 66 | + font-size: var(--text-2xl); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + .page__lede { | ||
| 70 | + max-width: var(--measure); | ||
| 71 | + margin-top: var(--space-xs); | ||
| 72 | + color: var(--color-muted); | ||
| 73 | + font-size: var(--text-md); | ||
| 74 | + line-height: 1.5; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + .page__body { | ||
| 78 | + padding-top: var(--space-lg); | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + .page__body :deep(p), | ||
| 82 | + .page__body :deep(ul), | ||
| 83 | + .page__body :deep(ol) { | ||
| 84 | + margin-block: var(--space-md); | ||
| 85 | + color: var(--color-muted); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + .page__body :deep(h2) { | ||
| 89 | + margin-block: var(--space-2xl) var(--space-sm); | ||
| 90 | + padding-top: var(--space-sm); | ||
| 91 | + border-top: var(--rule-hair) solid var(--color-rule); | ||
| 92 | + font-size: var(--text-lg); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + .page__body :deep(h3) { | ||
| 96 | + margin-block: var(--space-xl) var(--space-xs); | ||
| 97 | + font-size: var(--text-md); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + .page__body :deep(ul), | ||
| 101 | + .page__body :deep(ol) { | ||
| 102 | + padding-left: var(--space-lg); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + .page__body :deep(li) { | ||
| 106 | + margin-block: var(--space-2xs); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + .page__body :deep(li::marker) { | ||
| 110 | + color: var(--color-rule-strong); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + .page__body :deep(a) { | ||
| 114 | + text-decoration-color: var(--color-muted); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + .page__body :deep(code) { | ||
| 118 | + padding: 0.1em 0.25em; | ||
| 119 | + background: var(--color-paper-3); | ||
| 120 | + border-radius: var(--radius-xs); | ||
| 121 | + } | ||
| 122 | + </style> | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + <template> | ||
| 2 | + <SitePage slug="about" /> | ||
| 3 | + </template> | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -135,6 +135,11 @@ const anchor = (path: string) => path.replace(/[^a-z0-9]+/gi, '-').replace(/^-|- | |||
| 135 | 135 | plugins or CI checks, anywhere you want font metadata without shipping a resolver. There's an | |
| 136 | 136 | <a href="https://modelcontextprotocol.io">MCP</a> server too, if the thing asking is a model. | |
| 137 | 137 | </p> | |
| 138 | + <p class="head__machine"> | ||
| 139 | + Machine-readable: <a href="/openapi.json">openapi.json</a> describes every endpoint below, | ||
| 140 | + <a href="/llms.txt">llms.txt</a> indexes the site for agents, and every prose page here answers in | ||
| 141 | + markdown if you ask for <code>text/markdown</code> or append <code>.md</code> to its path. | ||
| 142 | + </p> | ||
| 138 | 143 | <p class="head__warn"> | |
| 139 | 144 | Not for production use. This API is best-effort, we'll rate-limit it if we have to, and it can | |
| 140 | 145 | change or go away, the same as <code>proxy.unifont.dev</code>. Build it into a script, an editor | |
@@ -283,6 +288,13 @@ const anchor = (path: string) => path.replace(/[^a-z0-9]+/gi, '-').replace(/^-|- | |||
| 283 | 288 | color: var(--color-muted); | |
| 284 | 289 | } | |
| 285 | 290 | ||
| 291 | + .head__machine { | ||
| 292 | + max-width: var(--measure); | ||
| 293 | + margin-top: var(--space-sm); | ||
| 294 | + color: var(--color-muted); | ||
| 295 | + font-size: var(--text-sm); | ||
| 296 | + } | ||
| 297 | + | ||
| 286 | 298 | .head__warn { | |
| 287 | 299 | max-width: var(--measure); | |
| 288 | 300 | margin-top: var(--space-md); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + <template> | ||
| 2 | + <SitePage slug="contact" /> | ||
| 3 | + </template> | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,7 +8,7 @@ const route = useRoute() | |||
| 8 | 8 | const path = computed(() => { | |
| 9 | 9 | const slug = route.params.slug | |
| 10 | 10 | const parts = Array.isArray(slug) ? slug : slug ? [slug] : [] | |
| 11 | - return parts.length ? `/${parts.join('/')}` : '/' | ||
| 11 | + return parts.length ? `/docs/${parts.join('/')}` : '/docs' | ||
| 12 | 12 | }) | |
| 13 | 13 | ||
| 14 | 14 | const { data: page } = await useAsyncData(() => `doc:${path.value}`, () => clientContent.get(path.value), { | |
@@ -23,21 +23,25 @@ const { data: nav } = await useAsyncData('doc-nav', () => clientContent.navigati | |||
| 23 | 23 | ||
| 24 | 24 | interface NavItem { title?: string, path: string, children?: NavItem[] } | |
| 25 | 25 | ||
| 26 | + /** | ||
| 27 | + * The `/docs` subtree of the navigation, flattened into reading order. A directory and its own | ||
| 28 | + * index are separate nodes with the same path, so the first of each wins. | ||
| 29 | + */ | ||
| 26 | 30 | const chapters = computed<NavItem[]>(() => { | |
| 27 | 31 | const items = (nav.value ?? []) as NavItem[] | |
| 28 | - const flat: NavItem[] = [] | ||
| 32 | + const flat = new Map<string, NavItem>() | ||
| 29 | 33 | const walk = (list: NavItem[]) => { | |
| 30 | 34 | for (const item of list) { | |
| 31 | - if (item.path) { | ||
| 32 | - flat.push(item) | ||
| 35 | + if ((item.path === '/docs' || item.path?.startsWith('/docs/')) && !flat.has(item.path)) { | ||
| 36 | + flat.set(item.path, item) | ||
| 33 | 37 | } | |
| 34 | 38 | if (item.children?.length) { | |
| 35 | 39 | walk(item.children) | |
| 36 | 40 | } | |
| 37 | 41 | } | |
| 38 | 42 | } | |
| 39 | 43 | walk(items) | |
| 40 | - return flat | ||
| 44 | + return [...flat.values()] | ||
| 41 | 45 | }) | |
| 42 | 46 | ||
| 43 | 47 | const position = computed(() => chapters.value.findIndex(item => item.path === path.value)) | |
@@ -64,8 +68,6 @@ usePageSeo({ | |||
| 64 | 68 | title: () => title.value, | |
| 65 | 69 | description: () => (page.value?.data.description as string | undefined) ?? undefined, | |
| 66 | 70 | }) | |
| 67 | - | ||
| 68 | - const href = (docPath: string) => (docPath === '/' ? '/docs' : `/docs${docPath}`) | ||
| 69 | 71 | </script> | |
| 70 | 72 | ||
| 71 | 73 | <template> | |
@@ -86,7 +88,7 @@ const href = (docPath: string) => (docPath === '/' ? '/docs' : `/docs${docPath}` | |||
| 86 | 88 | class="rail__link" | |
| 87 | 89 | :class="{ 'rail__link--current': item.path === path }" | |
| 88 | 90 | :aria-current="item.path === path ? 'page' : undefined" | |
| 89 | - :to="href(item.path)" | ||
| 91 | + :to="item.path" | ||
| 90 | 92 | >{{ item.title }}</NuxtLink> | |
| 91 | 93 | </li> | |
| 92 | 94 | </ol> | |
@@ -119,15 +121,15 @@ const href = (docPath: string) => (docPath === '/' ? '/docs' : `/docs${docPath}` | |||
| 119 | 121 | <NuxtLink | |
| 120 | 122 | v-if="previous" | |
| 121 | 123 | class="pager__link" | |
| 122 | - :to="href(previous.path)" | ||
| 124 | + :to="previous.path" | ||
| 123 | 125 | > | |
| 124 | 126 | <span class="pager__dir">previous</span> | |
| 125 | 127 | <span class="pager__name">{{ previous.title }}</span> | |
| 126 | 128 | </NuxtLink> | |
| 127 | 129 | <NuxtLink | |
| 128 | 130 | v-if="next" | |
| 129 | 131 | class="pager__link pager__link--next" | |
| 130 | - :to="href(next.path)" | ||
| 132 | + :to="next.path" | ||
| 131 | 133 | > | |
| 132 | 134 | <span class="pager__dir">next</span> | |
| 133 | 135 | <span class="pager__name">{{ next.title }}</span> | |
| Back | FazBrowse Home | New Git URL |
0 commit comments