orionplace-nuxt 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -1021,6 +1021,19 @@ replace options, colors, counts or price bounds with narrowed responses. New
1021
1021
  queries, categories and AppLoad refreshes reset that scope; Clear keeps
1022
1022
  its original selectors. Superseded requests are cancelled and ignored.
1023
1023
 
1024
+ Category, search, featured and seller lists keep their result page and
1025
+ filters in the URL as `?page=2&filters={...}` (JSON of the filter selection).
1026
+ The composable replaces the current history entry on each change, so the
1027
+ browser's Back button from a product, and a reload, open the same list, while
1028
+ Back from the list still leaves it in one step. Page loads read the same
1029
+ query through `productListLocationFromQuery()` in
1030
+ `app/utils/orionplace/productListLocation.ts`, which accepts only plain filter
1031
+ values within fixed limits. A list opened this way on a later page or with
1032
+ filters also requests the unfiltered first page once, so the filter panel
1033
+ keeps every choice. Pass `rememberInAddress: false` to keep a list out of the
1034
+ URL. The React and Svelte packages do the same in `useProductSearch()` and
1035
+ `useOrionplaceProductSearch()`.
1036
+
1024
1037
  `SearchFilters` supplies cumulative ID arrays and scalar price, discount and
1025
1038
  rating fields. Currency-keyed `selects.measures.min_price` and `max_price` supply
1026
1039
  the price range; Apply or a committed slider change emits `price_from`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionplace-nuxt",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Nuxt 4 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and Russian, plus an Android app build.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,11 +1,15 @@
1
1
  // Other server code can read event.context.marketplaceDomain for canonical URLs.
2
- import { defineEventHandler, toWebRequest } from 'h3';
2
+ // Pages under ORIONPLACE_NOINDEX_PATHS answer with `X-Robots-Tag: noindex, follow`.
3
+ import { defineEventHandler, getRequestURL, setResponseHeader, toWebRequest } from 'h3';
3
4
  import { useRuntimeConfig } from '#imports';
4
- import { resolveFrontDomain } from './marketplace.mjs';
5
+ import { marketplaceNoindexPath, resolveFrontDomain } from './marketplace.mjs';
5
6
 
6
7
  export default defineEventHandler((event) => {
7
8
  event.context.marketplaceDomain = resolveFrontDomain(
8
9
  toWebRequest(event),
9
10
  process.env.MIMIC_FRONT_DOMAIN ?? useRuntimeConfig(event).mimicFrontDomain,
10
11
  );
12
+ if (marketplaceNoindexPath(getRequestURL(event).pathname)) {
13
+ setResponseHeader(event, 'X-Robots-Tag', 'noindex, follow');
14
+ }
11
15
  });
