upcontent 0.1.0 → 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.
Files changed (41) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.github/workflows/publish.yml +75 -0
  3. package/.upcontent/config.json +3 -0
  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 +60 -18
  10. package/customization/config-json.md +9 -1
  11. package/customization/content.md +14 -0
  12. package/deployment/index.md +1 -0
  13. package/deployment/npm.md +39 -0
  14. package/guides/validate-your-site.md +5 -0
  15. package/package.json +15 -9
  16. package/scripts/upcontent-cli.mjs +2 -4
  17. package/scripts/verify-external-build.mjs +9 -1
  18. package/scripts/verify-golden-build.mjs +9 -0
  19. package/src/components/MermaidLoader.astro +17 -3
  20. package/src/content/i18n/en.json +1 -0
  21. package/src/content.config.test.ts +47 -1
  22. package/src/content.config.ts +26 -3
  23. package/src/lib/portal-config.test.ts +11 -1
  24. package/src/lib/portal-config.ts +11 -0
  25. package/src/lib/portal-routes.test.ts +62 -0
  26. package/src/lib/portal-routes.ts +59 -0
  27. package/src/lib/remark-doc-links.test.ts +50 -0
  28. package/src/lib/remark-doc-links.ts +21 -0
  29. package/src/lib/remark-wiki-links.test.ts +8 -8
  30. package/src/lib/remark-wiki-links.ts +6 -4
  31. package/src/lib/seo-sitemap.test.ts +37 -0
  32. package/src/lib/seo-sitemap.ts +54 -0
  33. package/src/lib/sidebar.test.ts +2 -2
  34. package/src/lib/sidebar.ts +3 -2
  35. package/src/overrides/Head.astro +86 -3
  36. package/src/pages/robots.txt.ts +46 -0
  37. package/src/upcontent-cli.test.ts +36 -0
  38. package/test-fixtures/external-consumer/.upcontent/config.json +4 -0
  39. package/test-fixtures/external-consumer/noindex.md +7 -0
  40. package/test-fixtures/external-consumer/public.md +6 -0
  41. 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
  },
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,30 @@ 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'
14
17
  import { PRODUCT_NAME, PRODUCT_TAGLINE } from './src/lib/product-identity.ts'
15
18
 
16
19
  const docsRoot = fileURLToPath(new URL('./src/content/docs', import.meta.url))
17
20
  const portalConfig = getPortalConfig()
18
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
+
19
37
  function resolvePortalAsset(assetPath) {
20
38
  if (!assetPath || assetPath.startsWith('http')) return assetPath
21
39
  if (assetPath.startsWith('/')) return assetPath
@@ -58,6 +76,41 @@ const consumerCss = (portalConfig.theme?.customCss ?? [])
58
76
 
59
77
  const customCss = ['./src/styles/callouts.css', './src/styles/structured-data-preview.css', ...consumerCss]
60
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
+ }
61
114
 
62
115
  // Remark plugin: converts ```mermaid blocks to <div class="mermaid"> BEFORE Shiki runs
63
116
  function remarkMermaid() {
@@ -75,24 +128,12 @@ function remarkMermaid() {
75
128
  }
76
129
  }
