seo:item keywords
This commit is contained in:
parent
7a456b508d
commit
8c9192e27a
@ -1 +1,19 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@ -88,8 +88,8 @@
|
||||
"pages":{
|
||||
"home":{
|
||||
"hero": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg",
|
||||
"_blog":{
|
||||
"store": "posts"
|
||||
"blog":{
|
||||
"store": "resources"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/base/seo.ts
Normal file
45
src/base/seo.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import { createComponent } from "astro/runtime/server/astro-component.js";
|
||||
import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
|
||||
|
||||
import { findUp } from 'find-up'
|
||||
import { createLogger } from '@polymech/log'
|
||||
import { parse, IProfile, IComponentConfig } from '@polymech/commons'
|
||||
import { renderMarkup } from "@/model/component.js";
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
|
||||
import {
|
||||
LOGGING_NAMESPACE,
|
||||
OSRL_ENV,
|
||||
OSRL_PRODUCT_PROFILE,
|
||||
PRODUCT_ROOT,
|
||||
I18N_SOURCE_LANGUAGE
|
||||
} from 'config/config.js'
|
||||
|
||||
import { translate } from '@/base/i18n.js'
|
||||
import { item_defaults } from '@/base/index.js'
|
||||
import config from "../app/config.json" with { "type": "json" }
|
||||
|
||||
export const item_keywords = async (item: IComponentConfig, locale: string = I18N_SOURCE_LANGUAGE) => {
|
||||
let system_keywords = ""
|
||||
if (item.PRODUCT_ROOT) {
|
||||
const defaultsJson = await item_defaults(item.PRODUCT_ROOT);
|
||||
let defaults: Record<string, string> = defaultsJson ? read(defaultsJson, 'json') as Record<string, string> || {} : {}
|
||||
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const configKeywords = (config.metadata.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const itemKeywords = (item.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const allKeywords = Array.from(new Set([
|
||||
...defaultsKeywords,
|
||||
...configKeywords,
|
||||
...itemKeywords
|
||||
])).join(',');
|
||||
system_keywords = await translate(allKeywords, I18N_SOURCE_LANGUAGE, locale)
|
||||
}
|
||||
const keywordsArray = [...new Set([item.name, ...system_keywords.split(',')])]
|
||||
const keywords = keywordsArray.join(',')
|
||||
return keywords
|
||||
}
|
||||
@ -1,23 +1,20 @@
|
||||
---
|
||||
import "../styles/flowbite.css"
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE, isRTL } from "config/config.js"
|
||||
import { translate } from '@/base/i18n.js'
|
||||
import { item_defaults } from '@/base/index.js'
|
||||
|
||||
import { LANGUAGES_PROD, PRODUCT_ROOT } from "config/config.js"
|
||||
import { LANGUAGES_PROD } from "config/config.js"
|
||||
|
||||
import config from "config/config.json"
|
||||
import { plainify } from "../base/strings.js"
|
||||
|
||||
import "../styles/global.css"
|
||||
import "../styles/custom.scss"
|
||||
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
|
||||
import { AstroSeo } from "@astrolib/seo"
|
||||
import { item_keywords } from '@/base/seo.js'
|
||||
import StructuredData from './head/ArticleStructuredData.astro'
|
||||
|
||||
import Hreflang from '@/components/polymech/hreflang.astro'
|
||||
import { IComponentConfig } from "@polymech/commons"
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
@ -28,8 +25,6 @@ export interface Props {
|
||||
canonical?: string;
|
||||
view: string;
|
||||
path: string;
|
||||
additionalKeywords?: string[];
|
||||
onKeywordsProcessed?: (keywords: string[]) => void;
|
||||
frontmatter?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@ -38,51 +33,27 @@ export interface Props {
|
||||
};
|
||||
}
|
||||
const env = import.meta.env
|
||||
const { frontmatter, view, path, additionalKeywords = [], onKeywordsProcessed } = Astro.props
|
||||
const { frontmatter, view, path } = Astro.props as Props;
|
||||
const item: IComponentConfig = frontmatter as IComponentConfig || {}
|
||||
const { url } = Astro.request
|
||||
const REDIRECT = false //import.meta.env.I18N_REDIRECT
|
||||
const REDIRECT = false
|
||||
const _url = Astro.url
|
||||
|
||||
const canonicalUrl = _url.origin
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
|
||||
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE
|
||||
|
||||
const hreflangs = LANGUAGES_PROD.filter((lang)=>lang!==Astro.currentLocale).map((lang) => ({
|
||||
lang,
|
||||
url: `${canonicalUrl}/${lang}/${view}/${path}`,
|
||||
}))
|
||||
|
||||
const image = frontmatter?.image || config.site.image
|
||||
const image = item?.image || config.site.image
|
||||
const image_url = image.url
|
||||
const image_alt = image.alt
|
||||
|
||||
const title = frontmatter?.title || config.site.title
|
||||
const description = frontmatter?.description || config.metadata.description
|
||||
const title = item?.title as string || config.site.title || ""
|
||||
const description = item?.description as string || config.metadata.description
|
||||
|
||||
let system_keywords = ""
|
||||
const item_config = frontmatter as any || {}
|
||||
if(item_config.PRODUCT_ROOT){
|
||||
const defaultsJson = await item_defaults(item_config.PRODUCT_ROOT);
|
||||
let defaults:Record<string,string> = read(defaultsJson, 'json') || {}
|
||||
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const configKeywords = (config.metadata.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const itemKeywords = (item_config.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
|
||||
const allKeywords = Array.from(new Set([
|
||||
...defaultsKeywords,
|
||||
...configKeywords,
|
||||
...itemKeywords,
|
||||
...additionalKeywords
|
||||
])).join(',');
|
||||
system_keywords = await translate(allKeywords, I18N_SOURCE_LANGUAGE, locale)
|
||||
}
|
||||
const keywordsArray = [...new Set([item_config.name, ...system_keywords.split(','), ...additionalKeywords])]
|
||||
const keywords = keywordsArray.join(',')
|
||||
|
||||
// Call the callback with the processed keywords if it exists
|
||||
if (typeof onKeywordsProcessed === 'function') {
|
||||
onKeywordsProcessed(keywordsArray);
|
||||
}
|
||||
const keywords = await item_keywords(item, Astro.currentLocale)
|
||||
---
|
||||
|
||||
<AstroSeo
|
||||
@ -118,31 +89,33 @@ if (typeof onKeywordsProcessed === 'function') {
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<Hreflang canonical={canonicalURL} hreflangs={hreflangs} />
|
||||
|
||||
<!-- meta-description -->
|
||||
<meta name="description" content={plainify(description)}/>
|
||||
|
||||
<!-- meta-keywords -->
|
||||
<meta name="keywords" content={plainify( keywords )}/>
|
||||
<meta name="author" content={plainify( "" )} />
|
||||
<meta name="author" content={plainify( config?.metadata?.author )} />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicons/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/favicons/site.webmanifest" />
|
||||
|
||||
<!-- Favicon for IE -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="/favicons/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<!-- Favicons for different sizes -->
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
||||
|
||||
<!-- Additional SEO -->
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="googlebot" content="index, follow" />
|
||||
|
||||
<!-- Apple Touch Icon (already included in favicons, but keeping for backwards compatibility) -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
|
||||
|
||||
<!-- Theme Color for Mobile Browsers -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
@ -162,15 +135,16 @@ if (typeof onKeywordsProcessed === 'function') {
|
||||
/>
|
||||
|
||||
<script is:inline src="https://cdn.jsdelivr.net/npm/flowbite@3.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
<!-- alpine JS -->
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
|
||||
<StructuredData frontmatter={frontmatter} />
|
||||
<StructuredData frontmatter={item} />
|
||||
|
||||
{ REDIRECT && <script>
|
||||
const currentPath = window.location.pathname;
|
||||
if (!/^\/[a-z]{2}(\/|$)/i.test(currentPath)) {
|
||||
let language = navigator.language || navigator.userLanguage
|
||||
let language = navigator.language
|
||||
language = language.split('-')[0]
|
||||
language = "en"
|
||||
window.location.href = `/${language}`;
|
||||
|
||||
@ -15,7 +15,6 @@ interface Image {
|
||||
export interface Props {
|
||||
images: Image[];
|
||||
id?: string;
|
||||
onAltTextsProcessed?: (altTexts: string[]) => void;
|
||||
siteKeywords?: string[];
|
||||
gallerySettings?: {
|
||||
SIZES_REGULAR?: string;
|
||||
@ -33,7 +32,7 @@ export interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const { images, gallerySettings = {}, lightboxSettings = {}, onAltTextsProcessed, siteKeywords = [] } = Astro.props;
|
||||
const { images, gallerySettings = {}, lightboxSettings = {}, siteKeywords = [] } = Astro.props;
|
||||
|
||||
const mergedGallerySettings = {
|
||||
SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR,
|
||||
@ -74,11 +73,6 @@ const translatedAltTexts = await Promise.all(
|
||||
})
|
||||
);
|
||||
|
||||
// Pass the translated alt texts to the parent component if callback exists
|
||||
if (onAltTextsProcessed && typeof onAltTextsProcessed === 'function') {
|
||||
onAltTextsProcessed(translatedAltTexts.filter(Boolean));
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<div
|
||||
|
||||
65
src/components/src/base/index.ts
Normal file
65
src/components/src/base/index.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { translate } from "@/base/i18n.js"
|
||||
import config from "config/config.json"
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and combine keywords from multiple sources, translate them, and trigger an optional callback
|
||||
*
|
||||
* @param options - Configuration for processing keywords
|
||||
* @param options.itemConfig - The frontmatter or item configuration object
|
||||
* @param options.sourceLanguage - The source language for translation
|
||||
* @param options.targetLocale - The target locale for translation
|
||||
* @param options.additionalKeywords - Extra keywords to add to the processed list
|
||||
* @param options.onKeywordsProcessed - Optional callback that receives the processed keywords array
|
||||
* @returns Object containing the processed keywords as both an array and comma-separated string
|
||||
*/
|
||||
export async function processKeywords({
|
||||
itemConfig = {},
|
||||
sourceLanguage,
|
||||
targetLocale,
|
||||
additionalKeywords = [],
|
||||
onKeywordsProcessed
|
||||
}: {
|
||||
itemConfig?: Record<string, any>;
|
||||
sourceLanguage: string;
|
||||
targetLocale: string;
|
||||
additionalKeywords?: string[];
|
||||
onKeywordsProcessed?: (keywords: string[]) => void;
|
||||
}) {
|
||||
let systemKeywords = "";
|
||||
|
||||
if (itemConfig.PRODUCT_ROOT) {
|
||||
const defaultsJson = await item_defaults(itemConfig.PRODUCT_ROOT);
|
||||
const defaults: Record<string, string> = await import('@polymech/fs/read').then(m => m.sync(defaultsJson, 'json')) || {};
|
||||
|
||||
// Extract keywords from different sources
|
||||
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
|
||||
const configKeywords = (config.metadata?.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
|
||||
const itemKeywords = (itemConfig.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
|
||||
|
||||
// Combine and deduplicate all keywords
|
||||
const allKeywords = Array.from(new Set([
|
||||
...defaultsKeywords,
|
||||
...configKeywords,
|
||||
...itemKeywords,
|
||||
...additionalKeywords
|
||||
])).join(',');
|
||||
|
||||
// Translate the keywords
|
||||
systemKeywords = await translate(allKeywords, sourceLanguage, targetLocale);
|
||||
}
|
||||
|
||||
// Final processing and deduplication
|
||||
const keywordsArray = [...new Set([itemConfig.name, ...systemKeywords.split(','), ...additionalKeywords])].filter(Boolean);
|
||||
const keywords = keywordsArray.join(',');
|
||||
|
||||
// Call the callback with the processed keywords if it exists
|
||||
if (typeof onKeywordsProcessed === 'function') {
|
||||
onKeywordsProcessed(keywordsArray);
|
||||
}
|
||||
|
||||
return {
|
||||
keywordsArray,
|
||||
keywords
|
||||
};
|
||||
}
|
||||
21
src/components/src/components/BaseHead.astro
Normal file
21
src/components/src/components/BaseHead.astro
Normal file
@ -0,0 +1,21 @@
|
||||
import { processKeywords } from '@/base/index.js'
|
||||
|
||||
const item_config = frontmatter as any || {}
|
||||
|
||||
// Process keywords using the extracted function
|
||||
import { item_defaults, processKeywords } from '@/base/index.js'
|
||||
itemConfig: item_config,
|
||||
sourceLanguage: I18N_SOURCE_LANGUAGE,
|
||||
targetLocale: locale,
|
||||
additionalKeywords,
|
||||
onKeywordsProcessed
|
||||
})
|
||||
// Process keywords using the extracted function
|
||||
const item_config = frontmatter as any || {}
|
||||
const { keywords, keywordsArray } = await processKeywords({
|
||||
itemConfig: item_config,
|
||||
sourceLanguage: I18N_SOURCE_LANGUAGE,
|
||||
targetLocale: locale,
|
||||
additionalKeywords,
|
||||
onKeywordsProcessed
|
||||
})
|
||||
@ -16,6 +16,7 @@
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"lib": ["DOM", "ES2015"],
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"config/*": ["src/app/*"]
|
||||
|
||||
Reference in New Issue
Block a user