create-bearminion 1.4.0 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # create-bearminion
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 8a428e7: Sitewide JSON-LD. `site.seo.jsonLd` is optional (up to 10 blocks, 50 KB, so no migration) and the default template emits its blocks on every page. A page's own `seo.jsonLd` still replaces only the generated `WebPage` block, and a block with the same `@id` is emitted once, the page's first. With the conventional ids `<url>/#organization` and `<url>/#website` in the site's blocks, the generated `WebPage` gets `isPartOf` and `publisher` as `@id` references. The Site tab has a Structured data group with an Edit JSON-LD dialog and a Generate button that fills an `Organization` and a `WebSite` from the site name, address and language, replacing blocks with the same `@id`. The logo is added only for a full https address. A warning shows while the site address is still `example.com`. `siteJsonLdIds`, `mergeJsonLd`, `hasJsonLdId`, `generateSiteJsonLd`, `upsertJsonLd` and `isPlaceholderSiteUrl` are exported; themes that emit JSON-LD should use `mergeJsonLd` (see the theme docs).
8
+
3
9
  ## 1.4.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bearminion",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Create a BearMinion site: an Astro website you edit by clicking on it.",
5
5
  "license": "MIT",
6
6
  "author": "Jakub Lange",
@@ -46,7 +46,7 @@
46
46
  "@types/node": "^22.19.0",
47
47
  "tsdown": "0.23.1",
48
48
  "typescript": "~5.9.3",
49
- "vitest": "^5.0.4"
49
+ "vitest": "^5.0.5"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "tsdown",
@@ -184,7 +184,7 @@ The site's own images (`logo.png`, `favicon.png`, `og.png`, `hero.png`) live in
184
184
 
185
185
  ## SEO and sitemap
186
186
 
187
- `Seo.astro` reads the page `seo` object and the site defaults in `site.json`. The document title is `seo.titleTemplate` with `%s` replaced by the page SEO title. Description falls back to the site description. The canonical URL is `seo.canonical` when set, otherwise the page path joined to `site.url`. Robots is `noindex` or `index`, plus `nofollow` or `follow`. Open Graph, Twitter, and WebPage JSON-LD use the same title, description, and image. A page that sets `seo.jsonLd` gets those blocks instead of the generated WebPage block, one script tag per block. `site.analytics.head` and `site.analytics.bodyEnd` are emitted by `Base.astro` in production builds only, behind the [cookie banner](#cookie-banner) unless it is switched off.
187
+ `Seo.astro` reads the page `seo` object and the site defaults in `site.json`. The document title is `seo.titleTemplate` with `%s` replaced by the page SEO title. Description falls back to the site description. The canonical URL is `seo.canonical` when set, otherwise the page path joined to `site.url`. Robots is `noindex` or `index`, plus `nofollow` or `follow`. Open Graph, Twitter, and WebPage JSON-LD use the same title, description, and image. Every block in `site.seo.jsonLd` (usually an `Organization` and a `WebSite`) is emitted on every page, and the generated WebPage then points at them by `@id`. A page that sets `seo.jsonLd` gets those blocks instead of the generated WebPage block only, never instead of the site's. A block with the same `@id` is emitted once, the page's copy winning. One script tag per block. `site.analytics.head` and `site.analytics.bodyEnd` are emitted by `Base.astro` in production builds only, behind the [cookie banner](#cookie-banner) unless it is switched off.
188
188
 
189
189
  `astro.config.ts` sets `site` from `site.json` and adds `@astrojs/sitemap`. The filter keeps content pages whose `seo.noindex` is false. `content/pages/preview.json` is the sample noindex page: it is built, it emits `noindex`, and it is absent from the sitemap. `src/pages/robots.txt.ts` points crawlers at `sitemap-index.xml`.
190
190
 
@@ -18,8 +18,8 @@
18
18
  "@fontsource/instrument-serif": "^5.3.0",
19
19
  "@fontsource/jetbrains-mono": "^5.3.0",
20
20
  "@tailwindcss/vite": "^4.1.12",
21
- "astro": "^7.3.8",
22
- "bearminion": "^1.4.0",
21
+ "astro": "^7.4.1",
22
+ "bearminion": "^1.5.0",
23
23
  "class-variance-authority": "^0.7.1",
24
24
  "sharp": "^0.35.5",
25
25
  "tailwind-merge": "^3.7.0",
@@ -27,8 +27,8 @@
27
27
  "zod": "^4.6.7"
28
28
  },
29
29
  "devDependencies": {
30
- "@astrojs/check": "^0.9.5",
31
- "@storybook-astro/framework": "^1.12.0",
30
+ "@astrojs/check": "^0.9.11",
31
+ "@storybook-astro/framework": "^1.13.0",
32
32
  "@storybook/builder-vite": "^10.6.2",
33
33
  "@types/node": "^22.19.0",
34
34
  "storybook": "^10.6.2",
@@ -1,5 +1,5 @@
1
1
  ---
2
- import type { Page } from 'bearminion'
2
+ import { hasJsonLdId, mergeJsonLd, siteJsonLdIds, type Page } from 'bearminion'
3
3
  import type { Site } from '../organisms/site-definition'
4
4
  import { isRemote, resolveLocalAsset } from '../../lib/assets'
5
5
 
@@ -31,19 +31,21 @@ const robots = [page.seo.noindex ? 'noindex' : 'index', page.seo.nofollow ? 'nof
31
31
  const ogImageSrc = page.seo.ogImage ?? site.seo.ogImage
32
32
  const ogImage = ogImageSrc ? absoluteImage(ogImageSrc) : undefined
33
33
 
34
- const jsonLd = {
34
+ // Blocks from site.seo.jsonLd go on every page. When the site describes itself, the generated WebPage points at those blocks.
35
+ const siteBlocks = site.seo.jsonLd ?? []
36
+ const ids = siteJsonLdIds(site.url)
37
+ const webPage = {
35
38
  '@context': 'https://schema.org',
36
39
  '@type': 'WebPage',
37
40
  name: title,
38
41
  description,
39
42
  url: canonical,
40
43
  ...(ogImage ? { image: ogImage } : {}),
41
- isPartOf: {
42
- '@type': 'WebSite',
43
- name: site.name,
44
- url: site.url,
45
- },
44
+ isPartOf: hasJsonLdId(siteBlocks, ids.website) ? { '@id': ids.website } : { '@type': 'WebSite', name: site.name, url: site.url },
45
+ ...(hasJsonLdId(siteBlocks, ids.organization) ? { publisher: { '@id': ids.organization } } : {}),
46
46
  }
47
+ // A page's own JSON-LD replaces only the generated WebPage. Blocks with the same @id are emitted once, the page's first.
48
+ const blocks = mergeJsonLd(siteBlocks, page.seo.jsonLd ?? [webPage])
47
49
  ---
48
50
 
49
51
  <title>{title}</title>
@@ -60,9 +62,9 @@ const jsonLd = {
60
62
  <meta name="twitter:title" content={title} />
61
63
  <meta name="twitter:description" content={description} />
62
64
  {ogImage && <meta name="twitter:image" content={ogImage} />}
63
- {/* Custom JSON-LD replaces the generated WebPage block. `<` is escaped so a value cannot close the script tag. */}
65
+ {/* `<` is escaped so a value cannot close the script tag. */}
64
66
  {
65
- (page.seo.jsonLd ?? [jsonLd]).map((block) => (
67
+ blocks.map((block) => (
66
68
  <script type="application/ld+json" set:html={JSON.stringify(block).replace(/</g, '\\u003c')} />
67
69
  ))
68
70
  }