upcontent 0.1.0 → 0.1.2

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.
Files changed (49) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.github/workflows/publish.yml +75 -0
  3. package/.upcontent/config.json +6 -1
  4. package/CONTEXT.md +25 -0
  5. package/Makefile +6 -2
  6. package/README.md +57 -39
  7. package/assets/readme/portal-home.png +0 -0
  8. package/assets/readme/portal-showcase.png +0 -0
  9. package/astro.config.mjs +67 -18
  10. package/customization/config-json.md +9 -1
  11. package/customization/content.md +14 -0
  12. package/customization/navigation.md +2 -0
  13. package/deployment/index.md +1 -0
  14. package/deployment/npm.md +39 -0
  15. package/guides/validate-your-site.md +5 -0
  16. package/index.md +19 -0
  17. package/package.json +15 -9
  18. package/scripts/upcontent-cli.mjs +2 -4
  19. package/scripts/verify-external-build.mjs +9 -1
  20. package/scripts/verify-golden-build.mjs +13 -0
  21. package/src/components/MermaidLoader.astro +17 -3
  22. package/src/content/i18n/en.json +1 -0
  23. package/src/content.config.test.ts +52 -1
  24. package/src/content.config.ts +29 -4
  25. package/src/lib/content-blocklist.ts +2 -2
  26. package/src/lib/doc-links.test.ts +21 -1
  27. package/src/lib/doc-links.ts +9 -4
  28. package/src/lib/homepage.ts +16 -0
  29. package/src/lib/markdown.ts +6 -0
  30. package/src/lib/portal-config.test.ts +11 -1
  31. package/src/lib/portal-config.ts +11 -0
  32. package/src/lib/portal-routes.test.ts +73 -0
  33. package/src/lib/portal-routes.ts +65 -0
  34. package/src/lib/remark-doc-links.test.ts +50 -0
  35. package/src/lib/remark-doc-links.ts +21 -0
  36. package/src/lib/remark-wiki-links.test.ts +10 -8
  37. package/src/lib/remark-wiki-links.ts +17 -8
  38. package/src/lib/seo-sitemap.test.ts +46 -0
  39. package/src/lib/seo-sitemap.ts +55 -0
  40. package/src/lib/sidebar.test.ts +32 -3
  41. package/src/lib/sidebar.ts +26 -17
  42. package/src/overrides/Footer.astro +3 -5
  43. package/src/overrides/Head.astro +86 -3
  44. package/src/pages/robots.txt.ts +46 -0
  45. package/src/upcontent-cli.test.ts +36 -0
  46. package/test-fixtures/external-consumer/.upcontent/config.json +4 -0
  47. package/test-fixtures/external-consumer/noindex.md +7 -0
  48. package/test-fixtures/external-consumer/public.md +6 -0
  49. package/src/pages/index.astro +0 -20
@@ -40,10 +40,10 @@ jobs:
40
40
  - name: Run Astro diagnostics
41
41
  run: pnpm check
42
42
 
43
- - name: Build golden consumer
44
- run: make build CONTENT_PATH=.
45
-
46
43
  - name: Check golden artifact
44
+ run: make check-golden
45
+
46
+ - name: Check golden runtime assets
47
47
  run: |
48
48
  test -f dist/index.html
49
49
  test -f dist/pagefind/pagefind.js