77
130
 
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
131
  export default defineConfig({
92
132
  output: 'static',
93
- site: process.env.SITE_URL || portalConfig.site?.url || undefined,
94
- base: process.env.BASE_PATH || undefined,
133
+ site,
134
+ base,
95
135
  integrations: [
136
+ sitemap({ filter: includeInSitemap, serialize: serializeSitemapEntry }),
96
137
  starlight({
97
138
  title: portalConfig.site?.title ?? PRODUCT_NAME,
98
139
  description: portalConfig.site?.description ?? PRODUCT_TAGLINE,
@@ -115,11 +156,12 @@ export default defineConfig({
115
156
  processor: unified({
116
157
  remarkPlugins: [
117
158
  remarkStripDuplicateTitle,
118
- [remarkWikiLinks, { contentRoot: docsRoot, failOnBrokenLinks: true }],
159
+ [remarkWikiLinks, { contentRoot: docsRoot, basePath: import.meta.env.BASE_URL, failOnBrokenLinks: true }],
160
+ [remarkDocumentLinks, { contentRoot: docsRoot, basePath: import.meta.env.BASE_URL }],
119
161
  remarkMermaid,
120
162
  remarkStructuredDataPreview,
121
163
  ],
122
- rehypePlugins: [rehypeCallouts, rehypeStripMdLinks],
164
+ rehypePlugins: [rehypeCallouts],
123
165
  }),
124
166
  },
125
167
  })
@@ -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
@@ -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/package.json CHANGED
@@ -1,18 +1,31 @@
1
1
  {
2
2
  "name": "upcontent",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
+ "packageManager": "pnpm@10.20.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/lumamontes/upcontent.git"
9
+ },
5
10
  "bin": {
6
11
  "upcontent": "scripts/upcontent-cli.mjs"
7
12
  },
13
+ "scripts": {
14
+ "dev": "astro dev",
15
+ "build": "astro build",
16
+ "check": "astro check",
17
+ "test": "vitest run"
18
+ },
8
19
  "dependencies": {
9
20
  "@astrojs/markdown-remark": "^7.3.1",
21
+ "@astrojs/sitemap": "^3.7.4",
10
22
  "@astrojs/starlight": "^0.42.1",
11
23
  "@fontsource/poppins": "^5.3.0",
12
24
  "astro": "^7.3.3",
13
25
  "js-yaml": "^4.3.2",
14
26
  "mermaid": "^11.0.0",
15
27
  "papaparse": "^5.7.0",
28
+ "unified": "^11.0.5",
16
29
  "unist-util-visit": "^5.0.0",
17
30
  "zod": "^3.0.0"
18
31
  },
@@ -24,13 +37,6 @@
24
37
  "remark-parse": "^11.0.0",
25
38
  "remark-rehype": "^11.1.2",
26
39
  "typescript": "^5.0.0",
27
- "unified": "^11.0.5",
28
40
  "vitest": "^2.0.0"
29
- },
30
- "scripts": {
31
- "dev": "astro dev",
32
- "build": "astro build",
33
- "check": "astro check",
34
- "test": "vitest run"
35
41
  }
36
- }
42
+ }
@@ -56,7 +56,7 @@ on:
56
56
 
57
57
  jobs:
58
58
  publish:
59
- uses: lumamontes/upcontent/.github/workflows/reusable-pages.yml@main
59
+ uses: lumamontes/upcontent/.github/workflows/reusable-pages.yml@main
60
60
  permissions:
61
61
  contents: read
62
62
  pages: write
@@ -103,9 +103,7 @@ else if (command === 'dev') {
103
103
  process.exitCode = run('make', ['dev', `CONTENT_PATH=${root}`]) ? 0 : 1
104
104
  }
105
105
  else if (command === 'check') {
106
- const passed = run('pnpm', ['test'])
107
- && run('pnpm', ['check'])
108
- && run('make', ['build', `CONTENT_PATH=${root}`])
106
+ const passed = run('make', ['build', `CONTENT_PATH=${root}`])
109
107
  process.exitCode = passed ? 0 : 1
110
108
  }
111
109
  else {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
 
3
- const html = readFileSync('dist/readme/index.html', 'utf8')
3
+ const html = readFileSync('dist/index.html', 'utf8')
4
4
  const required = [
5
5
  'External Consumer',
6
6
  'https://github.com/example/external-consumer',
@@ -13,7 +13,15 @@ for (const value of required) {
13
13
 
14
14
  if (!existsSync('dist/upcontent-assets/favicon.svg')) throw new Error('External consumer favicon was not copied')
15
15
  if (!html.includes('alt="External Consumer"')) throw new Error('External consumer logo was not rendered')
16
+ if (!html.includes('application/ld+json')) throw new Error('External consumer JSON-LD metadata is missing')
17
+ if (!existsSync('dist/robots.txt')) throw new Error('External consumer robots.txt is missing')
18
+ if (!existsSync('dist/sitemap-index.xml')) throw new Error('External consumer sitemap is missing')
19
+ if (!readFileSync('dist/noindex/index.html', 'utf8').includes('noindex, nofollow')) throw new Error('External consumer noindex page is not marked noindex')
20
+ const sitemap = readFileSync('dist/sitemap-0.xml', 'utf8')
21
+ if (!sitemap.includes('<loc>https://docs.example.com/</loc>')) throw new Error('External consumer root route is missing from sitemap')
22
+ if (sitemap.includes('/noindex/')) throw new Error('External consumer noindex page leaked into sitemap')
16
23
  if (existsSync('dist/forbidden/index.html')) throw new Error('External consumer blocklist leaked forbidden.md')
24
+ if (existsSync('dist/assets/readme')) throw new Error('Golden README assets leaked into external consumer build')
17
25
  if (existsSync('dist/upcontent-renderer/readme/index.html')) {
18
26
  throw new Error('External consumer build leaked renderer checkout content')
19
27
  }