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 +13 -0
- package/package.json +1 -1
- package/runtime/domain.mjs +6 -2
- package/runtime/marketplace.mjs +31 -2
- package/src/project-readme.mjs +6 -0
- package/templates/app/components/orionplace/blog/BlogArticle.README.md +2 -2
- package/templates/app/components/orionplace/blog/BlogArticle.vue +188 -71
- package/templates/app/components/orionplace/catalog/StorefrontCatalog.vue +2 -2
- package/templates/app/components/orionplace/products/ProductCard.vue +14 -0
- package/templates/app/components/orionplace/products/ProductGroup.vue +3 -1
- package/templates/app/composables/useOrionplacePageSeo.ts +35 -6
- package/templates/app/composables/useOrionplaceProductSearch.ts +115 -11
- package/templates/app/pages/browse.vue +8 -1
- package/templates/app/pages/category/[id].vue +12 -4
- package/templates/app/pages/featured.vue +11 -3
- package/templates/app/utils/orionplace/blogArticleHtml.ts +1 -1
- package/templates/app/utils/orionplace/blogProductPlacements.ts +105 -0
- package/templates/app/utils/orionplace/pageSeo.ts +68 -0
- package/templates/app/utils/orionplace/productListLocation.ts +130 -0
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.
|
|
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",
|
package/runtime/domain.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// Other server code can read event.context.marketplaceDomain for canonical URLs.
|
|
2
|
-
|
|
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
|
});
|
package/runtime/marketplace.mjs
CHANGED
|
@@ -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',
|
package/src/project-readme.mjs
CHANGED
|
@@ -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.
|
|
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)
|