Feat: JsonLd product metadata (#2)

* Generates JSON ld structured metadata from a product & appends to head

* Updated IProduct & IVariation interface

* Added IProductResponse & IProductsResponse interfaces

* Fixed sitemap urls having to many protocols
This commit is contained in:
2022-11-28 22:19:32 +01:00
committed by GitHub
parent f3751da335
commit 296cfb80a0
17 changed files with 103 additions and 45 deletions

View File

@@ -15,3 +15,13 @@ export interface IOrderSummaryResponse {
success: boolean;
order: IOrderSummary;
}
export interface IProductResponse {
success: boolean
product: IProduct
}
export interface IProductsResponse {
success: boolean
products: Array<IProduct>
}

View File

@@ -1,16 +1,21 @@
import type IProductVariation from './IProductVariation';
export default interface IProduct {
export interface IProduct {
product_no: number;
name: string;
subtext?: string;
description?: string;
image: string;
currency: string;
variations?: IProductVariation[];
primary_color?: string;
variation_count?: string;
sum_stock?: number;
updated?: Date;
created?: Date;
variations?: IVariation[];
}
export interface IVariation {
sku_id: number;
price: number;
size: string;
stock: number;
default_price: boolean;
updated?: Date;
created?: Date;
}

38
src/lib/jsonld/product.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { IProduct } from '../interfaces/IProduct'
function structureProduct(product: IProduct) {
const output = product?.variations?.map(variation => {
return {
'@context': 'https://schema.org/',
'@type': 'Product',
name: `${product.name} - ${variation.size}`,
image: [product.image],
description: product.description,
sku: `${product.product_no}-${variation.sku_id}`,
productID: product.product_no,
mpn: product.product_no,
brand: {
'@type': 'Brand',
name: 'Planetposen'
},
offers: {
'@type': 'Offer',
url: `https://planet.schleppe.cloud/shop/${product.product_no}`,
priceCurrency: 'NOK',
price: variation.price,
itemCondition: 'https://schema.org/NewCondition',
availability: 'https://schema.org/InStock'
}
}
})
return JSON.stringify(output);
}
export default function generateProductJsonLd(product: IProduct): HTMLElement {
const jsonldScript = document.createElement('script');
jsonldScript.setAttribute('type', 'application/ld+json');
jsonldScript.textContent = structureProduct(product);
return jsonldScript;
}

View File

@@ -7,7 +7,7 @@
import VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte';
import { cart } from '$lib/cartStore';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
function postOrder(event: any) {
const formData = new FormData(event.target);

View File

@@ -3,7 +3,7 @@
import { mockProducts } from '$lib/utils/mock';
import type { PageServerData } from './$types';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
function subTotal(products: Array<IProduct>) {
let total = 0;

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
import type { IProductsResponse } from '$lib/interfaces/ApiResponse';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
@@ -9,7 +10,7 @@ export const load: PageServerLoad = async ({ fetch }) => {
}
const res = await fetch(url);
const products = await res.json();
const products: IProductsResponse = await res.json();
return products;
};

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import ProductTile from './ProductTile.svelte';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
export let data: PageData;
const products = data.products as Array<IProduct>;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
export let product: IProduct;
export let large = false;
@@ -12,14 +12,16 @@
product?.primary_color === '#231B1D' ? '#f3efeb' : '#37301e'
}`}"
>
{#if !large}<h3>{product?.name}</h3>{/if}
{#if !large}
<h3>{product?.name}</h3>
{/if}
<div class="{`image-frame ${large ? 'large' : null}`}">
<img src="{product?.image}" alt="{product?.name}" />
</div>
{#if !large}
<p class="subtext">{product?.subtext}</p>
<p class="subtext">{product?.subtext}</p>
{/if}
</div>
</a>

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
import type { IProductResponse } from '$lib/interfaces/ApiResponse';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, params }) => {
@@ -11,6 +12,6 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
}
const res = await fetch(url);
const product = await res.json();
const product: IProductResponse = await res.json();
return product;
};

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import { onMount } from 'svelte'
import type { PageData } from './$types';
import ProductTile from '../ProductTile.svelte';
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
import SizesSection from './SizesSection.svelte';
import type IProduct from '$lib/interfaces/IProduct';
import type IProductVariation from '$lib/interfaces/IProductVariation';
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
import Button from '$lib/components/Button.svelte';
import generateProductJsonLd from '$lib/jsonld/product';
export let data: PageData;
const product = data.product as IProduct;
@@ -28,12 +29,19 @@
cooldownInputs = false;
}
function defaultVariation() {
return product.variations?.find(variation => variation.default_price)
}
let jsonLd: HTMLElement;
let cooldownInputs = false;
let quantity = 1;
let selectedVariation: IProductVariation;
let selectedVariation: IVariation | undefined = defaultVariation()
$: addProductButtonText = cooldownInputs
? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til`
: `Legg til ${quantity} i handlekurven`;
onMount(() => document.head.appendChild(generateProductJsonLd(product)))
</script>
<div class="product-container">

View File

@@ -41,18 +41,16 @@ export async function GET() {
return new Response(body, { headers });
}
function buildSitemapUrl(address: string, modified: string, frequency: string) {
function buildSitemapUrl(path: string, modified: string, frequency: string) {
return `<url>
<loc>https://${address}</loc>
<loc>https://${domain}${path}</loc>
<lastmod>${modified}</lastmod>
<changefreq>${frequency}</changefreq>
</url>`;
}
function sitemapPages(): string {
return pages
.map((page) => buildSitemapUrl(`https://${domain}/${page.name}`, page.modified, 'yearly'))
.join('\n');
return pages.map((page) => buildSitemapUrl(`/${page.name}`, page.modified, 'yearly')).join('\n');
}
async function sitemapShopPages(): Promise<string> {
@@ -66,11 +64,7 @@ async function sitemapShopPages(): Promise<string> {
return products?.products
?.map((product) =>
buildSitemapUrl(
`https://${domain}/shop/${product.product_no}`,
String(product.updated),
'daily'
)
buildSitemapUrl(`/shop/${product.product_no}`, String(product.updated), 'daily')
)
.join('\n');
}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import ProductList from './WarehouseProductList.svelte';
import type IProduct from '$lib/interfaces/IProduct';
import Button from '$lib/components/Button.svelte';
import type { IProduct } from '$lib/interfaces/IProduct';
import type { PageData } from './$types';
export let data: PageData;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
export let products: Array<IProduct>;
@@ -29,10 +29,10 @@
<td class="name-and-price">
<p><a href="/warehouse/{product.product_no}">{product.name}</a></p>
<p>{product.variation_count} variation(s)</p>
<p>{product?.variations?.length} variation(s)</p>
</td>
<td class="stock-column">{product.sum_stock}</td>
<td class="stock-column">{product?.sum_stock}</td>
<td class="date-column"
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(

View File

@@ -3,7 +3,7 @@
import DetailsSection from './DetailsSection.svelte';
import Button from '$lib/components/Button.svelte';
import type { PageServerData } from './$types';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
export let data: PageServerData;
const product = data.product as IProduct;
@@ -20,7 +20,7 @@
<img src="{product.image}" alt="Product" />
<div class="name-and-price">
<p>{product.name}</p>
<p>NOK {product.price}</p>
<p>NOK {product?.price}</p>
</div>
<div class="edit-button">

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
export let product: IProduct;
export let edit: boolean;

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import type IProduct from '$lib/interfaces/IProduct';
import type IProductVariation from '$lib/interfaces/IProductVariation';
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
import Button from '$lib/components/Button.svelte';
import Badge from '$lib/components/Badge.svelte';
import BadgeType from '$lib/interfaces/BadgeType';
@@ -10,7 +9,7 @@
let editingVariationIndex = -1;
interface ISkuResponse {
skus: IProductVariation[];
skus: IVariation[];
success: boolean;
}
@@ -19,7 +18,7 @@
product.variations = response?.skus;
};
function setDefault(variation: IProductVariation) {
function setDefault(variation: IVariation) {
if (!product.variations) return;
let url = `/api/product/${product.product_no}/sku/${variation.sku_id}/default_price`;
@@ -60,7 +59,7 @@
.then(() => (editingVariationIndex = product.variations.length - 1));
}
function saveSkuVariation(variation: IProductVariation) {
function saveSkuVariation(variation: IVariation) {
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
if (window?.location?.href.includes('localhost')) {
url = 'http://localhost:30010'.concat(url);
@@ -81,7 +80,7 @@
.then(() => resetEditingIndex());
}
function deleteVariation(variation: IProductVariation) {
function deleteVariation(variation: IVariation) {
console.log('delete it using api', variation);
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
if (window?.location?.href.includes('localhost')) {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Input from '$lib/components/Input.svelte';
import type IProduct from '$lib/interfaces/IProduct';
import type { IProduct } from '$lib/interfaces/IProduct';
import type { PageServerData } from './$types';
export let data: PageServerData;
@@ -12,9 +12,9 @@
<div>
<h2>Product attributes</h2>
<Input label="Name" value="{product.name}" required="{false}" />
<Input label="Description" value="{product.description}" required="{false}" />
<Input label="Subtext" value="{product.subtext}" required="{false}" />
<Input label="Color" value="{product.primary_color}" required="{false}" />
<Input label="Description" value="{product.description || ''}" required="{false}" />
<Input label="Subtext" value="{product.subtext || ''}" required="{false}" />
<Input label="Color" value="{product.primary_color || ''}" required="{false}" />
</div>
<div>