@@ -0,0 +1,75 @@
1
+ name: Publish npm package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Check out repository
17
+ uses: actions/checkout@v5
18
+
19
+ - name: Set up pnpm
20
+ uses: pnpm/action-setup@v4
21
+ with:
22
+ version: 10.20.0
23
+
24
+ - name: Set up Node.js
25
+ uses: actions/setup-node@v5
26
+ with:
27
+ node-version: 24
28
+ registry-url: https://registry.npmjs.org
29
+ package-manager-cache: false
30
+
31
+ - name: Verify npm version
32
+ run: npm --version
33
+
34
+ - name: Require npm Trusted Publishing support
35
+ run: |
36
+ node -e "const [major, minor, patch] = process.argv[1].split('.').map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) process.exit(1)" "$(npm --version)"
37
+
38
+ - name: Verify tag matches package version
39
+ env:
40
+ TAG_VERSION: ${{ github.ref_name }}
41
+ run: |
42
+ test "${TAG_VERSION#v}" = "$(node -p "require('./package.json').version")"
43
+
44
+ - name: Install dependencies
45
+ run: pnpm install --frozen-lockfile
46
+
47
+ - name: Run tests
48
+ run: pnpm test
49
+
50
+ - name: Run Astro diagnostics
51
+ run: pnpm check
52
+
53
+ - name: Validate golden consumer
54
+ run: make check-golden
55
+
56
+ - name: Validate external consumer
57
+ run: make check-external
58
+
59
+ - name: Inspect package contents
60
+ run: npm pack --dry-run
61
+
62
+ - name: Check diff hygiene
63
+ run: |
64
+ git diff --check
65
+ git diff --exit-code
66
+ test -z "$(git status --porcelain --untracked-files=all)"
67
+
68
+ - name: Check release artifacts
69
+ run: |
70
+ test -f dist/index.html
71
+ test -f dist/pagefind/pagefind.js
72
+ test -f dist/upcontent-assets/favicon.svg
73
+
74
+ - name: Publish package
75
+ run: npm publish --access public
@@ -10,6 +10,9 @@
10
10
  },
11
11
  "favicon": ".upcontent/favicon.svg"
12
12
  },
13
+ "seo": {
14
+ "enabled": true
15
+ },
13
16
  "repo": {
14
17
  "url": "https://github.com/lumamontes/upcontent"
15
18
  },
@@ -38,6 +41,7 @@
38
41
  },
39
42
  "navigation": {
40
43
  "roots": [
44
+ "index.md",
41
45
  "README.md",
42
46
  "getting-started",
43
47
  "guides",
@@ -73,7 +77,8 @@
73
77
  ]
74
78
  },
75
79
  "labelOverrides": {
76
- "docs": "Project Docs"
80
+ "docs": "Project Docs",
81
+ "readme": "Repository README"
77
82
  }
78
83
  },