@@ -329,6 +329,7 @@ export async function forwardMarketplace(request, configuration = {}) {
329
329
  // node_modules/.cache/orionplace/sitemaps/<domain>.json (ORIONPLACE_CACHE_DIR
330
330
  // moves it). "orionplace-<framework> sitemap rebuild" rebuilds that file at
331
331
  // once; the running storefront serves the new file without a restart.
332
+ // Paths in ORIONPLACE_NOINDEX_PATHS are left out when the sitemap is served.
332
333
 
333
334
  export const SITEMAP_LIFETIME = 60 * 60 * 1000;
334
335
  const sitemapPageSize = 100;
@@ -353,6 +354,34 @@ export function marketplacePublicOrigin(request, tenantDomain = '') {
353
354
  return local || !tenantDomain ? url.origin : `https://${tenantDomain}`;
354
355
  }
355
356
 
357
+ /**
358
+ * Pages kept out of search engines: ORIONPLACE_NOINDEX_PATHS lists path
359
+ * prefixes separated by spaces or commas, for example "/catalog /category
360
+ * /product" on a demo or staging storefront. Those pages answer with
361
+ * `X-Robots-Tag: noindex, follow` and the sitemap leaves them out. Empty
362
+ * (the default) keeps every public page indexable.
363
+ */
364
+ export function noindexPathPrefixes(value = environment.ORIONPLACE_NOINDEX_PATHS) {
365
+ return String(value ?? '')
366
+ .split(/[\s,]+/)
367
+ .map((item) => item.trim().replace(/\/+$/, ''))
368
+ .filter((item) => /^\/[A-Za-z0-9._~\/-]+$/.test(item));
369
+ }
370
+
371
+ /** Whether a page path is kept out of search engines (see noindexPathPrefixes). */
372
+ export function marketplaceNoindexPath(pathname, value = environment.ORIONPLACE_NOINDEX_PATHS) {
373
+ const path = String(pathname ?? '');
374
+ return noindexPathPrefixes(value)
375
+ .some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
376
+ }
377
+
378
+ /** The sitemap pages search engines may index. */
379
+ export function sitemapIndexedPages(pages, value = environment.ORIONPLACE_NOINDEX_PATHS) {
380
+ return noindexPathPrefixes(value).length
381
+ ? pages.filter((page) => !marketplaceNoindexPath(page.path, value))
382
+ : pages;
383
+ }
384
+
356
385
  /** @param {string} origin */
357
386
  export function marketplaceRobots(origin) {
358
387
  return `User-agent: *\nDisallow: /api/\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`;
@@ -618,7 +647,7 @@ async function buildMarketplaceSitemap(tenant) {
618
647
  /** How many <url> entries a built sitemap has. */
619
648
  export function sitemapUrlCount(sitemap) {
620
649
  let count = 0;
621
- for (const page of sitemap.pages) {
650
+ for (const page of sitemapIndexedPages(sitemap.pages)) {
622
651
  const own = page.locales
623
652
  ? sitemap.locales.filter((code) => page.locales.includes(code))
624
653
  : sitemap.locales;
@@ -776,7 +805,7 @@ export async function marketplaceSitemapResponse(request, configuration = {}) {
776
805
  }
777
806
  }
778
807
  const origin = marketplacePublicOrigin(request, tenant.domain);
779
- return new Response(marketplaceSitemapXml(origin, sitemap.pages, sitemap.locales), {
808
+ return new Response(marketplaceSitemapXml(origin, sitemapIndexedPages(sitemap.pages), sitemap.locales), {
780
809
  headers: {
781
810
  'Content-Type': 'application/xml; charset=utf-8',
782
811
  'Cache-Control': 'public, max-age=3600',
@@ -571,6 +571,12 @@ npx orionplace-nuxt sitemap rebuild shop.example.com # one domain
571
571
  npx orionplace-nuxt sitemap status # when each was built
572
572
  \`\`\`
573
573
 
574
+ To keep a demo or staging storefront's shop pages out of search engines, set
575
+ \`ORIONPLACE_NOINDEX_PATHS\` in \`.env\` to path prefixes separated by spaces,
576
+ for example \`ORIONPLACE_NOINDEX_PATHS="/catalog /category /product"\`. Those
577
+ pages answer with \`X-Robots-Tag: noindex, follow\` and the sitemap leaves them
578
+ out; the journal and other pages stay indexable. Leave it empty on a live shop.
579
+
574
580
  This setup does not add analytics.
575
581
 
576
582
  ## Android app
@@ -8,7 +8,7 @@ The supplied storefront uses this component at `/journal`.
8
8
 
9
9
  ## Contract and dependencies
10
10
 
11
- The supplied `app/pages/journal/[id].vue` loads the article during the server render with `blog_posts_get` and `filters.id = postId`, using the visitor's cookies, and passes it in `initialPost`; the browser shows it without another request. Without `initialPost` (after client-side navigation) the component sends the same request itself. Full text appears only when both `is_full` and `has_full_access` are true; otherwise the preview and the access panel remain. Article HTML is sanitized with DOMPurify in `app/utils/orionplace/blogArticleHtml.ts` and keeps what the marketplace blog editor writes: headings, paragraphs and line breaks, bold, italic, underline, strikethrough, subscript and superscript, links, bullet, numbered and task lists, quotes, dividers, inline code and code blocks, tables with merged cells, images and galleries, video and audio with controls, collapsible sections, alignment, text color, highlight, font and size. External links open in a new tab with `rel="noopener noreferrer"`; editor links to other articles open `/journal/<id>`. Formulas are shown as their LaTeX source. Wide tables and code blocks scroll inside the article, and floated images stack on narrow screens. Product placements from the editor are not shown inside the text. Frames, scripts, forms, event attributes and other styles are removed. The server render shows the text as plain paragraphs, which search engines can read; the formatted HTML replaces them in the browser.
11
+ The supplied `app/pages/journal/[id].vue` loads the article during the server render with `blog_posts_get` and `filters.id = postId`, using the visitor's cookies, and passes it in `initialPost`; the browser shows it without another request. Without `initialPost` (after client-side navigation) the component sends the same request itself. Full text appears only when both `is_full` and `has_full_access` are true; otherwise the preview and the access panel remain. Article HTML is sanitized with DOMPurify in `app/utils/orionplace/blogArticleHtml.ts` and keeps what the marketplace blog editor writes: headings, paragraphs and line breaks, bold, italic, underline, strikethrough, subscript and superscript, links, bullet, numbered and task lists, quotes, dividers, inline code and code blocks, tables with merged cells, images and galleries, video and audio with controls, collapsible sections, alignment, text color, highlight, font and size. External links open in a new tab with `rel="noopener noreferrer"`; editor links to other articles open `/journal/<id>`. Formulas are shown as their LaTeX source. Wide tables and code blocks scroll inside the article, and floated images stack on narrow screens. Products the author places in the text appear at those positions: an empty block with `data-product-id="12"` shows one product card, `data-product-ids="12,15,9"` a row of up to 24 cards. The block keeps the editor's float or centred style, and `data-product-hidden-badges` (`new`, `popular`, `good-deal`) turns card badges off. Products missing from `related_products` are loaded with `products_search` and `filters.id_in`, up to 35 IDs per request, and stored with `store.setProduct()`; a block whose products are not found stays hidden. Article text styles do not reach the cards, and in a narrow article a floated card stands alone in the middle. The helper is `app/utils/orionplace/blogProductPlacements.ts`. Frames, scripts, forms, event attributes and other styles are removed. The server render shows the text as plain paragraphs, which search engines can read; the formatted HTML replaces them in the browser.
12
12
 
13
13
  Reading accounting for an accessible paid article starts after interaction with its visible text. `blog_reading_start` starts a session; `blog_reading_heartbeat` follows the returned interval. Leaving, losing focus, hiding the text or 60 seconds without interaction stops requests. Resuming and delayed timers start a fresh baseline. Confirmed loss of access removes full text and offers a refresh.
14
14
 
@@ -41,4 +41,4 @@ pages supplied by the full starter.
41
41
  - `blog_posts_get`
42
42
  - `blog_reading_start`
43
43
  - `blog_reading_heartbeat`
44
- - `products_search` (variant targets, through the host's variant controller)
44
+ - `products_search` (products placed in the text by `filters.id_in`; variant targets, through the host's variant controller)