upcontent 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +61 -0
- package/.github/workflows/deploy.yml +59 -0
- package/.github/workflows/publish.yml +75 -0
- package/.github/workflows/reusable-pages.yml +74 -0
- package/.upcontent/config.json +85 -0
- package/.upcontent/favicon.svg +4 -0
- package/.upcontent/logo.svg +6 -0
- package/.upcontent/portal.css +139 -0
- package/CONTEXT.md +25 -0
- package/Makefile +33 -0
- package/README.md +132 -2
- package/SHOWCASE.mdx +163 -0
- package/assets/readme/portal-home.png +0 -0
- package/assets/readme/portal-showcase.png +0 -0
- package/astro.config.mjs +167 -0
- package/customization/config-json.md +100 -0
- package/customization/content.md +62 -0
- package/customization/environment.md +43 -0
- package/customization/index.md +33 -0
- package/customization/navigation.md +57 -0
- package/customization/site-identity.md +43 -0
- package/customization/theme.md +47 -0
- package/deployment/github-pages.md +46 -0
- package/deployment/index.md +25 -0
- package/deployment/npm.md +39 -0
- package/deployment/static-hosts.md +34 -0
- package/getting-started/consumer-repository.md +156 -0
- package/getting-started/first-build.md +78 -0
- package/guides/authoring-content.md +105 -0
- package/guides/validate-your-site.md +58 -0
- package/package.json +40 -4
- package/scripts/upcontent-cli.mjs +111 -0
- package/scripts/verify-external-build.mjs +32 -0
- package/scripts/verify-golden-build.mjs +9 -0
- package/src/components/MermaidLoader.astro +304 -0
- package/src/components/PaletteShowcase.astro +122 -0
- package/src/components/StructuredDataCopy.astro +22 -0
- package/src/content/__mocks__/astro-content.ts +7 -0
- package/src/content/__mocks__/astro-loaders.ts +3 -0
- package/src/content/i18n/en.json +1 -0
- package/src/content.config.test.ts +209 -0
- package/src/content.config.ts +113 -0
- package/src/lib/content-blocklist.test.ts +47 -0
- package/src/lib/content-blocklist.ts +55 -0
- package/src/lib/doc-links.test.ts +62 -0
- package/src/lib/doc-links.ts +31 -0
- package/src/lib/mermaid-render.test.ts +42 -0
- package/src/lib/mermaid-render.ts +24 -0
- package/src/lib/portal-config.test.ts +153 -0
- package/src/lib/portal-config.ts +187 -0
- package/src/lib/portal-routes.test.ts +62 -0
- package/src/lib/portal-routes.ts +59 -0
- package/src/lib/product-identity.ts +2 -0
- package/src/lib/rehype-callouts.test.ts +69 -0
- package/src/lib/rehype-callouts.ts +61 -0
- package/src/lib/remark-doc-links.test.ts +50 -0
- package/src/lib/remark-doc-links.ts +21 -0
- package/src/lib/remark-strip-duplicate-title.test.ts +73 -0
- package/src/lib/remark-strip-duplicate-title.ts +37 -0
- package/src/lib/remark-structured-data-preview.test.ts +104 -0
- package/src/lib/remark-structured-data-preview.ts +66 -0
- package/src/lib/remark-wiki-links.test.ts +102 -0
- package/src/lib/remark-wiki-links.ts +112 -0
- package/src/lib/seo-sitemap.test.ts +37 -0
- package/src/lib/seo-sitemap.ts +54 -0
- package/src/lib/sidebar.test.ts +142 -0
- package/src/lib/sidebar.ts +114 -0
- package/src/lib/structured-data-tree.test.ts +75 -0
- package/src/lib/structured-data-tree.ts +55 -0
- package/src/overrides/Footer.astro +114 -0
- package/src/overrides/Head.astro +103 -0
- package/src/pages/robots.txt.ts +46 -0
- package/src/styles/callouts.css +29 -0
- package/src/styles/structured-data-preview.css +95 -0
- package/src/upcontent-cli.test.ts +36 -0
- package/test-fixtures/external-consumer/.upcontent/config.json +30 -0
- package/test-fixtures/external-consumer/.upcontent/favicon.svg +4 -0
- package/test-fixtures/external-consumer/.upcontent/logo.svg +4 -0
- package/test-fixtures/external-consumer/.upcontent/theme.css +4 -0
- package/test-fixtures/external-consumer/.upcontent-renderer/README.md +3 -0
- package/test-fixtures/external-consumer/README.md +6 -0
- package/test-fixtures/external-consumer/forbidden.md +5 -0
- package/test-fixtures/external-consumer/noindex.md +7 -0
- package/test-fixtures/external-consumer/public.md +6 -0
- package/tsconfig.json +7 -0
- package/vitest.config.ts +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,133 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
heading: Upcontent
|
|
3
|
+
description: Turn a documentation repository into a fast, searchable, customizable static portal.
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
<div align="center">
|
|
7
|
+
|
|
8
|
+
# Upcontent
|
|
9
|
+
|
|
10
|
+
**Turn the documentation repository you already have into a fast, searchable, customizable portal.**
|
|
11
|
+
|
|
12
|
+
[](https://github.com/lumamontes/upcontent/actions/workflows/ci.yml)
|
|
13
|
+
[](https://www.npmjs.com/package/upcontent)
|
|
14
|
+
|
|
15
|
+
<a href="https://lumamontes.github.io/upcontent/">See the live showcase</a> · <a href="#get-started">Get started in under a minute</a>
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## See the result
|
|
20
|
+
|
|
21
|
+
This is the real Upcontent portal generated from this repository and deployed to GitHub Pages.
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://lumamontes.github.io/upcontent/"><img src="assets/readme/portal-home.png" alt="Upcontent documentation portal home page" width="900"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
The same build includes a capability showcase for technical content, structured data, callouts, diagrams, navigation, and search.
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="https://lumamontes.github.io/upcontent/showcase/"><img src="assets/readme/portal-showcase.png" alt="Upcontent capability showcase page" width="900"></a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
## From repository to portal
|
|
34
|
+
|
|
35
|
+
Upcontent keeps your source of truth in Git and adds the publishing layer: clear navigation, full-text search, technical content rendering, consumer-owned branding, and a repeatable static build.
|
|
36
|
+
|
|
37
|
+
If your Markdown already lives in a GitHub repository, the flow is:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pnpm dlx upcontent init
|
|
41
|
+
pnpm dlx upcontent dev
|
|
42
|
+
pnpm dlx upcontent check
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The first command creates `.upcontent/` and a GitHub Pages workflow. The second starts a local portal using the repository you are already in. The third builds the consumer portal and validates the generated site. You do not need to clone the Upcontent renderer into your documentation repository.
|
|
46
|
+
|
|
47
|
+
Push the generated workflow and your documentation is published as a static site. The source repository remains the source of truth; Upcontent does not require a database, a companion server, or a new authoring system.
|
|
48
|
+
|
|
49
|
+
## What you get
|
|
50
|
+
|
|
51
|
+
- A responsive [Starlight](https://starlight.astro.build/) portal with navigation, search, themes, and table of contents.
|
|
52
|
+
- A `.upcontent/config.json` file for identity, navigation, rendering, and content boundaries.
|
|
53
|
+
- Consumer-owned logos, favicon, CSS, site identity, and navigation.
|
|
54
|
+
- Rendered callouts, Mermaid diagrams, JSON, YAML, CSV, and wiki links.
|
|
55
|
+
- A static `dist/` directory deployable to GitHub Pages or any static host.
|
|
56
|
+
- Build checks that catch broken wiki links and excluded content before publication.
|
|
57
|
+
|
|
58
|
+
## Is it a good fit?
|
|
59
|
+
|
|
60
|
+
Upcontent is a good fit when:
|
|
61
|
+
|
|
62
|
+
- Your documentation already lives in Markdown or MDX.
|
|
63
|
+
- Your team wants to keep writing in Git.
|
|
64
|
+
- You want the portal to be owned and deployed by the documentation repository.
|
|
65
|
+
- You need more than raw Markdown, but do not need a dynamic application.
|
|
66
|
+
- You want branding and navigation without maintaining a custom docs frontend.
|
|
67
|
+
|
|
68
|
+
It is not the right fit when your site needs runtime authentication, server-rendered personalization, or review comments inside the published portal.
|
|
69
|
+
|
|
70
|
+
## Explore this repository locally
|
|
71
|
+
|
|
72
|
+
Clone this repository, then start the included golden consumer:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
git clone https://github.com/lumamontes/upcontent.git
|
|
76
|
+
cd upcontent
|
|
77
|
+
pnpm install
|
|
78
|
+
make dev CONTENT_PATH=.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then open the local URL printed by Astro. The [capability showcase](showcase/) is the fastest way to see the complete rendering and customization surface.
|
|
82
|
+
|
|
83
|
+
## Configure your portal
|
|
84
|
+
|
|
85
|
+
The consumer configuration is deliberately small:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"site": {
|
|
90
|
+
"title": "Engineering Docs",
|
|
91
|
+
"description": "Documentation for the engineering team.",
|
|
92
|
+
"socialImage": "https://docs.example.com/social-card.png",
|
|
93
|
+
"locale": "en-US",
|
|
94
|
+
"logo": {
|
|
95
|
+
"src": ".upcontent/logo.svg",
|
|
96
|
+
"alt": "Engineering Docs"
|
|
97
|
+
},
|
|
98
|
+
"favicon": ".upcontent/favicon.svg"
|
|
99
|
+
},
|
|
100
|
+
"seo": {
|
|
101
|
+
"enabled": true
|
|
102
|
+
},
|
|
103
|
+
"repo": {
|
|
104
|
+
"url": "https://github.com/acme/engineering-docs"
|
|
105
|
+
},
|
|
106
|
+
"theme": {
|
|
107
|
+
"customCss": [".upcontent/theme.css"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Follow [Set up a consumer repository](getting-started/consumer-repository/) for the complete setup, then use [Customization](customization/) to shape the portal.
|
|
113
|
+
|
|
114
|
+
## Build for production
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
make build CONTENT_PATH=/path/to/your-consumer-repo
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The generated site is written to `dist/` and includes the Pagefind search index. Before publishing, run:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
pnpm test
|
|
124
|
+
pnpm check
|
|
125
|
+
make build CONTENT_PATH=.
|
|
126
|
+
make check-external
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Read the [deployment guide](deployment/) for GitHub Pages and other static hosts.
|
|
130
|
+
|
|
131
|
+
## Built on Astro and Starlight
|
|
132
|
+
|
|
133
|
+
Upcontent uses the official [Astro](https://astro.build/) framework and [Starlight](https://starlight.astro.build/) documentation theme as its rendering foundation. Upcontent adds the consumer-repository contract, content integrity checks, configuration surface, and deployment workflow around them.
|
package/SHOWCASE.mdx
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
heading: Capability showcase
|
|
3
|
+
description: One practical page showing what a consumer repository can render, configure, and publish with Upcontent.
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
import PaletteShowcase from './src/components/PaletteShowcase.astro';
|
|
9
|
+
|
|
10
|
+
Upcontent turns Markdown into a searchable static documentation site. Configure the consumer repository, add the content formats your team needs, and publish the generated `dist/` directory to a static host.
|
|
11
|
+
|
|
12
|
+
## Rendered content
|
|
13
|
+
|
|
14
|
+
The portal accepts ordinary Markdown and adds a small set of useful content features.
|
|
15
|
+
|
|
16
|
+
### Callouts
|
|
17
|
+
|
|
18
|
+
:::tip[Use callouts for decisions]
|
|
19
|
+
Keep callouts short. Use them for a warning, a recommendation, or a next action that should not disappear in the surrounding text.
|
|
20
|
+
:::
|
|
21
|
+
|
|
22
|
+
:::caution
|
|
23
|
+
Static output is public wherever the selected host is public. A private source repository does not automatically make the published site private.
|
|
24
|
+
:::
|
|
25
|
+
|
|
26
|
+
### Code and diagrams
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export function buildPortal(contentPath: string) {
|
|
30
|
+
return `make build CONTENT_PATH=${contentPath}`
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart LR
|
|
36
|
+
Content[Consumer repository] --> Config[.upcontent/config.json]
|
|
37
|
+
Config --> Build[Static build]
|
|
38
|
+
Content --> Build
|
|
39
|
+
Build --> Output[Searchable portal]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Mermaid diagrams support theme synchronization, fullscreen, zoom, pan, source copying, and an invalid-source fallback.
|
|
43
|
+
|
|
44
|
+
### Structured examples
|
|
45
|
+
|
|
46
|
+
JSON and YAML blocks remain readable as collapsible data:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"portal": "static",
|
|
51
|
+
"search": "pagefind",
|
|
52
|
+
"themes": ["light", "dark"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
contentPath: ./docs
|
|
58
|
+
publish: github-pages
|
|
59
|
+
sourceOfTruth: consumer-repository
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
CSV becomes a table when the content is tabular:
|
|
63
|
+
|
|
64
|
+
```csv
|
|
65
|
+
capability,status
|
|
66
|
+
custom-css,available
|
|
67
|
+
blocklist,available
|
|
68
|
+
pagefind,available
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Links and navigation
|
|
72
|
+
|
|
73
|
+
Regular Markdown links are best for stable routes, such as [site validation](../guides/validate-your-site/). Wiki links are useful when the source repository refers to files by name, such as `[[README]]`. Missing wiki-link targets fail the build instead of producing broken navigation.
|
|
74
|
+
|
|
75
|
+
## Consumer customization
|
|
76
|
+
|
|
77
|
+
The consumer changes identity and presentation in `.upcontent/`, without editing the portal renderer:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
.upcontent/
|
|
81
|
+
├── config.json
|
|
82
|
+
├── favicon.svg
|
|
83
|
+
├── logo.svg
|
|
84
|
+
└── theme.css
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The configuration controls:
|
|
88
|
+
|
|
89
|
+
- Site title, description, URL, logo, and favicon
|
|
90
|
+
- Repository links and local custom CSS
|
|
91
|
+
- Social links, table of contents, pagination, last-updated metadata, and code styling
|
|
92
|
+
- Sidebar roots, labels, and content blocklists
|
|
93
|
+
- The frontmatter field used as a page title
|
|
94
|
+
|
|
95
|
+
Example identity configuration:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"site": {
|
|
100
|
+
"title": "Engineering Docs",
|
|
101
|
+
"description": "Documentation for the engineering team.",
|
|
102
|
+
"logo": {
|
|
103
|
+
"src": ".upcontent/logo.svg",
|
|
104
|
+
"alt": "Engineering Docs"
|
|
105
|
+
},
|
|
106
|
+
"favicon": ".upcontent/favicon.svg"
|
|
107
|
+
},
|
|
108
|
+
"repo": {
|
|
109
|
+
"url": "https://github.com/acme/engineering-docs"
|
|
110
|
+
},
|
|
111
|
+
"theme": {
|
|
112
|
+
"customCss": [".upcontent/theme.css"]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The [configuration reference](../customization/config-json/) explains every supported field and its fallback behavior.
|
|
118
|
+
|
|
119
|
+
### Palette variations
|
|
120
|
+
|
|
121
|
+
The same Starlight surface can carry different consumer identities through tokens and custom CSS:
|
|
122
|
+
|
|
123
|
+
<PaletteShowcase />
|
|
124
|
+
|
|
125
|
+
These are examples for a consumer repository, not a runtime theme picker that every published portal needs to expose.
|
|
126
|
+
|
|
127
|
+
## Content boundaries
|
|
128
|
+
|
|
129
|
+
The build excludes protected internal paths and consumer blocklists before parsing. This keeps excluded files out of both the content collection and the sidebar.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"navigation": {
|
|
134
|
+
"blocklist": {
|
|
135
|
+
"exact": ["notes.md"],
|
|
136
|
+
"prefixes": ["drafts/", "internal/"]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Broken wiki links and traversal attempts fail with the source file and invalid reference. The same pipeline also works when `CONTENT_PATH` points to a separate consumer repository.
|
|
143
|
+
|
|
144
|
+
## Validation and publishing
|
|
145
|
+
|
|
146
|
+
The repeatable local contract is:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
pnpm test
|
|
150
|
+
pnpm check
|
|
151
|
+
make build CONTENT_PATH=.
|
|
152
|
+
make check-external
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The reference CI workflow validates the site and an external consumer fixture. The deployment workflow publishes `dist/` to a static host such as GitHub Pages.
|
|
156
|
+
|
|
157
|
+
## Explore the product
|
|
158
|
+
|
|
159
|
+
- [Your first build](../getting-started/first-build/)
|
|
160
|
+
- [Set up a consumer repository](../getting-started/consumer-repository/)
|
|
161
|
+
- [Authoring content](../guides/authoring-content/)
|
|
162
|
+
- [Validate your site](../guides/validate-your-site/)
|
|
163
|
+
- [Deployment](../deployment/)
|
|
Binary file
|
|
Binary file
|
package/astro.config.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs'
|
|
3
|
+
import { basename, resolve, sep } from 'node:path'
|
|
4
|
+
import { unified } from '@astrojs/markdown-remark'
|
|
5
|
+
import sitemap from '@astrojs/sitemap'
|
|
6
|
+
import starlight from '@astrojs/starlight'
|
|
7
|
+
import { defineConfig } from 'astro/config'
|
|
8
|
+
import { visit } from 'unist-util-visit'
|
|
9
|
+
import { rehypeCallouts } from './src/lib/rehype-callouts.ts'
|
|
10
|
+
import { remarkStripDuplicateTitle } from './src/lib/remark-strip-duplicate-title.ts'
|
|
11
|
+
import { remarkStructuredDataPreview } from './src/lib/remark-structured-data-preview.ts'
|
|
12
|
+
import { remarkWikiLinks } from './src/lib/remark-wiki-links.ts'
|
|
13
|
+
import { remarkDocumentLinks } from './src/lib/remark-doc-links.ts'
|
|
14
|
+
import { getPortalConfig } from './src/lib/portal-config.ts'
|
|
15
|
+
import { buildSidebar } from './src/lib/sidebar.ts'
|
|
16
|
+
import { getNoindexRoutes } from './src/lib/seo-sitemap.ts'
|
|
17
|
+
import { PRODUCT_NAME, PRODUCT_TAGLINE } from './src/lib/product-identity.ts'
|
|
18
|
+
|
|
19
|
+
const docsRoot = fileURLToPath(new URL('./src/content/docs', import.meta.url))
|
|
20
|
+
const portalConfig = getPortalConfig()
|
|
21
|
+
|
|
22
|
+
function copyContentAssetDirectory(assetPath) {
|
|
23
|
+
const source = resolve(docsRoot, assetPath)
|
|
24
|
+
const target = resolve(process.cwd(), 'public', assetPath)
|
|
25
|
+
if (!existsSync(source) || !statSync(source).isDirectory()) {
|
|
26
|
+
rmSync(target, { force: true, recursive: true })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rmSync(target, { force: true, recursive: true })
|
|
31
|
+
mkdirSync(resolve(target, '..'), { recursive: true })
|
|
32
|
+
cpSync(source, target, { recursive: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
copyContentAssetDirectory('assets/readme')
|
|
36
|
+
|
|
37
|
+
function resolvePortalAsset(assetPath) {
|
|
38
|
+
if (!assetPath || assetPath.startsWith('http')) return assetPath
|
|
39
|
+
if (assetPath.startsWith('/')) return assetPath
|
|
40
|
+
|
|
41
|
+
const source = resolve(docsRoot, assetPath)
|
|
42
|
+
if (!source.startsWith(`${resolve(docsRoot)}${sep}`) || !existsSync(source) || !statSync(source).isFile()) {
|
|
43
|
+
console.warn(`[${PRODUCT_NAME}] Portal asset not found: ${source}`)
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const targetDir = resolve(process.cwd(), 'public/upcontent-assets')
|
|
48
|
+
mkdirSync(targetDir, { recursive: true })
|
|
49
|
+
const targetName = basename(source)
|
|
50
|
+
cpSync(source, resolve(targetDir, targetName))
|
|
51
|
+
return `/upcontent-assets/${targetName}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolvePortalLogo(logo) {
|
|
55
|
+
if (!logo) return undefined
|
|
56
|
+
if (logo.src.startsWith('http') || logo.src.startsWith('/')) return logo
|
|
57
|
+
const source = resolve(docsRoot, logo.src)
|
|
58
|
+
if (!source.startsWith(`${resolve(docsRoot)}${sep}`) || !existsSync(source) || !statSync(source).isFile()) {
|
|
59
|
+
console.warn(`[${PRODUCT_NAME}] Portal logo not found: ${source}`)
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
const targetDir = resolve(process.cwd(), 'src/assets')
|
|
63
|
+
mkdirSync(targetDir, { recursive: true })
|
|
64
|
+
const target = resolve(targetDir, 'consumer-logo.svg')
|
|
65
|
+
cpSync(source, target)
|
|
66
|
+
return { ...logo, src: './src/assets/consumer-logo.svg' }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const consumerCss = (portalConfig.theme?.customCss ?? [])
|
|
70
|
+
.map(cssPath => resolve(docsRoot, cssPath))
|
|
71
|
+
.filter(cssPath => {
|
|
72
|
+
if (existsSync(cssPath)) return true
|
|
73
|
+
console.warn(`[${PRODUCT_NAME}] Custom CSS file not found: ${cssPath}`)
|
|
74
|
+
return false
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const customCss = ['./src/styles/callouts.css', './src/styles/structured-data-preview.css', ...consumerCss]
|
|
78
|
+
const starlightOptions = portalConfig.starlight ?? {}
|
|
79
|
+
const noindexRoutes = getNoindexRoutes(docsRoot)
|
|
80
|
+
const configuredSiteUrl = process.env.SITE_URL || portalConfig.site?.url
|
|
81
|
+
let site
|
|
82
|
+
let base = process.env.BASE_PATH || undefined
|
|
83
|
+
if (portalConfig.seo?.enabled === true && configuredSiteUrl) {
|
|
84
|
+
try {
|
|
85
|
+
const parsedSiteUrl = new URL(configuredSiteUrl)
|
|
86
|
+
if (parsedSiteUrl.protocol !== 'http:' && parsedSiteUrl.protocol !== 'https:') throw new Error('unsupported protocol')
|
|
87
|
+
if (parsedSiteUrl.username || parsedSiteUrl.password) throw new Error('userinfo is not allowed')
|
|
88
|
+
site = parsedSiteUrl.origin
|
|
89
|
+
if (!process.env.BASE_PATH) base = parsedSiteUrl.pathname === '/' ? undefined : parsedSiteUrl.pathname
|
|
90
|
+
} catch {
|
|
91
|
+
console.warn(`[${PRODUCT_NAME}] SEO site URL must be an absolute URL: ${configuredSiteUrl}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function includeInSitemap(page) {
|
|
96
|
+
const pathname = new URL(page).pathname.replace(/\/+$/, '') || '/'
|
|
97
|
+
const basePath = (base || '').replace(/\/+$/, '')
|
|
98
|
+
const route = pathname === basePath
|
|
99
|
+
? '/'
|
|
100
|
+
: basePath && pathname.startsWith(`${basePath}/`)
|
|
101
|
+
? pathname.slice(basePath.length)
|
|
102
|
+
: pathname
|
|
103
|
+
return !noindexRoutes.has(route)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function serializeSitemapEntry(entry) {
|
|
107
|
+
if (!site) return entry
|
|
108
|
+
const homepageUrl = new URL(`${(base || '').replace(/\/+$/, '')}/`, site).href
|
|
109
|
+
if (entry.url === homepageUrl.replace(/\/$/, '') || entry.url === homepageUrl) {
|
|
110
|
+
return { ...entry, url: homepageUrl }
|
|
111
|
+
}
|
|
112
|
+
return entry
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remark plugin: converts ```mermaid blocks to <div class="mermaid"> BEFORE Shiki runs
|
|
116
|
+
function remarkMermaid() {
|
|
117
|
+
return (tree) => {
|
|
118
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
119
|
+
if (node.lang !== 'mermaid') return
|
|
120
|
+
// <br/> inside a div becomes a DOM element before Mermaid parses the text, breaking the parser
|
|
121
|
+
const safe = node.value.replace(/<br\s*\/?>/gi, ' ')
|
|
122
|
+
parent.children.splice(index, 1, {
|
|
123
|
+
type: 'html',
|
|
124
|
+
value: `<div class="mermaid">${safe}</div>`,
|
|
125
|
+
})
|
|
126
|
+
return index + 1
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default defineConfig({
|
|
132
|
+
output: 'static',
|
|
133
|
+
site,
|
|
134
|
+
base,
|
|
135
|
+
integrations: [
|
|
136
|
+
sitemap({ filter: includeInSitemap, serialize: serializeSitemapEntry }),
|
|
137
|
+
starlight({
|
|
138
|
+
title: portalConfig.site?.title ?? PRODUCT_NAME,
|
|
139
|
+
description: portalConfig.site?.description ?? PRODUCT_TAGLINE,
|
|
140
|
+
logo: resolvePortalLogo(portalConfig.site?.logo),
|
|
141
|
+
favicon: resolvePortalAsset(portalConfig.site?.favicon),
|
|
142
|
+
components: {
|
|
143
|
+
Head: './src/overrides/Head.astro',
|
|
144
|
+
Footer: './src/overrides/Footer.astro',
|
|
145
|
+
},
|
|
146
|
+
customCss,
|
|
147
|
+
social: starlightOptions.social,
|
|
148
|
+
tableOfContents: starlightOptions.tableOfContents,
|
|
149
|
+
lastUpdated: starlightOptions.lastUpdated,
|
|
150
|
+
pagination: starlightOptions.pagination,
|
|
151
|
+
expressiveCode: starlightOptions.expressiveCode,
|
|
152
|
+
sidebar: buildSidebar(docsRoot),
|
|
153
|
+
}),
|
|
154
|
+
],
|
|
155
|
+
markdown: {
|
|
156
|
+
processor: unified({
|
|
157
|
+
remarkPlugins: [
|
|
158
|
+
remarkStripDuplicateTitle,
|
|
159
|
+
[remarkWikiLinks, { contentRoot: docsRoot, basePath: import.meta.env.BASE_URL, failOnBrokenLinks: true }],
|
|
160
|
+
[remarkDocumentLinks, { contentRoot: docsRoot, basePath: import.meta.env.BASE_URL }],
|
|
161
|
+
remarkMermaid,
|
|
162
|
+
remarkStructuredDataPreview,
|
|
163
|
+
],
|
|
164
|
+
rehypePlugins: [rehypeCallouts],
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: JSON configuration reference
|
|
3
|
+
description: Complete reference for the consumer-owned .upcontent/config.json file.
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The `.upcontent/config.json` file is optional. An omitted field uses the portal or Starlight default.
|
|
9
|
+
|
|
10
|
+
## Start with the minimum
|
|
11
|
+
|
|
12
|
+
You only need a title and repository link to get started. Add a logo and custom CSS when you are ready to make the portal yours:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"site": {
|
|
17
|
+
"title": "Engineering Docs",
|
|
18
|
+
"description": "Documentation for the engineering team."
|
|
19
|
+
},
|
|
20
|
+
"repo": {
|
|
21
|
+
"url": "https://github.com/acme/engineering-docs"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Relative asset paths resolve from the consumer repository. Absolute URLs are also supported for externally hosted assets.
|
|
27
|
+
|
|
28
|
+
## Complete example
|
|
29
|
+
|
|
30
|
+
The optional fields below cover navigation, Starlight presentation, and content conventions. You can add them one group at a time.
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary>Show the complete configuration</summary>
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"site": {
|
|
38
|
+
"title": "Engineering Docs",
|
|
39
|
+
"description": "Documentation for the engineering team.",
|
|
40
|
+
"url": "https://docs.example.com",
|
|
41
|
+
"socialImage": "https://docs.example.com/social-card.png",
|
|
42
|
+
"locale": "en-US",
|
|
43
|
+
"logo": {
|
|
44
|
+
"src": ".upcontent/logo.svg",
|
|
45
|
+
"alt": "Engineering Docs",
|
|
46
|
+
"replacesTitle": false
|
|
47
|
+
},
|
|
48
|
+
"favicon": ".upcontent/favicon.svg"
|
|
49
|
+
},
|
|
50
|
+
"seo": {
|
|
51
|
+
"enabled": true
|
|
52
|
+
},
|
|
53
|
+
"repo": {
|
|
54
|
+
"url": "https://github.com/acme/engineering-docs"
|
|
55
|
+
},
|
|
56
|
+
"theme": {
|
|
57
|
+
"customCss": [".upcontent/theme.css"]
|
|
58
|
+
},
|
|
59
|
+
"starlight": {
|
|
60
|
+
"social": [
|
|
61
|
+
{ "icon": "github", "label": "GitHub", "href": "https://github.com/acme/engineering-docs" }
|
|
62
|
+
],
|
|
63
|
+
"tableOfContents": { "minHeadingLevel": 2, "maxHeadingLevel": 3 },
|
|
64
|
+
"lastUpdated": true,
|
|
65
|
+
"pagination": true,
|
|
66
|
+
"expressiveCode": {
|
|
67
|
+
"styleOverrides": { "borderRadius": "0.6rem" }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"navigation": {
|
|
71
|
+
"roots": ["README.md", "guides"],
|
|
72
|
+
"labelOverrides": { "api": "API reference" },
|
|
73
|
+
"blocklist": {
|
|
74
|
+
"exact": ["notes.md"],
|
|
75
|
+
"prefixes": ["drafts/"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"content": {
|
|
79
|
+
"titleField": "title"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
## Field groups
|
|
87
|
+
|
|
88
|
+
| Group | Controls |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `site` | Name, description, URL, social image, locale, logo, and favicon. |
|
|
91
|
+
| `seo` | Explicitly enables search-engine discoverability. Defaults to disabled. |
|
|
92
|
+
| `repo` | Source repository links. |
|
|
93
|
+
| `theme` | Consumer-owned local CSS. |
|
|
94
|
+
| `starlight` | Safe layout, social, table of contents, pagination, and code options. |
|
|
95
|
+
| `navigation` | Sidebar roots, labels, and blocklists. |
|
|
96
|
+
| `content` | Frontmatter title field selection. |
|
|
97
|
+
|
|
98
|
+
Invalid curated values are ignored or fall back safely. Heading levels must be integers from 1 through 6, and the minimum cannot exceed the maximum.
|
|
99
|
+
|
|
100
|
+
SEO is opt-in. With `seo.enabled` omitted or set to `false`, the portal emits `noindex, nofollow` and `robots.txt` disallows crawling. This does not protect the site: use access-controlled hosting for a private portal.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Content behavior
|
|
3
|
+
description: Configure page titles, metadata, and the Markdown features available to authors.
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Upcontent accepts Markdown and MDX files from the consumer repository. Starlight provides the page layout and frontmatter schema; Upcontent adds title fallback, wiki links, callouts, Mermaid, and structured previews.
|
|
9
|
+
|
|
10
|
+
## Choose a title field
|
|
11
|
+
|
|
12
|
+
If an existing repository uses a field such as `heading` instead of `title`, configure it once:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"content": {
|
|
17
|
+
"titleField": "heading"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When the configured field is missing, the portal falls back to the filename. A file named `first-build.md` becomes `First Build`.
|
|
23
|
+
|
|
24
|
+
## Page metadata
|
|
25
|
+
|
|
26
|
+
Use frontmatter at the top of a page:
|
|
27
|
+
|
|
28
|
+
```md
|
|
29
|
+
---
|
|
30
|
+
heading: Configure a consumer repository
|
|
31
|
+
sidebar:
|
|
32
|
+
order: 2
|
|
33
|
+
---
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `##` for body sections because the page title is rendered as the top-level heading.
|
|
37
|
+
|
|
38
|
+
SEO-specific frontmatter is optional:
|
|
39
|
+
|
|
40
|
+
```md
|
|
41
|
+
---
|
|
42
|
+
title: Configure a consumer repository
|
|
43
|
+
description: Set up a documentation repository and publish it as a searchable portal.
|
|
44
|
+
canonical: https://docs.example.com/getting-started/consumer-repository/
|
|
45
|
+
image: https://docs.example.com/social-card.png
|
|
46
|
+
noindex: false
|
|
47
|
+
---
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`description` is used in search and social metadata. `canonical`, `image`, and `noindex` override the generated defaults for that page. The site-level `socialImage` in `.upcontent/config.json` is used when a page does not define its own image.
|
|
51
|
+
|
|
52
|
+
## Supported content features
|
|
53
|
+
|
|
54
|
+
- Obsidian-style callouts for notes, tips, cautions, and dangers
|
|
55
|
+
- Mermaid diagrams with theme-aware rendering and interaction controls
|
|
56
|
+
- JSON and YAML data previews
|
|
57
|
+
- CSV tables
|
|
58
|
+
- `[[wiki links]]` with build-time target validation
|
|
59
|
+
- Syntax-highlighted code blocks
|
|
60
|
+
- Standard Markdown links, tables, lists, and images
|
|
61
|
+
|
|
62
|
+
See [Authoring content](../guides/authoring-content/) for writing rules and the [capability showcase](../showcase/) for rendered examples.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Environment variables
|
|
3
|
+
description: Configure repository links and hosting paths at build time.
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 7
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The portal is static, but a few values belong to the build environment rather than consumer content.
|
|
9
|
+
|
|
10
|
+
## Supported variables
|
|
11
|
+
|
|
12
|
+
| Variable | Purpose | Example |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `CONTENT_PATH` | Filesystem path passed to `make dev` or `make build`. | `./` |
|
|
15
|
+
| `REPO_URL` | Overrides the configured source repository URL for generated GitHub links. | `https://github.com/acme/docs` |
|
|
16
|
+
| `BASE_PATH` | URL prefix for a project site hosted below the domain root. | `/engineering-docs` |
|
|
17
|
+
| `SITE_URL` | Canonical site origin used by Astro integrations such as the sitemap. | `https://docs.example.com` |
|
|
18
|
+
|
|
19
|
+
## Local development
|
|
20
|
+
|
|
21
|
+
Pass the content path to Make:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
make dev CONTENT_PATH=.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`CONTENT_PATH` is a Make variable, not a value read from `.upcontent/config.json`.
|
|
28
|
+
|
|
29
|
+
## Static hosting
|
|
30
|
+
|
|
31
|
+
For GitHub Pages project sites, set a base path and site URL in the deployment workflow:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
env:
|
|
35
|
+
BASE_PATH: /engineering-docs
|
|
36
|
+
SITE_URL: https://acme.github.io/engineering-docs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For a custom domain or a host serving the site at `/`, leave `BASE_PATH` empty and set `SITE_URL` to the canonical origin.
|
|
40
|
+
|
|
41
|
+
## Precedence
|
|
42
|
+
|
|
43
|
+
`REPO_URL` takes precedence over `repo.url` for generated source links. `BASE_PATH` and `SITE_URL` are build inputs; they do not change the consumer configuration file.
|