79
84
  "content": {
package/CONTEXT.md ADDED
@@ -0,0 +1,25 @@
1
+ # Upcontent Context
2
+
3
+ Upcontent renders documentation repositories into static portals. This context defines the publication and discoverability terms used by the renderer and its consumer repositories.
4
+
5
+ ## Publication And Discoverability
6
+
7
+ **Private source**:
8
+ The Git repository containing the documentation is access-controlled. This says nothing about the visibility of the generated portal.
9
+ _Avoid_: private portal
10
+
11
+ **Public portal**:
12
+ A generated portal that is publicly accessible and has explicitly enabled SEO discoverability.
13
+ _Avoid_: public repository
14
+
15
+ **Unlisted portal**:
16
+ A publicly accessible portal that asks crawlers not to index its pages. It is not access control and must not be described as private.
17
+ _Avoid_: private portal
18
+
19
+ **Private portal**:
20
+ A generated portal protected by the hosting layer so unauthenticated visitors and crawlers cannot access its content.
21
+ _Avoid_: hidden portal, noindex portal
22
+
23
+ **SEO policy**:
24
+ The explicit site-level choice to enable or disable search-engine discoverability for a portal. SEO is opt-in; page-level `noindex` can narrow an enabled policy to individual pages.
25
+ _Avoid_: SEO config, SEO mode
package/Makefile CHANGED
@@ -3,7 +3,7 @@ REPO_URL ?=
3
3
  BASE_PATH ?=
4
4
  SITE_URL ?=
5
5
 
6
- .PHONY: dev build preview check-external
6
+ .PHONY: dev build preview check-golden check-external
7
7
 
8
8
  define prepare-content
9
9
  @test -d "$(CONTENT_PATH)" || (printf 'Content path does not exist: %s\n' "$(CONTENT_PATH)" >&2; exit 1)
@@ -23,7 +23,11 @@ preview:
23
23
  $(MAKE) build CONTENT_PATH="$(CONTENT_PATH)" REPO_URL="$(REPO_URL)" BASE_PATH="$(BASE_PATH)" SITE_URL="$(SITE_URL)"
24
24
  pnpm exec astro preview
25
25
 
26
+ check-golden:
27
+ $(MAKE) build CONTENT_PATH="$(CURDIR)"
28
+ node scripts/verify-golden-build.mjs
29
+
26
30
  check-external:
27
31
  $(MAKE) build CONTENT_PATH="$(CURDIR)/test-fixtures/external-consumer" REPO_URL="https://github.com/example/external-consumer"
28
- @test -f dist/readme/index.html
32
+ @test -f dist/index.html
29
33
  node scripts/verify-external-build.mjs
package/README.md CHANGED
@@ -3,53 +3,73 @@ heading: Upcontent
3
3
  description: Turn a documentation repository into a fast, searchable, customizable static portal.
4
4
  ---
5
5
 
6
- Upcontent turns a Markdown repository into a documentation site that is ready to share.
6
+ <div align="center">
7
7
 
8
- It is for software teams that already have documentation, but need a better way to publish it: clear navigation, search, useful rendering for technical content, consumer-owned branding, and a repeatable static build.
8
+ # Upcontent
9
9
 
10
- ## The short version
10
+ **Turn the documentation repository you already have into a fast, searchable, customizable portal.**
11
11
 
12
- Upcontent gives a documentation repository:
12
+ [![CI](https://github.com/lumamontes/upcontent/actions/workflows/ci.yml/badge.svg)](https://github.com/lumamontes/upcontent/actions/workflows/ci.yml)
13
+ [![npm](https://img.shields.io/npm/v/upcontent?color=0f766e&label=npm)](https://www.npmjs.com/package/upcontent)
13
14
 
14
- - A responsive [Starlight](https://starlight.astro.build/) portal with navigation, search, themes, and table of contents
15
- - A `.upcontent/config.json` file for identity, navigation, rendering, and content boundaries
16
- - Custom CSS for the consumer's own brand
17
- - Rendered callouts, Mermaid diagrams, JSON, YAML, CSV, and wiki links
18
- - A static `dist/` directory that can be deployed to GitHub Pages or any static host
19
- - Build checks that catch broken wiki links and excluded content before publication
15
+ <a href="https://lumamontes.github.io/upcontent/">See the live showcase</a> · <a href="#get-started">Get started in under a minute</a>
20
16
 
21
- The source repository remains the source of truth. Upcontent does not require a database, a companion server, or a new authoring system.
17
+ </div>
22
18
 
23
- ## Is this the right tool?
19
+ ## See the result
24
20
 
25
- Upcontent is a good fit when:
21
+ This is the real Upcontent portal generated from this repository and deployed to GitHub Pages.
26
22
 
27
- - Your documentation already lives in Markdown or MDX.
28
- - Your team wants to keep writing in Git.
29
- - You want the portal to be owned and deployed by the documentation repository.
30
- - You need more than raw Markdown, but do not need a dynamic application.
31
- - You want branding and navigation without maintaining a custom docs frontend.
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>
32
26
 
33
- It is not the right fit when your site needs runtime authentication, server-rendered personalization, or review comments inside the published portal.
27
+ The same build includes a capability showcase for technical content, structured data, callouts, diagrams, navigation, and search.
34
28
 
35
- ## Get started
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>
36
32
 
37
- ### Publish an existing documentation repository
33
+ ## From repository to portal
38
34
 
39
- If your Markdown already lives in a GitHub repository, run the bootstrap command from that repository's root:
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:
40
38
 
41
39
  ```sh
42
40
  pnpm dlx upcontent init
43
41
  pnpm dlx upcontent dev
42
+ pnpm dlx upcontent check
44
43
  ```
45
44
 
46
- The first command creates `.upcontent/` and a GitHub Pages workflow. The second starts a local portal using the repository you are already in. You do not need to clone the Upcontent renderer into your documentation repository.
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.
47
48
 
48
- See [Set up a consumer repository](getting-started/consumer-repository/) for the generated structure and configuration.
49
+ ## What you get
49
50
 
50
- ### Explore this repository locally
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.
51
57
 
52
- Clone this repository, then start the included golden consumer. This path is for exploring Upcontent itself:
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:
53
73
 
54
74
  ```sh
55
75
  git clone https://github.com/lumamontes/upcontent.git
@@ -60,19 +80,26 @@ make dev CONTENT_PATH=.
60
80
 
61
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.
62
82
 
63
- The generated consumer configuration looks like this:
83
+ ## Configure your portal
84
+
85
+ The consumer configuration is deliberately small:
64
86
 
65
87
  ```json
66
88
  {
67
89
  "site": {
68
90
  "title": "Engineering Docs",
69
91
  "description": "Documentation for the engineering team.",
92
+ "socialImage": "https://docs.example.com/social-card.png",
93
+ "locale": "en-US",
70
94
  "logo": {
71
95
  "src": ".upcontent/logo.svg",
72
96
  "alt": "Engineering Docs"
73
97
  },
74
98
  "favicon": ".upcontent/favicon.svg"
75
99
  },
100
+ "seo": {
101
+ "enabled": true
102
+ },
76
103
  "repo": {
77
104
  "url": "https://github.com/acme/engineering-docs"
78
105
  },
@@ -84,24 +111,13 @@ The generated consumer configuration looks like this:
84
111
 
85
112
  Follow [Set up a consumer repository](getting-started/consumer-repository/) for the complete setup, then use [Customization](customization/) to shape the portal.
86
113
 
87
- ## See the full path
88
-
89
- - [First build](getting-started/first-build/): run the portal locally and generate static output.
90
- - [Consumer repository](getting-started/consumer-repository/): prepare content, assets, and configuration.
91
- - [Customization](customization/): configure identity, theme, navigation, content, JSON, and environment variables.
92
- - [Authoring content](guides/authoring-content/): write pages that are clear and easy to scan.
93
- - [Deployment](deployment/): publish the generated files to GitHub Pages or another static host.
94
- - [Capability showcase](showcase/): inspect all supported content and rendering features in one page.
95
-
96
114
  ## Build for production
97
115
 
98
116
  ```sh
99
117
  make build CONTENT_PATH=/path/to/your-consumer-repo
100
118
  ```
101
119
 
102
- The generated site is written to `dist/` and includes the Pagefind search index.
103
-
104
- Before publishing, run:
120
+ The generated site is written to `dist/` and includes the Pagefind search index. Before publishing, run:
105
121
 
106
122
  ```sh
107
123
  pnpm test
@@ -110,6 +126,8 @@ make build CONTENT_PATH=.
110
126
  make check-external
111
127
  ```
112
128
 
129
+ Read the [deployment guide](deployment/) for GitHub Pages and other static hosts.
130
+
113
131
  ## Built on Astro and Starlight
114
132
 
115
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.
Binary file
package/astro.config.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { fileURLToPath } from 'node:url'
2
- import { cpSync, existsSync, mkdirSync, statSync } from 'node:fs'
2
+ import { cpSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs'
3
3
  import { basename, resolve, sep } from 'node:path'
4
4
  import { unified } from '@astrojs/markdown-remark'
5
+ import sitemap from '@astrojs/sitemap'
5
6
  import starlight from '@astrojs/starlight'
6
7
  import { defineConfig } from 'astro/config'
7
8
  import { visit } from 'unist-util-visit'
@@ -9,13 +10,37 @@ import { rehypeCallouts } from './src/lib/rehype-callouts.ts'
9
10
  import { remarkStripDuplicateTitle } from './src/lib/remark-strip-duplicate-title.ts'
10
11
  import { remarkStructuredDataPreview } from './src/lib/remark-structured-data-preview.ts'
11
12
  import { remarkWikiLinks } from './src/lib/remark-wiki-links.ts'
13
+ import { remarkDocumentLinks } from './src/lib/remark-doc-links.ts'
12
14
  import { getPortalConfig } from './src/lib/portal-config.ts'
13
15
  import { buildSidebar } from './src/lib/sidebar.ts'
16
+ import { getNoindexRoutes } from './src/lib/seo-sitemap.ts'
17
+ import { hasRootIndex } from './src/lib/homepage.ts'
14
18
  import { PRODUCT_NAME, PRODUCT_TAGLINE } from './src/lib/product-identity.ts'
15
19
 
16
20
  const docsRoot = fileURLToPath(new URL('./src/content/docs', import.meta.url))
17
21
  const portalConfig = getPortalConfig()
18
22
 
23
+ function copyContentAssetDirectory(assetPath) {
24
+ const source = resolve(docsRoot, assetPath)
25
+ const target = resolve(process.cwd(), 'public', assetPath)
26
+ const readmeTarget = resolve(process.cwd(), 'public', 'readme', assetPath)
27
+ const hasHomepage = hasRootIndex(docsRoot)
28
+ const targets = hasHomepage ? [target, readmeTarget] : [target]
29
+ if (!hasHomepage) rmSync(readmeTarget, { force: true, recursive: true })
30
+ if (!existsSync(source) || !statSync(source).isDirectory()) {
31
+ for (const target of targets) rmSync(target, { force: true, recursive: true })
32
+ return
33
+ }
34
+
35
+ for (const target of targets) {
36
+ rmSync(target, { force: true, recursive: true })
37
+ mkdirSync(resolve(target, '..'), { recursive: true })
38
+ cpSync(source, target, { recursive: true })
39
+ }
40
+ }
41
+
42
+ copyContentAssetDirectory('assets/readme')
43
+
19
44
  function resolvePortalAsset(assetPath) {
20
45
  if (!assetPath || assetPath.startsWith('http')) return assetPath
21
46
  if (assetPath.startsWith('/')) return assetPath
@@ -58,6 +83,41 @@ const consumerCss = (portalConfig.theme?.customCss ?? [])
58
83
 
59
84
  const customCss = ['./src/styles/callouts.css', './src/styles/structured-data-preview.css', ...consumerCss]
60
85
  const starlightOptions = portalConfig.starlight ?? {}
86
+ const noindexRoutes = getNoindexRoutes(docsRoot)
87
+ const configuredSiteUrl = process.env.SITE_URL || portalConfig.site?.url
88
+ let site
89
+ let base = process.env.BASE_PATH || undefined
90
+ if (portalConfig.seo?.enabled === true && configuredSiteUrl) {
91
+ try {
92
+ const parsedSiteUrl = new URL(configuredSiteUrl)
93
+ if (parsedSiteUrl.protocol !== 'http:' && parsedSiteUrl.protocol !== 'https:') throw new Error('unsupported protocol')
94
+ if (parsedSiteUrl.username || parsedSiteUrl.password) throw new Error('userinfo is not allowed')
95
+ site = parsedSiteUrl.origin
96
+ if (!process.env.BASE_PATH) base = parsedSiteUrl.pathname === '/' ? undefined : parsedSiteUrl.pathname
97
+ } catch {
98
+ console.warn(`[${PRODUCT_NAME}] SEO site URL must be an absolute URL: ${configuredSiteUrl}`)
99
+ }
100
+ }
101
+
102
+ function includeInSitemap(page) {
103
+ const pathname = new URL(page).pathname.replace(/\/+$/, '') || '/'
104
+ const basePath = (base || '').replace(/\/+$/, '')
105
+ const route = pathname === basePath
106
+ ? '/'
107
+ : basePath && pathname.startsWith(`${basePath}/`)
108
+ ? pathname.slice(basePath.length)
109
+ : pathname
110
+ return !noindexRoutes.has(route)
111
+ }
112
+
113
+ function serializeSitemapEntry(entry) {
114
+ if (!site) return entry
115
+ const homepageUrl = new URL(`${(base || '').replace(/\/+$/, '')}/`, site).href
116
+ if (entry.url === homepageUrl.replace(/\/$/, '') || entry.url === homepageUrl) {
117
+ return { ...entry, url: homepageUrl }
118
+ }
119
+ return entry
120
+ }
61
121
 
62
122
  // Remark plugin: converts ```mermaid blocks to <div class="mermaid"> BEFORE Shiki runs
63
123
  function remarkMermaid() {
@@ -75,24 +135,12 @@ function remarkMermaid() {
75
135
  }
76
136
  }
77
137
 
78
- // Rehype plugin: strip .md suffix from internal hrefs so links resolve correctly
79
- function rehypeStripMdLinks() {
80
- return (tree) => {
81
- visit(tree, 'element', (node) => {
82
- if (node.tagName !== 'a') return
83
- const href = node.properties?.href
84
- if (typeof href !== 'string') return
85
- if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#')) return
86
- if (href.endsWith('.md')) node.properties.href = href.slice(0, -3)
87
- })
88
- }
89
- }
90
-
91
138
  export default defineConfig({
92
139
  output: 'static',
93
- site: process.env.SITE_URL || portalConfig.site?.url || undefined,
94
- base: process.env.BASE_PATH || undefined,
140
+ site,
141
+ base,
95
142
  integrations: [
143
+ sitemap({ filter: includeInSitemap, serialize: serializeSitemapEntry }),
96
144
  starlight({
97
145
  title: portalConfig.site?.title ?? PRODUCT_NAME,
98
146
  description: portalConfig.site?.description ?? PRODUCT_TAGLINE,
@@ -115,11 +163,12 @@ export default defineConfig({
115
163
  processor: unified({
116
164
  remarkPlugins: [
117
165
  remarkStripDuplicateTitle,
118
- [remarkWikiLinks, { contentRoot: docsRoot, failOnBrokenLinks: true }],
166
+ [remarkWikiLinks, { contentRoot: docsRoot, basePath: base || '/', failOnBrokenLinks: true }],
167
+ [remarkDocumentLinks, { contentRoot: docsRoot, basePath: base || '/' }],
119
168
  remarkMermaid,
120
169
  remarkStructuredDataPreview,
121
170
  ],
122
- rehypePlugins: [rehypeCallouts, rehypeStripMdLinks],
171
+ rehypePlugins: [rehypeCallouts],
123
172
  }),
124
173
  },
125
174
  })
@@ -38,6 +38,8 @@ The optional fields below cover navigation, Starlight presentation, and content
38
38
  "title": "Engineering Docs",
39
39
  "description": "Documentation for the engineering team.",
40
40
  "url": "https://docs.example.com",
41
+ "socialImage": "https://docs.example.com/social-card.png",
42
+ "locale": "en-US",
41
43
  "logo": {
42
44
  "src": ".upcontent/logo.svg",
43
45
  "alt": "Engineering Docs",
@@ -45,6 +47,9 @@ The optional fields below cover navigation, Starlight presentation, and content
45
47
  },
46
48
  "favicon": ".upcontent/favicon.svg"
47
49
  },
50
+ "seo": {
51
+ "enabled": true
52
+ },
48
53
  "repo": {
49
54
  "url": "https://github.com/acme/engineering-docs"
50
55
  },
@@ -82,7 +87,8 @@ The optional fields below cover navigation, Starlight presentation, and content
82
87
 
83
88
  | Group | Controls |
84
89
  | --- | --- |
85
- | `site` | Name, description, URL, logo, and favicon. |
90
+ | `site` | Name, description, URL, social image, locale, logo, and favicon. |
91
+ | `seo` | Explicitly enables search-engine discoverability. Defaults to disabled. |
86
92
  | `repo` | Source repository links. |
87
93
  | `theme` | Consumer-owned local CSS. |
88
94
  | `starlight` | Safe layout, social, table of contents, pagination, and code options. |
@@ -90,3 +96,5 @@ The optional fields below cover navigation, Starlight presentation, and content
90
96
  | `content` | Frontmatter title field selection. |
91
97
 
92
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.
@@ -35,6 +35,20 @@ sidebar:
35
35
 
36
36
  Use `##` for body sections because the page title is rendered as the top-level heading.
37
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
+
38
52
  ## Supported content features
39
53
 
40
54
  - Obsidian-style callouts for notes, tips, cautions, and dangers
@@ -7,6 +7,8 @@ sidebar:
7
7
 
8
8
  The sidebar is generated from the consumer file structure. Use configuration when the default filesystem order is not the right experience for readers.
9
9
 
10
+ If the repository has a root `index.md`, it becomes the website homepage and a root `README.md` remains available at `/readme/`. Repositories without `index.md` keep the backwards-compatible behavior where `README.md` is the homepage.
11
+
10
12
  ## Select top-level roots
11
13
 
12
14
  ```json
@@ -11,6 +11,7 @@ Upcontent produces static files. The deployment job only needs to build the cont
11
11
 
12
12
  - [GitHub Pages](github-pages/): use the included workflow and repository settings.
13
13
  - [Other static hosts](static-hosts/): publish `dist/` to Netlify, Vercel, S3, or another file host.
14
+ - [Release the npm package](npm/): publish versioned package releases through GitHub Actions.
14
15
  - [Environment variables](../customization/environment/): set the URL and base path for the host.
15
16
 
16
17
  ## Before publishing
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: Release the npm package
3
+ description: Publish versioned Upcontent releases through GitHub Actions and npm Trusted Publishing.
4
+ sidebar:
5
+ order: 3
6
+ ---
7
+
8
+ Upcontent publishes the `upcontent` package from version tags. Ordinary pushes to `main` run validation and deploy the documentation portal, but do not publish to npm.
9
+
10
+ ## Configure npm once
11
+
12
+ On the [upcontent npm package](https://www.npmjs.com/package/upcontent), add a GitHub Actions Trusted Publisher with:
13
+
14
+ - Organization or user: `lumamontes`
15
+ - Repository: `upcontent`
16
+ - Workflow filename: `publish.yml`
17
+ - Allow direct `npm publish`
18
+
19
+ Trusted Publishing uses short-lived GitHub OIDC credentials. No npm token is stored in GitHub Actions.
20
+
21
+ ## Release a version
22
+
23
+ Choose the next semantic version, update `package.json`, and commit the change:
24
+
25
+ ```sh
26
+ npm version patch --no-git-tag-version
27
+ git add package.json
28
+ git commit -m "release: v$(node -p "require('./package.json').version")"
29
+ ```
30
+
31
+ Create and push the matching tag:
32
+
33
+ ```sh
34
+ VERSION=$(node -p "require('./package.json').version")
35
+ git tag -a "v$VERSION" -m "Release v$VERSION"
36
+ git push origin main --follow-tags
37
+ ```
38
+
39
+ The publish workflow verifies that the tag and package version match, runs the full validation contract, inspects the package contents, and publishes the package with npm provenance.
@@ -13,6 +13,7 @@ Use this checklist before opening a pull request or publishing a consumer reposi
13
13
  pnpm test
14
14
  pnpm check
15
15
  make build CONTENT_PATH=.
16
+ make check-golden
16
17
  make check-external
17
18
  git diff --check
18
19
  ```
@@ -36,6 +37,10 @@ Open the pages listed in the [capability showcase](../showcase/) and check:
36
37
  - Callouts, Mermaid, JSON, YAML, and CSV examples render correctly.
37
38
  - The layout works at desktop and mobile widths.
38
39
  - The browser console has no required-asset errors.
40
+ - Each indexable page has a useful title, description, canonical URL, and one top-level heading.
41
+ - `dist/robots.txt` points to the generated sitemap.
42
+ - The page source contains JSON-LD and uses the intended `SITE_URL` and `BASE_PATH`.
43
+ - `seo.enabled` matches the intended portal policy; disabled SEO must produce `noindex, nofollow` and `Disallow: /`.
39
44
 
40
45
  ## Check content boundaries
41
46
 
package/index.md ADDED
@@ -0,0 +1,19 @@
1
+ ---
2
+ heading: Upcontent
3
+ description: Turn a documentation repository into a fast, searchable, customizable static portal.
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ Upcontent turns a Markdown repository into a documentation site that is ready to share.
9
+
10
+ It adds clear navigation, full-text search, technical content rendering, consumer-owned branding, and a repeatable static build while keeping the source repository as the source of truth.
11
+
12
+ ## Start here
13
+
14
+ - [Set up a consumer repository](getting-started/consumer-repository/)
15
+ - [Run your first build](getting-started/first-build/)
16
+ - [Customize the portal](customization/)
17
+ - [Validate before publishing](guides/validate-your-site/)
18
+
19
+ The [repository README](readme/) contains the project and development details for Upcontent itself.