site template cleanup 1/3
This commit is contained in:
parent
a64acb9f71
commit
bc07c94501
File diff suppressed because one or more lines are too long
935
package-lock.json
generated
935
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -53,7 +53,7 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.1",
|
||||
"got": "^14.4.6",
|
||||
"imagetools": "file:packages/imagetools",
|
||||
"imagetools": "file:../astro-components/packages/imagetools",
|
||||
"lighthouse": "^12.3.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"marked": "^15.0.7",
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import cli from 'yargs'
|
||||
import { hideBin } from 'yargs/helpers'
|
||||
import { } from './network.js'
|
||||
|
||||
const argv = cli(hideBin(process.argv)).parse()
|
||||
|
||||
export const options = () => {
|
||||
console.log('Options: ', argv)
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
{
|
||||
"site": {
|
||||
"title": "Polymech",
|
||||
"base_url": "https://polymech.io/",
|
||||
"description" : "",
|
||||
"base_path": "/",
|
||||
"trailing_slash": false,
|
||||
"favicon": "/images/favicon.png",
|
||||
"logo": "/images/logo.png",
|
||||
"logo_darkmode": "/images/logo-darkmode.png",
|
||||
"logo_width": "150",
|
||||
"logo_height": "33",
|
||||
"logo_text": "Astrofront",
|
||||
"image": {
|
||||
"default": "/images/default-image.png",
|
||||
"error": "/images/error-image.png",
|
||||
"alt": "Astrofront",
|
||||
"src": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg"
|
||||
}
|
||||
},
|
||||
"footer_left": [
|
||||
{
|
||||
"href": "/infopages/privacy",
|
||||
"text": "Privacy"
|
||||
},
|
||||
{
|
||||
"href": "/infopages/contact",
|
||||
"text": "Contact"
|
||||
} ,
|
||||
{
|
||||
"href": "/newsletter/newsletter_2024_09_en-hugo-release.html",
|
||||
"text": "Newsletter"
|
||||
}
|
||||
],
|
||||
"footer_right": [
|
||||
|
||||
],
|
||||
"settings": {
|
||||
"search": true,
|
||||
"account": true,
|
||||
"sticky_header": true,
|
||||
"theme_switcher": true,
|
||||
"default_theme": "system"
|
||||
},
|
||||
"params": {
|
||||
"contact_form_action": "#",
|
||||
"copyright": "Designed And Developed by [Themefisher](https://themefisher.com/)"
|
||||
},
|
||||
"navigation_button": {
|
||||
"enable": true,
|
||||
"label": "Get Started",
|
||||
"link": "https://github.com/themefisher/astrofront"
|
||||
},
|
||||
"ecommerce": {
|
||||
"brand": "Polymech",
|
||||
"currencySymbol": "",
|
||||
"currencyCode": "EUR"
|
||||
},
|
||||
"metadata": {
|
||||
"country": "Spain",
|
||||
"city": "Barcelona",
|
||||
"author": "Polymech",
|
||||
"author_bio": "I am in, if its true",
|
||||
"author_url": "https://polymech.io/",
|
||||
"image": "/images/og-image.png",
|
||||
"description": "Polymech is a plastic prototyping company that offers product design services.",
|
||||
"keywords": "Plastic, Prototyping, Product Design, Opensource"
|
||||
},
|
||||
"shopify": {
|
||||
"currencySymbol": "",
|
||||
"currencyCode": "EUR",
|
||||
"collections": {
|
||||
"hero_slider": "hidden-homepage-carousel",
|
||||
"featured_products": "featured-products"
|
||||
}
|
||||
},
|
||||
"pages":{
|
||||
"home":{
|
||||
"hero": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg",
|
||||
"blog":{
|
||||
"store": "resources"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracking": {
|
||||
"googleAnalytics": "G-RW6Q6EG3J0"
|
||||
}
|
||||
}
|
||||
@ -1,170 +0,0 @@
|
||||
import * as path from 'path'
|
||||
import { IMAGE_PRESET, E_BROADBAND_SPEED } from "./network.js"
|
||||
import { resolve, template } from '@polymech/commons'
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sanitizeUri } from 'micromark-util-sanitize-uri'
|
||||
|
||||
export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
|
||||
|
||||
export const LOGGING_NAMESPACE = 'polymech-site'
|
||||
export const TRANSLATE_CONTENT = true
|
||||
export const LANGUAGES = ['en', 'ar', 'de', 'ja', 'es', 'zh', 'fr']
|
||||
export const LANGUAGES_PROD = ['en', 'es', 'ar', 'de', 'ja', 'zh', 'fr', 'nl', 'it', 'pt']
|
||||
export const isRTL = (lang) => lang === 'ar'
|
||||
|
||||
// i18n constants
|
||||
export const I18N_STORE = (root, lang) => `${root}/i18n-store/store-${lang}.json`
|
||||
export const I18N_SOURCE_LANGUAGE = 'en'
|
||||
export const I18N_CACHE = true
|
||||
export const I18N_ASSET_PATH = "${SRC_DIR}/${SRC_NAME}-${DST_LANG}${SRC_EXT}"
|
||||
|
||||
// Products
|
||||
export const PRODUCT_ROOT = () => path.resolve(resolve("${OSR_ROOT}/products"))
|
||||
export const PRODUCT_BRANCHES = read(path.join(PRODUCT_ROOT(), 'config/machines.json'), 'json')
|
||||
export const PRODUCT_GLOB = '**/config.json'
|
||||
|
||||
// Product compiler
|
||||
export const PRODUCT_CONFIG = (product) =>
|
||||
path.resolve(resolve(`${PRODUCT_ROOT()}/${product}/config.json`, false,
|
||||
{
|
||||
product
|
||||
}))
|
||||
export const PRODUCT_DIR = (product) => path.resolve(resolve(`${PRODUCT_ROOT()}/${product}`))
|
||||
export const PRODUCT_HUGO_TEMPLATE = './osr/hugo/root.html'
|
||||
export const PRODUCTS_TARGET_SRC = './src/content/en/retail'
|
||||
export const PRODUCTS_TARGET = (lang) => `./content/${lang}/products`
|
||||
|
||||
// Product assets
|
||||
|
||||
export const ASSETS_LOCAL = false
|
||||
|
||||
// OSRL - Language
|
||||
export const IS_DEV = true
|
||||
export const OSRL_ENV = 'astro-release'
|
||||
export const OSRL_ENV_DEV = 'astro-debug'
|
||||
export const OSRL_ENVIRONMENT = IS_DEV ? OSRL_ENV_DEV : OSRL_ENV
|
||||
export const OSRL_MODULE_NAME = 'polymech.io'
|
||||
export const OSRL_PRODUCT_PROFILE = './src/app/profile.json'
|
||||
export const OSRL_LANG_FLAVOR = 'osr'
|
||||
|
||||
// Products
|
||||
export const ENABLED_PRODUCTS = "${OSR_ROOT}/products/config/machines.json"
|
||||
export const PRODUCT_SPECS = (rel) => `${PRODUCT_ROOT()}/${rel}/specs.xlsx`
|
||||
|
||||
// Tasks
|
||||
export const TASK_CONFIG_LOG_DIRECTORY = './config/'
|
||||
|
||||
// Task: compile:content
|
||||
export const TASK_COMPILE_CONTENT = true
|
||||
export const TASK_COMPILE_CONTENT_CACHE = false
|
||||
|
||||
// Task - Logging
|
||||
export const TASK_LOG_DIRECTORY = './logs/'
|
||||
|
||||
// Task - Retail Config
|
||||
export const REGISTER_PRODUCT_TASKS = true
|
||||
export const RETAIL_PRODUCT_BRANCH = 'site'
|
||||
export const PROJECTS_BRANCH = 'projects'
|
||||
export const RETAIL_COMPILE_CACHE = false
|
||||
export const RETAIL_MEDIA_CACHE = true
|
||||
export const RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS = 'info'
|
||||
|
||||
export const ConvertProductMedia = true
|
||||
export const TranslateProductAssets = false
|
||||
export const PopulateProductDefaults = true
|
||||
|
||||
// CAD
|
||||
export const CAD_MAIN_MATCH = (product) => `${product}/cad*/*Global*.+(SLDASM)`
|
||||
export const CAD_CAM_MAIN_MATCH = (product) => `${product}/cad*/*-CNC*.+(SLDASM)`
|
||||
|
||||
export const CAD_CACHE = true
|
||||
export const CAD_EXPORT_CONFIGURATIONS = true
|
||||
export const CAD_EXPORT_SUB_COMPONENTS = true
|
||||
export const CAD_MODEL_FILE_PATH = (SOURCE, CONFIGURATION = '') =>
|
||||
SOURCE.replace('.json', `${CONFIGURATION ? '-' + CONFIGURATION : ''}.tree.json`)
|
||||
export const CAD_DEFAULT_CONFIGURATION = 'Default'
|
||||
export const CAD_RENDERER = 'solidworks'
|
||||
export const CAD_RENDERER_VIEW = 'Render'
|
||||
export const CAD_RENDERER_QUALITY = 1
|
||||
export const CAD_EXTENSIONS = ['.STEP', '.html']
|
||||
export const CAD_MODEL_EXT = '.tree.json'
|
||||
|
||||
export const CAD_URL = (file: string, variables: Record<string, string>) =>
|
||||
sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/${file}", { file, ...variables }))
|
||||
|
||||
export const ASSET_URL = (file: string, variables: Record<string, string>) =>
|
||||
sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/products/${product_rel_min}/${file}", { file, ...variables }))
|
||||
|
||||
export const ITEM_ASSET_URL = (variables: Record<string, string>) =>
|
||||
template("${OSR_MACHINES_ASSETS_URL}/${ITEM_REL}/${assetPath}/${filePath}", variables)
|
||||
|
||||
|
||||
//back compat - osr-cad
|
||||
export const parseBoolean = (value: string): boolean => {
|
||||
return value === '1' || value.toLowerCase() === 'true';
|
||||
}
|
||||
/////////////////////////////////////////////
|
||||
//
|
||||
// Rendering
|
||||
export const SHOW_DESCRIPTION = false
|
||||
export const SHOW_LICENSE = true
|
||||
export const SHOW_RENDERINGS = true
|
||||
|
||||
export const SHOW_TABS = false
|
||||
export const SHOW_GALLERY = true
|
||||
export const SHOW_FILES = true
|
||||
export const SHOW_SPECS = true
|
||||
export const SHOW_CHECKOUT = true
|
||||
export const SHOW_CONTACT = true
|
||||
export const SHOW_3D_PREVIEW = true
|
||||
export const SHOW_RESOURCES = true
|
||||
export const SHOW_DEBUG = false
|
||||
export const SHOW_SAMPLES = true
|
||||
export const SHOW_README = false
|
||||
export const SHOW_RELATED = true
|
||||
|
||||
/////////////////////////////////////////////
|
||||
//
|
||||
// Plugins
|
||||
|
||||
// RSS
|
||||
export const RSS_CONFIG =
|
||||
{
|
||||
title: 'Polymech RSS Feed',
|
||||
description: '',
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
//
|
||||
// Defaults
|
||||
export const DEFAULT_IMAGE_URL = 'https://picsum.photos/640/640'
|
||||
export const default_image = () => {
|
||||
return {
|
||||
alt: 'none',
|
||||
src: DEFAULT_IMAGE_URL
|
||||
}
|
||||
}
|
||||
export const DEFAULT_LICENSE = `CERN Open Source Hardware License`
|
||||
export const DEFAULT_CONTACT = `sales@plastic-hub.com`
|
||||
/////////////////////////////////////////////
|
||||
//
|
||||
// Optimization
|
||||
|
||||
export const O_IMAGE = IMAGE_PRESET[E_BROADBAND_SPEED.MEDIUM]
|
||||
export const IMAGE_SETTINGS =
|
||||
{
|
||||
GALLERY: {
|
||||
SHOW_TITLE: true,
|
||||
SHOW_DESCRIPTION: false,
|
||||
SIZES_THUMB: O_IMAGE.sizes_thumbs,
|
||||
SIZES_LARGE: O_IMAGE.sizes_large,
|
||||
SIZES_REGULAR: O_IMAGE.sizes_medium
|
||||
},
|
||||
LIGHTBOX: {
|
||||
SHOW_TITLE: true,
|
||||
SHOW_DESCRIPTION: true,
|
||||
SIZES_THUMB: O_IMAGE.sizes_thumbs,
|
||||
SIZES_LARGE: O_IMAGE.sizes_large,
|
||||
SIZES_REGULAR: O_IMAGE.sizes_medium
|
||||
}
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
import { promises as fs, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import yaml from 'js-yaml';
|
||||
import { posixRelative } from '../utils.js';
|
||||
import type { Loader, LoaderContext } from './types.js';
|
||||
|
||||
export interface FileOptions {
|
||||
/**
|
||||
* the parsing function to use for this data
|
||||
* @default JSON.parse or yaml.load, depending on the extension of the file
|
||||
* */
|
||||
parser?: (
|
||||
text: string,
|
||||
) => Record<string, Record<string, unknown>> | Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads entries from a JSON file. The file must contain an array of objects that contain unique `id` fields, or an object with string keys.
|
||||
* @param fileName The path to the JSON file to load, relative to the content directory.
|
||||
* @param options Additional options for the file loader
|
||||
*/
|
||||
export function file(fileName: string, options?: FileOptions): Loader {
|
||||
if (fileName.includes('*')) {
|
||||
// TODO: AstroError
|
||||
throw new Error('Glob patterns are not supported in `file` loader. Use `glob` loader instead.');
|
||||
}
|
||||
|
||||
let parse: ((text: string) => any) | null = null;
|
||||
|
||||
const ext = fileName.split('.').at(-1);
|
||||
if (ext === 'json') {
|
||||
parse = JSON.parse;
|
||||
} else if (ext === 'yml' || ext === 'yaml') {
|
||||
parse = (text) =>
|
||||
yaml.load(text, {
|
||||
filename: fileName,
|
||||
});
|
||||
}
|
||||
if (options?.parser) parse = options.parser;
|
||||
|
||||
if (parse === null) {
|
||||
// TODO: AstroError
|
||||
throw new Error(
|
||||
`No parser found for file '${fileName}'. Try passing a parser to the \`file\` loader.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function syncData(filePath: string, { logger, parseData, store, config }: LoaderContext) {
|
||||
let data: Array<Record<string, unknown>> | Record<string, Record<string, unknown>>;
|
||||
|
||||
try {
|
||||
const contents = await fs.readFile(filePath, 'utf-8');
|
||||
data = parse!(contents);
|
||||
} catch (error: any) {
|
||||
logger.error(`Error reading data from ${fileName}`);
|
||||
logger.debug(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedFilePath = posixRelative(fileURLToPath(config.root), filePath);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) {
|
||||
logger.warn(`No items found in ${fileName}`);
|
||||
}
|
||||
logger.debug(`Found ${data.length} item array in ${fileName}`);
|
||||
store.clear();
|
||||
for (const rawItem of data) {
|
||||
const id = (rawItem.id ?? rawItem.slug)?.toString();
|
||||
if (!id) {
|
||||
logger.error(`Item in ${fileName} is missing an id or slug field.`);
|
||||
continue;
|
||||
}
|
||||
const parsedData = await parseData({ id, data: rawItem, filePath });
|
||||
store.set({ id, data: parsedData, filePath: normalizedFilePath });
|
||||
}
|
||||
} else if (typeof data === 'object') {
|
||||
const entries = Object.entries<Record<string, unknown>>(data);
|
||||
logger.debug(`Found object with ${entries.length} entries in ${fileName}`);
|
||||
store.clear();
|
||||
for (const [id, rawItem] of entries) {
|
||||
const parsedData = await parseData({ id, data: rawItem, filePath });
|
||||
store.set({ id, data: parsedData, filePath: normalizedFilePath });
|
||||
}
|
||||
} else {
|
||||
logger.error(`Invalid data in ${fileName}. Must be an array or object.`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'file-loader',
|
||||
load: async (context) => {
|
||||
const { config, logger, watcher } = context;
|
||||
logger.debug(`Loading data from ${fileName}`);
|
||||
const url = new URL(fileName, config.root);
|
||||
if (!existsSync(url)) {
|
||||
logger.error(`File not found: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
const filePath = fileURLToPath(url);
|
||||
|
||||
await syncData(filePath, context);
|
||||
|
||||
watcher?.add(filePath);
|
||||
|
||||
watcher?.on('change', async (changedPath) => {
|
||||
if (changedPath === filePath) {
|
||||
logger.info(`Reloading data from ${fileName}`);
|
||||
await syncData(filePath, context);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,358 +0,0 @@
|
||||
import { promises as fs, existsSync } from 'node:fs';
|
||||
import { relative } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import fastGlob from 'fast-glob';
|
||||
import { bold, green } from 'kleur/colors';
|
||||
import micromatch from 'micromatch';
|
||||
import pLimit from 'p-limit';
|
||||
import type { ContentEntryRenderFunction, ContentEntryType } from '../../types/public/content.js';
|
||||
import type { RenderedContent } from '../data-store.js';
|
||||
import { getContentEntryIdAndSlug, posixRelative } from '../utils.js';
|
||||
import type { Loader } from './types.js';
|
||||
|
||||
export interface GenerateIdOptions {
|
||||
/** The path to the entry file, relative to the base directory. */
|
||||
entry: string;
|
||||
|
||||
/** The base directory URL. */
|
||||
base: URL;
|
||||
/** The parsed, unvalidated data of the entry. */
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GlobOptions {
|
||||
/** The glob pattern to match files, relative to the base directory */
|
||||
pattern: string | Array<string>;
|
||||
/** The base directory to resolve the glob pattern from. Relative to the root directory, or an absolute file URL. Defaults to `.` */
|
||||
base?: string | URL;
|
||||
/**
|
||||
* Function that generates an ID for an entry. Default implementation generates a slug from the entry path.
|
||||
* @returns The ID of the entry. Must be unique per collection.
|
||||
**/
|
||||
generateId?: (options: GenerateIdOptions) => string;
|
||||
}
|
||||
|
||||
function generateIdDefault({ entry, base, data }: GenerateIdOptions): string {
|
||||
if (data.slug) {
|
||||
return data.slug as string;
|
||||
}
|
||||
const entryURL = new URL(encodeURI(entry), base);
|
||||
const { slug } = getContentEntryIdAndSlug({
|
||||
entry: entryURL,
|
||||
contentDir: base,
|
||||
collection: '',
|
||||
});
|
||||
return slug;
|
||||
}
|
||||
|
||||
function checkPrefix(pattern: string | Array<string>, prefix: string) {
|
||||
if (Array.isArray(pattern)) {
|
||||
return pattern.some((p) => p.startsWith(prefix));
|
||||
}
|
||||
return pattern.startsWith(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads multiple entries, using a glob pattern to match files.
|
||||
* @param pattern A glob pattern to match files, relative to the content directory.
|
||||
*/
|
||||
export function glob(globOptions: GlobOptions): Loader;
|
||||
/** @private */
|
||||
export function glob(
|
||||
globOptions: GlobOptions & {
|
||||
/** @deprecated */
|
||||
_legacy?: true;
|
||||
},
|
||||
): Loader;
|
||||
|
||||
export function glob(globOptions: GlobOptions): Loader {
|
||||
if (checkPrefix(globOptions.pattern, '../')) {
|
||||
throw new Error(
|
||||
'Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead.',
|
||||
);
|
||||
}
|
||||
if (checkPrefix(globOptions.pattern, '/')) {
|
||||
throw new Error(
|
||||
'Glob patterns cannot start with `/`. Set the `base` option to a parent directory or use a relative path instead.',
|
||||
);
|
||||
}
|
||||
|
||||
const generateId = globOptions?.generateId ?? generateIdDefault;
|
||||
|
||||
const fileToIdMap = new Map<string, string>();
|
||||
|
||||
return {
|
||||
name: 'glob-loader',
|
||||
load: async ({ config, logger, watcher, parseData, store, generateDigest, entryTypes }) => {
|
||||
const renderFunctionByContentType = new WeakMap<
|
||||
ContentEntryType,
|
||||
ContentEntryRenderFunction
|
||||
>();
|
||||
|
||||
const untouchedEntries = new Set(store.keys());
|
||||
const isLegacy = (globOptions as any)._legacy;
|
||||
// If global legacy collection handling flag is *not* enabled then this loader is used to emulate them instead
|
||||
const emulateLegacyCollections = !config.legacy.collections;
|
||||
async function syncData(
|
||||
entry: string,
|
||||
base: URL,
|
||||
entryType?: ContentEntryType,
|
||||
oldId?: string,
|
||||
) {
|
||||
if (!entryType) {
|
||||
logger.warn(`No entry type found for ${entry}`);
|
||||
return;
|
||||
}
|
||||
const fileUrl = new URL(encodeURI(entry), base);
|
||||
const contents = await fs.readFile(fileUrl, 'utf-8').catch((err) => {
|
||||
logger.error(`Error reading ${entry}: ${err.message}`);
|
||||
return;
|
||||
});
|
||||
|
||||
if (!contents && contents !== '') {
|
||||
logger.warn(`No contents found for ${entry}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { body, data } = await entryType.getEntryInfo({
|
||||
contents,
|
||||
fileUrl,
|
||||
});
|
||||
|
||||
const id = generateId({ entry, base, data });
|
||||
|
||||
if (oldId && oldId !== id) {
|
||||
store.delete(oldId);
|
||||
}
|
||||
|
||||
let legacyId: string | undefined;
|
||||
|
||||
if (isLegacy) {
|
||||
const entryURL = new URL(encodeURI(entry), base);
|
||||
const legacyOptions = getContentEntryIdAndSlug({
|
||||
entry: entryURL,
|
||||
contentDir: base,
|
||||
collection: '',
|
||||
});
|
||||
legacyId = legacyOptions.id;
|
||||
}
|
||||
untouchedEntries.delete(id);
|
||||
|
||||
const existingEntry = store.get(id);
|
||||
|
||||
const digest = generateDigest(contents);
|
||||
const filePath = fileURLToPath(fileUrl);
|
||||
|
||||
if (existingEntry && existingEntry.digest === digest && existingEntry.filePath) {
|
||||
if (existingEntry.deferredRender) {
|
||||
store.addModuleImport(existingEntry.filePath);
|
||||
}
|
||||
|
||||
if (existingEntry.assetImports?.length) {
|
||||
// Add asset imports for existing entries
|
||||
store.addAssetImports(existingEntry.assetImports, existingEntry.filePath);
|
||||
}
|
||||
|
||||
fileToIdMap.set(filePath, id);
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = posixRelative(fileURLToPath(config.root), filePath);
|
||||
|
||||
const parsedData = await parseData({
|
||||
id,
|
||||
data,
|
||||
filePath,
|
||||
});
|
||||
|
||||
if (entryType.getRenderFunction) {
|
||||
if (isLegacy && data.layout) {
|
||||
logger.error(
|
||||
`The Markdown "layout" field is not supported in content collections in Astro 5. Ignoring layout for ${JSON.stringify(entry)}. Enable "legacy.collections" if you need to use the layout field.`,
|
||||
);
|
||||
}
|
||||
|
||||
let render = renderFunctionByContentType.get(entryType);
|
||||
if (!render) {
|
||||
render = await entryType.getRenderFunction(config);
|
||||
// Cache the render function for this content type, so it can re-use parsers and other expensive setup
|
||||
renderFunctionByContentType.set(entryType, render);
|
||||
}
|
||||
let rendered: RenderedContent | undefined = undefined;
|
||||
|
||||
try {
|
||||
rendered = await render?.({
|
||||
id,
|
||||
data,
|
||||
body,
|
||||
filePath,
|
||||
digest,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`Error rendering ${entry}: ${error.message}`);
|
||||
}
|
||||
|
||||
store.set({
|
||||
id,
|
||||
data: parsedData,
|
||||
body,
|
||||
filePath: relativePath,
|
||||
digest,
|
||||
rendered,
|
||||
assetImports: rendered?.metadata?.imagePaths,
|
||||
legacyId,
|
||||
});
|
||||
|
||||
// todo: add an explicit way to opt in to deferred rendering
|
||||
} else if ('contentModuleTypes' in entryType) {
|
||||
store.set({
|
||||
id,
|
||||
data: parsedData,
|
||||
body,
|
||||
filePath: relativePath,
|
||||
digest,
|
||||
deferredRender: true,
|
||||
legacyId,
|
||||
});
|
||||
} else {
|
||||
store.set({ id, data: parsedData, body, filePath: relativePath, digest, legacyId });
|
||||
}
|
||||
|
||||
fileToIdMap.set(filePath, id);
|
||||
}
|
||||
|
||||
const baseDir = globOptions.base ? new URL(globOptions.base, config.root) : config.root;
|
||||
|
||||
if (!baseDir.pathname.endsWith('/')) {
|
||||
baseDir.pathname = `${baseDir.pathname}/`;
|
||||
}
|
||||
|
||||
const filePath = fileURLToPath(baseDir);
|
||||
const relativePath = relative(fileURLToPath(config.root), filePath);
|
||||
|
||||
const exists = existsSync(baseDir);
|
||||
|
||||
if (!exists) {
|
||||
// We warn and don't return because we will still set up the watcher in case the directory is created later
|
||||
logger.warn(`The base directory "${fileURLToPath(baseDir)}" does not exist.`);
|
||||
}
|
||||
|
||||
const files = await fastGlob(globOptions.pattern, {
|
||||
cwd: fileURLToPath(baseDir),
|
||||
});
|
||||
|
||||
if (exists && files.length === 0) {
|
||||
logger.warn(
|
||||
`No files found matching "${globOptions.pattern}" in directory "${relativePath}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
function configForFile(file: string) {
|
||||
const ext = file.split('.').at(-1);
|
||||
if (!ext) {
|
||||
logger.warn(`No extension found for ${file}`);
|
||||
return;
|
||||
}
|
||||
return entryTypes.get(`.${ext}`);
|
||||
}
|
||||
|
||||
const limit = pLimit(10);
|
||||
const skippedFiles: Array<string> = [];
|
||||
|
||||
const contentDir = new URL('content/', config.srcDir);
|
||||
|
||||
function isInContentDir(file: string) {
|
||||
const fileUrl = new URL(file, baseDir);
|
||||
return fileUrl.href.startsWith(contentDir.href);
|
||||
}
|
||||
|
||||
const configFiles = new Set(
|
||||
['config.js', 'config.ts', 'config.mjs'].map((file) => new URL(file, contentDir).href),
|
||||
);
|
||||
|
||||
function isConfigFile(file: string) {
|
||||
const fileUrl = new URL(file, baseDir);
|
||||
return configFiles.has(fileUrl.href);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
files.map((entry) => {
|
||||
if (isConfigFile(entry)) {
|
||||
return;
|
||||
}
|
||||
if (!emulateLegacyCollections && isInContentDir(entry)) {
|
||||
skippedFiles.push(entry);
|
||||
return;
|
||||
}
|
||||
return limit(async () => {
|
||||
const entryType = configForFile(entry);
|
||||
await syncData(entry, baseDir, entryType);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const skipCount = skippedFiles.length;
|
||||
|
||||
if (skipCount > 0) {
|
||||
const patternList = Array.isArray(globOptions.pattern)
|
||||
? globOptions.pattern.join(', ')
|
||||
: globOptions.pattern;
|
||||
|
||||
logger.warn(
|
||||
`The glob() loader cannot be used for files in ${bold('src/content')} when legacy mode is enabled.`,
|
||||
);
|
||||
if (skipCount > 10) {
|
||||
logger.warn(
|
||||
`Skipped ${green(skippedFiles.length)} files that matched ${green(patternList)}.`,
|
||||
);
|
||||
} else {
|
||||
logger.warn(`Skipped the following files that matched ${green(patternList)}:`);
|
||||
skippedFiles.forEach((file) => logger.warn(`• ${green(file)}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove entries that were not found this time
|
||||
untouchedEntries.forEach((id) => store.delete(id));
|
||||
|
||||
if (!watcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
watcher.add(filePath);
|
||||
|
||||
const matchesGlob = (entry: string) =>
|
||||
!entry.startsWith('../') && micromatch.isMatch(entry, globOptions.pattern);
|
||||
|
||||
const basePath = fileURLToPath(baseDir);
|
||||
|
||||
async function onChange(changedPath: string) {
|
||||
const entry = posixRelative(basePath, changedPath);
|
||||
if (!matchesGlob(entry)) {
|
||||
return;
|
||||
}
|
||||
const entryType = configForFile(changedPath);
|
||||
const baseUrl = pathToFileURL(basePath);
|
||||
const oldId = fileToIdMap.get(changedPath);
|
||||
await syncData(entry, baseUrl, entryType, oldId);
|
||||
logger.info(`Reloaded data from ${green(entry)}`);
|
||||
}
|
||||
|
||||
watcher.on('change', onChange);
|
||||
|
||||
watcher.on('add', onChange);
|
||||
|
||||
watcher.on('unlink', async (deletedPath) => {
|
||||
const entry = posixRelative(basePath, deletedPath);
|
||||
if (!matchesGlob(entry)) {
|
||||
return;
|
||||
}
|
||||
const id = fileToIdMap.get(deletedPath);
|
||||
if (id) {
|
||||
store.delete(id);
|
||||
fileToIdMap.delete(deletedPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export { file } from './file.js';
|
||||
export { glob } from './glob.js';
|
||||
export * from './types.js';
|
||||
@ -1,51 +0,0 @@
|
||||
import type { FSWatcher } from 'vite';
|
||||
import type { ZodSchema } from 'zod';
|
||||
import type { AstroIntegrationLogger } from '../../core/logger/core.js';
|
||||
import type { AstroConfig } from '../../types/public/config.js';
|
||||
import type { ContentEntryType } from '../../types/public/content.js';
|
||||
import type { DataStore, MetaStore } from '../mutable-data-store.js';
|
||||
|
||||
export type { DataStore, MetaStore };
|
||||
|
||||
export interface ParseDataOptions<TData extends Record<string, unknown>> {
|
||||
/** The ID of the entry. Unique per collection */
|
||||
id: string;
|
||||
/** The raw, unvalidated data of the entry */
|
||||
data: TData;
|
||||
/** An optional file path, where the entry represents a local file. */
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
export interface LoaderContext {
|
||||
/** The unique name of the collection */
|
||||
collection: string;
|
||||
/** A database to store the actual data */
|
||||
store: DataStore;
|
||||
/** A simple KV store, designed for things like sync tokens */
|
||||
meta: MetaStore;
|
||||
logger: AstroIntegrationLogger;
|
||||
/** Astro config, with user config and merged defaults */
|
||||
config: AstroConfig;
|
||||
/** Validates and parses the data according to the collection schema */
|
||||
parseData<TData extends Record<string, unknown>>(props: ParseDataOptions<TData>): Promise<TData>;
|
||||
|
||||
/** Generates a non-cryptographic content digest. This can be used to check if the data has changed */
|
||||
generateDigest(data: Record<string, unknown> | string): string;
|
||||
|
||||
/** When running in dev, this is a filesystem watcher that can be used to trigger updates */
|
||||
watcher?: FSWatcher;
|
||||
|
||||
/** If the loader has been triggered by an integration, this may optionally contain extra data set by that integration */
|
||||
refreshContextData?: Record<string, unknown>;
|
||||
/** @internal */
|
||||
entryTypes: Map<string, ContentEntryType>;
|
||||
}
|
||||
|
||||
export interface Loader {
|
||||
/** Unique name of the loader, e.g. the npm package name */
|
||||
name: string;
|
||||
/** Do the actual loading of the data */
|
||||
load: (context: LoaderContext) => Promise<void>;
|
||||
/** Optionally, define the schema of the data. Will be overridden by user-defined schema */
|
||||
schema?: ZodSchema | Promise<ZodSchema> | (() => ZodSchema | Promise<ZodSchema>);
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
{
|
||||
"main": [
|
||||
{
|
||||
"name": "Home",
|
||||
"url": "/"
|
||||
},
|
||||
{
|
||||
"name": "Products",
|
||||
"url": "/products"
|
||||
},
|
||||
{
|
||||
"name": "Pages",
|
||||
"url": "",
|
||||
"hasChildren": true,
|
||||
"children": [
|
||||
{
|
||||
"name": "About",
|
||||
"url": "/about"
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"url": "/contact"
|
||||
},
|
||||
{
|
||||
"name": "404 Page",
|
||||
"url": "/404"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"url": "/contact"
|
||||
}
|
||||
],
|
||||
"footer": [
|
||||
{
|
||||
"name": "About",
|
||||
"url": "/about"
|
||||
},
|
||||
{
|
||||
"name": "Products",
|
||||
"url": "/products"
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"url": "/contact"
|
||||
}
|
||||
],
|
||||
"footerCopyright": [
|
||||
{
|
||||
"name": "Privacy & Policy",
|
||||
"url": "/privacy-policy"
|
||||
},
|
||||
{
|
||||
"name": "Terms of Service",
|
||||
"url": "/terms-services"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import { translate } from '../base/i18n.js'
|
||||
import { I18N_SOURCE_LANGUAGE } from 'config/config.js'
|
||||
import config from "./config.json"
|
||||
import pMap from 'p-map'
|
||||
|
||||
export const items = async (opts: { locale: string }) => {
|
||||
const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, opts.locale)
|
||||
return [
|
||||
{
|
||||
"href": `/${opts.locale}`,
|
||||
"title": _T("Home"),
|
||||
"ariaLabel": "Home",
|
||||
"class": "hover:text-orange-600"
|
||||
},
|
||||
{
|
||||
"href": `/${opts.locale}/infopages/resources`,
|
||||
"title": _T("Resources"),
|
||||
"ariaLabel": "Resources",
|
||||
"class": "hover:text-orange-600"
|
||||
}
|
||||
]
|
||||
}
|
||||
const isAbsoluteUrl = (url: string): boolean => /^[a-zA-Z]+:/.test(url);
|
||||
|
||||
const createFooterLinks = async (items: any[], locale: string) => {
|
||||
const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, locale);
|
||||
|
||||
return await pMap(items, async (item) => {
|
||||
const translatedText = await _T(item.text); // Single translation call
|
||||
|
||||
return {
|
||||
"href": isAbsoluteUrl(item.href) ? item.href : `/${locale}${item.href}`,
|
||||
"title": translatedText, // Use cached translation
|
||||
"ariaLabel": translatedText, // Use cached translation
|
||||
"class": "hover:text-orange-600"
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const footer_left = (locale: string) => createFooterLinks(config.footer_left, locale);
|
||||
export const footer_right = (locale: string) => createFooterLinks(config.footer_right, locale);
|
||||
@ -1,49 +0,0 @@
|
||||
import { z } from "zod"
|
||||
/////////////////////////////////////////////
|
||||
//
|
||||
// Optimizations
|
||||
|
||||
// Image optimization (imagetools breakpoints & min widths)
|
||||
|
||||
export enum E_BROADBAND_SPEED {
|
||||
SLOW = "slow",
|
||||
MEDIUM = "medium",
|
||||
FAST = "fast",
|
||||
}
|
||||
|
||||
const imageConfigSchema = z.object({
|
||||
sizes_medium: z.string(),
|
||||
sizes_thumbs: z.string(),
|
||||
sizes_large: z.string(),
|
||||
})
|
||||
const imagesSchema = z.object({
|
||||
[E_BROADBAND_SPEED.SLOW]: imageConfigSchema,
|
||||
[E_BROADBAND_SPEED.MEDIUM]: imageConfigSchema,
|
||||
[E_BROADBAND_SPEED.FAST]: imageConfigSchema,
|
||||
});
|
||||
|
||||
type Images = z.infer<typeof imagesSchema>;
|
||||
|
||||
export const IMAGE_PRESET: Images =
|
||||
{
|
||||
[E_BROADBAND_SPEED.SLOW]: {
|
||||
// For 2g connections: smaller image widths help performance. (Middle East & Africa)
|
||||
sizes_medium: "(min-width: 100px) 100px, 100vw",
|
||||
sizes_thumbs: "(min-width: 80px) 80px, 80vw",
|
||||
sizes_large: "(min-width: 320px) 320px, 320vw",
|
||||
},
|
||||
[E_BROADBAND_SPEED.MEDIUM]:
|
||||
{
|
||||
// For 3g connections: a moderate size image for a balance of quality and speed.
|
||||
sizes_medium: "(min-width: 400px) 400px, 400vw",
|
||||
sizes_thumbs: "(min-width: 120px) 120px, 120vw",
|
||||
sizes_large: "(min-width: 1024px) 1024px, 1024vw",
|
||||
},
|
||||
[E_BROADBAND_SPEED.FAST]:
|
||||
{
|
||||
// For 4g connections: larger images for high-resolution displays.
|
||||
sizes_medium: "(min-width: 1024px) 1024px, 1024vw",
|
||||
sizes_thumbs: "(min-width: 180px) 180px, 180vw",
|
||||
sizes_large: "(min-width: 1200px) 1200px, 1200vw"
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
{
|
||||
"includes": [],
|
||||
"variables": {
|
||||
"PRODUCT_ROOT": "${root}/${product}/",
|
||||
"abs_url": "https://assets.osr-plastic.org",
|
||||
"CACHE": "${root}/cache/",
|
||||
"CACHE_URL": "${abs_url}/cache/",
|
||||
"GIT_REPO": "https://git.polymech.io/",
|
||||
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org",
|
||||
"PRODUCTS_ASSETS_URL":"https://assets.osr-plastic.org/${product_rel}",
|
||||
"OSR_FILES_WEB":"https://files.polymech.io/files/machines",
|
||||
"PRODUCTS_FILES_URL":"${OSR_FILES_WEB}/${product_rel}",
|
||||
"DISCORD":"https://discord.gg/s8K7yKwBRc"
|
||||
},
|
||||
"env": {
|
||||
"astro-release":{
|
||||
"includes": [
|
||||
"${PRODUCT_ROOT}"
|
||||
],
|
||||
"variables": {
|
||||
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org/"
|
||||
}
|
||||
},
|
||||
"astro-debug":{
|
||||
"includes": [
|
||||
"${PRODUCT_ROOT}"
|
||||
],
|
||||
"variables": {
|
||||
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org",
|
||||
"showCart": false,
|
||||
"showPrice": false,
|
||||
"showResources": false,
|
||||
"showShipping": false,
|
||||
"showPaymentTerms": false,
|
||||
"showHowtos": false,
|
||||
"showRenderings": true,
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"main": [
|
||||
{
|
||||
"name": "facebook",
|
||||
"icon": "FaFacebookF",
|
||||
"link": "https://www.facebook.com/themefisher"
|
||||
},
|
||||
{
|
||||
"name": "twitter",
|
||||
"icon": "FaXTwitter",
|
||||
"link": "https://x.com/themefisher"
|
||||
},
|
||||
{
|
||||
"name": "linkedin",
|
||||
"icon": "FaLinkedinIn",
|
||||
"link": "https://bd.linkedin.com/company/themefisher"
|
||||
},
|
||||
{
|
||||
"name": "github",
|
||||
"icon": "FaGithub",
|
||||
"link": "https://github.com/themefisher/astrofront"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"shop":{
|
||||
"title": "Shop",
|
||||
"description": "",
|
||||
"items":"${OSR_ROOT}/products/products/**/config.json",
|
||||
"root":"${OSR_ROOT}/products"
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"colors": {
|
||||
"default": {
|
||||
"theme_color": {
|
||||
"primary": "#121212",
|
||||
"body": "#fff",
|
||||
"border": "#eaeaea",
|
||||
"theme_light": "#f2f2f2",
|
||||
"theme_dark": "#000"
|
||||
},
|
||||
"text_color": {
|
||||
"default": "#444",
|
||||
"dark": "#000",
|
||||
"light": "#666"
|
||||
}
|
||||
},
|
||||
"darkmode": {
|
||||
"theme_color": {
|
||||
"primary": "#fff",
|
||||
"body": "#252525",
|
||||
"border": "#3E3E3E",
|
||||
"theme_light": "#222222",
|
||||
"theme_dark": "#000"
|
||||
},
|
||||
"text_color": {
|
||||
"default": "#DDD",
|
||||
"dark": "#fff",
|
||||
"light": "#DDD"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"font_family": {
|
||||
"primary": "Karla:wght@400;500;700",
|
||||
"primary_type": "sans-serif",
|
||||
"secondary": "",
|
||||
"secondary_type": ""
|
||||
},
|
||||
"font_size": {
|
||||
"base": "16",
|
||||
"scale": "1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ import {
|
||||
I18N_SOURCE_LANGUAGE,
|
||||
PRODUCT_SPECS,
|
||||
RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS
|
||||
} from '@/app/config.js'
|
||||
} from 'config/config.js'
|
||||
|
||||
import { translateXLS } from '@polymech/i18n/translate_xls'
|
||||
import { I18N_STORE, OSR_ROOT } from 'config/config.js'
|
||||
|
||||
@ -13,14 +13,23 @@ import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as mv } from '@polymech/fs/move'
|
||||
import { logger } from '@/base/index.js'
|
||||
|
||||
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
|
||||
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL, ASSETS_LOCAL } from '../app/config.js'
|
||||
import {
|
||||
removeArrayValues,
|
||||
removeArrays,
|
||||
removeBufferValues,
|
||||
removeEmptyObjects
|
||||
} from '@/base/objects.js'
|
||||
|
||||
import {
|
||||
ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT,
|
||||
DEFAULT_IMAGE_URL, ASSETS_LOCAL,
|
||||
ASSETS_GLOB
|
||||
} from 'config/config.js'
|
||||
import { GalleryImage, MetaJSON } from './images.js'
|
||||
|
||||
import { validateFilename, sanitizeFilename } from "@polymech/fs/utils"
|
||||
|
||||
import { env } from './index.js'
|
||||
const IMAGES_GLOB = '*.+(JPG|jpg|jpeg|png|PNG|gif)'
|
||||
|
||||
|
||||
export const default_sort = (files: string[]): string[] => {
|
||||
@ -80,7 +89,7 @@ export const default_filter_locale = async (url: string) => {
|
||||
}
|
||||
|
||||
export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
|
||||
if(exists(src)) return src
|
||||
if (exists(src)) return src
|
||||
let safeSrc = src
|
||||
try {
|
||||
const response = await fetch(src, { method: 'HEAD' })
|
||||
@ -112,7 +121,7 @@ export const gallery = async (
|
||||
logger.warn(`item gallery : item ${product} media path not found ${mediaPath}!`)
|
||||
return []
|
||||
}
|
||||
const galleryGlob = (productConfig.gallery || {})[assetSlug]?.glob || IMAGES_GLOB
|
||||
const galleryGlob = (productConfig.gallery || {})[assetSlug]?.glob || ASSETS_GLOB
|
||||
let galleryFiles: any[] = files(mediaPath, galleryGlob, {
|
||||
cwd: mediaPath,
|
||||
absolute: false,
|
||||
|
||||
@ -6,7 +6,7 @@ import { 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" }
|
||||
import config from "../config/config.json" with { "type": "json" }
|
||||
|
||||
const keywords = (keywords: string) => keywords.split(',').map(k => k.trim()).filter(Boolean);
|
||||
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
git checkout --orphan latest_branch
|
||||
git add -A
|
||||
git commit -am "init v0.1.3"
|
||||
git branch -D master
|
||||
git branch -m master
|
||||
git push -f origin master
|
||||
git gc --aggressive --prune=all
|
||||
@ -3,7 +3,7 @@ import "../styles/flowbite.css"
|
||||
|
||||
import { LANGUAGES_PROD } from "config/config.js"
|
||||
|
||||
import config from "config/config.json"
|
||||
import config from "@/config/config.json"
|
||||
import { plainify } from "../base/strings.js"
|
||||
|
||||
import "../styles/global.css"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
const { title, url, pubDate, description, image } = Astro.props;
|
||||
import { DEFAULT_IMAGE_URL } from "@/app/config";
|
||||
import { DEFAULT_IMAGE_URL } from "@/config/config";
|
||||
import { Img } from "imagetools/components";
|
||||
---
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
import Wrapper from "@/components/containers/Wrapper.astro"
|
||||
import { footer_left, footer_right } from "@/app/navigation.js"
|
||||
import { footer_left, footer_right } from "@/config/navigation.js"
|
||||
import { ISO_LANGUAGE_LABELS } from "@polymech/i18n"
|
||||
import {
|
||||
LANGUAGES_PROD,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
import { I18N_SOURCE_LANGUAGE } from "@/app/config";
|
||||
import { items } from "config/navigation.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "@/config/config";
|
||||
import { items } from "@/config/navigation.js";
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE;
|
||||
const navItems = await items({ locale });
|
||||
---
|
||||
|
||||
@ -1,43 +1,43 @@
|
||||
---
|
||||
import config from "@/app/config.json"
|
||||
import { get } from "@/model/registry.js"
|
||||
import { default_image } from "@/app/config.js"
|
||||
|
||||
const { frontmatter } = Astro.props
|
||||
const title = frontmatter?.title || config.site.title
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site)
|
||||
const image = frontmatter ? frontmatter.image || default_image() : default_image()
|
||||
|
||||
const itemData = frontmatter ? await get("json-ld", {} as any, frontmatter,
|
||||
{
|
||||
url: pageUrl.href,
|
||||
}) : null
|
||||
|
||||
const meta = config.metadata || { }
|
||||
|
||||
let data = itemData || {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: title,
|
||||
description: frontmatter?.description || meta.description,
|
||||
//datePublished: createdAt?.toISOString(),
|
||||
//dateModified: updatedAt?.toISOString() ?? undefined,
|
||||
url: pageUrl,
|
||||
thumbnailUrl: image,
|
||||
image: {
|
||||
"@type": "ImageObject",
|
||||
url: image.src || "",
|
||||
caption: image.alt,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
},
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: meta.author,
|
||||
description: meta.author_bio,
|
||||
url: meta.author_url
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(data, null, 2)} />
|
||||
---
|
||||
import config from "@/config/config.json"
|
||||
import { get } from "@/model/registry.js"
|
||||
import { default_image } from "config/config.js"
|
||||
|
||||
const { frontmatter } = Astro.props
|
||||
const title = frontmatter?.title || config.site.title
|
||||
const pageUrl = new URL(Astro.url.pathname, Astro.site)
|
||||
const image = frontmatter ? frontmatter.image || default_image() : default_image()
|
||||
|
||||
const itemData = frontmatter ? await get("json-ld", {} as any, frontmatter,
|
||||
{
|
||||
url: pageUrl.href,
|
||||
}) : null
|
||||
|
||||
const meta = config.metadata || { }
|
||||
|
||||
let data = itemData || {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: title,
|
||||
description: frontmatter?.description || meta.description,
|
||||
//datePublished: createdAt?.toISOString(),
|
||||
//dateModified: updatedAt?.toISOString() ?? undefined,
|
||||
url: pageUrl,
|
||||
thumbnailUrl: image,
|
||||
image: {
|
||||
"@type": "ImageObject",
|
||||
url: image.src || "",
|
||||
caption: image.alt,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
},
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: meta.author,
|
||||
description: meta.author_bio,
|
||||
url: meta.author_url
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(data, null, 2)} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { DEFAULT_IMAGE_URL } from '@/app/config.js'
|
||||
import { DEFAULT_IMAGE_URL } from 'config/config.js'
|
||||
import { Picture } from "imagetools/components"
|
||||
import { image_url } from "@/base/media.js"
|
||||
|
||||
|
||||
@ -1,65 +1,65 @@
|
||||
---
|
||||
import { translate } from '@/base/i18n.js';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
|
||||
// A simple slugify helper (you could import this from a utility file instead)
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.normalize('NFD') // Normalise accented characters
|
||||
.replace(/[\u0300-\u036f]/g, '') // Strip accents
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
clazz?: string;
|
||||
src?: string; // optional
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'title',
|
||||
clazz = 'unstyled',
|
||||
src,
|
||||
} = Astro.props;
|
||||
|
||||
// Compute the slug from the original title
|
||||
const id = slugify(title);
|
||||
|
||||
// Then retrieve the user's locale and translate the title
|
||||
const locale = Astro.currentLocale;
|
||||
const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
|
||||
---
|
||||
|
||||
<button
|
||||
id={id}
|
||||
slot="tab"
|
||||
class="inline-flex whitespace-nowrap border-b-2 border-transparent py-2 px-3 text-sm font-medium text-gray-300 transition-all duration-200 ease-in-out hover:border-b-[#9249ed] hover:text-[#9249ed] aria-selected:border-b-[#9249ed] aria-selected:text-[#9249ed]"
|
||||
>{title_i18n}</button>
|
||||
|
||||
<div slot="panel" class={clazz}>
|
||||
{src ? (
|
||||
<div class="iframe-container">
|
||||
<iframe src={src}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<slot />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 ratio; adjust if needed */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
---
|
||||
import { translate } from '@/base/i18n.js';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
|
||||
// A simple slugify helper (you could import this from a utility file instead)
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.normalize('NFD') // Normalise accented characters
|
||||
.replace(/[\u0300-\u036f]/g, '') // Strip accents
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
clazz?: string;
|
||||
src?: string; // optional
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'title',
|
||||
clazz = 'unstyled',
|
||||
src,
|
||||
} = Astro.props;
|
||||
|
||||
// Compute the slug from the original title
|
||||
const id = slugify(title);
|
||||
|
||||
// Then retrieve the user's locale and translate the title
|
||||
const locale = Astro.currentLocale;
|
||||
const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
|
||||
---
|
||||
|
||||
<button
|
||||
id={id}
|
||||
slot="tab"
|
||||
class="inline-flex whitespace-nowrap border-b-2 border-transparent py-2 px-3 text-sm font-medium text-gray-300 transition-all duration-200 ease-in-out hover:border-b-[#9249ed] hover:text-[#9249ed] aria-selected:border-b-[#9249ed] aria-selected:text-[#9249ed]"
|
||||
>{title_i18n}</button>
|
||||
|
||||
<div slot="panel" class={clazz}>
|
||||
{src ? (
|
||||
<div class="iframe-container">
|
||||
<iframe src={src}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<slot />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 ratio; adjust if needed */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,65 +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
|
||||
};
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { default_image } from "@/app/config.js";
|
||||
import { default_image } from "config/config.js";
|
||||
import Translate from "@/components/polymech/i18n.astro";
|
||||
import { Img } from "imagetools/components"
|
||||
const { title, url, price, model, selected = false } = Astro.props;
|
||||
|
||||
@ -4,7 +4,7 @@ description: Contact Plastic Hub
|
||||
layout: '@layouts/Markup.astro'
|
||||
---
|
||||
|
||||
import { default_image } from '@/app/config.js'
|
||||
import { default_image } from 'config/config.js'
|
||||
import GalleryK from "@/components/polymech/GalleryK.astro"
|
||||
|
||||
## {frontmatter.title}
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
import * as path from 'path'
|
||||
import { findUp } from 'find-up'
|
||||
import { } from 'astro:content'
|
||||
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
import { validateFilename, validatePath, sanitizeFilename } from '@polymech/fs/utils'
|
||||
|
||||
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
|
||||
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
|
||||
|
||||
import { ContentEntryRenderFunction, ContentEntryType } from 'astro'
|
||||
import { RenderedContent, DataEntry } from "astro:content"
|
||||
|
||||
import type { Loader, LoaderContext } from 'astro/loaders'
|
||||
|
||||
import {
|
||||
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
|
||||
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
|
||||
PRODUCT_ROOT, RETAIL_PRODUCT_BRANCH, CAD_EXPORT_CONFIGURATIONS,
|
||||
PRODUCT_ROOT, CAD_EXPORT_CONFIGURATIONS,
|
||||
CAD_DEFAULT_CONFIGURATION,
|
||||
CAD_URL,
|
||||
parseBoolean,
|
||||
DEFAULT_CONTACT
|
||||
DEFAULT_CONTACT,
|
||||
default_image
|
||||
} from 'config/config.js'
|
||||
|
||||
import { env } from '../base/index.js'
|
||||
import { gallery } from '../base/media.js';
|
||||
import { env } from '@/base/index.js'
|
||||
import { gallery } from '@/base/media.js';
|
||||
|
||||
import { get } from '@polymech/commons/component'
|
||||
import { PFilterValid } from '@polymech/commons/filter'
|
||||
@ -34,9 +35,6 @@ import { translate } from "@/base/i18n.js"
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
|
||||
import { slugify } from "@/base/strings.js"
|
||||
|
||||
export interface ILoaderContextEx extends LoaderContext {
|
||||
entryTypes: Map<string, ContentEntryType>
|
||||
}
|
||||
export interface IComponentConfigEx extends IComponentConfig {
|
||||
content: string
|
||||
extra_resources?: string
|
||||
@ -49,10 +47,6 @@ export interface IStoreItem extends DataEntry {
|
||||
rendered?: RenderedContent
|
||||
}
|
||||
|
||||
let loaderCtx: ILoaderContextEx
|
||||
|
||||
const renderFunctionByContentType = new WeakMap<ContentEntryType, ContentEntryRenderFunction>();
|
||||
|
||||
const filterBranch = (items: { rel: string, config, path }[], branch: string) => {
|
||||
if (!PRODUCT_BRANCHES) {
|
||||
return items
|
||||
@ -69,7 +63,7 @@ export const items = (branch: string) =>
|
||||
PRODUCT_ROOT(), PFilterValid.marketplace_component), branch)
|
||||
|
||||
|
||||
const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
const onComponent = async (item: IStoreItem, ctx: LoaderContext) => {
|
||||
/*
|
||||
const onNode = async (data: INodeCallback, configuration: string) => {
|
||||
if (!CAD_EXPORT_SUB_COMPONENTS || !data.target.endsWith('.json')) {
|
||||
@ -93,7 +87,7 @@ const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
*/
|
||||
}
|
||||
|
||||
const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSchema[]> => {
|
||||
const cad = async (item: IStoreItem): Promise<ICADNodeSchema[]> => {
|
||||
const root = PRODUCT_ROOT()
|
||||
const data: IComponentConfigEx = item.data
|
||||
const itemRel = data.rel
|
||||
@ -137,33 +131,24 @@ const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSch
|
||||
return cadMeta
|
||||
}
|
||||
|
||||
|
||||
|
||||
const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
const onItem = async (item: IStoreItem, ctx: LoaderContext) => {
|
||||
if (!item || !item.data) {
|
||||
ctx.logger.error(`Error completing ${''}: no data`);
|
||||
return
|
||||
}
|
||||
if (!loaderCtx) {
|
||||
loaderCtx = ctx
|
||||
}
|
||||
const { logger } = ctx
|
||||
let data: IComponentConfigEx = item.data
|
||||
|
||||
const itemRel = data.rel
|
||||
const itemRelMin = itemRel.replace('products/', '')
|
||||
const itemDir = PRODUCT_DIR(itemRel)
|
||||
const default_profile = env(itemRel)
|
||||
|
||||
data.product_rel = itemRelMin
|
||||
data.assets = {
|
||||
renderings: [],
|
||||
gallery: []
|
||||
}
|
||||
|
||||
data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || ""
|
||||
data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || ""
|
||||
|
||||
//////////////////////////////////////////
|
||||
//
|
||||
// Item Shared Resources
|
||||
@ -207,18 +192,16 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
item.data = data
|
||||
//////////////////////////////////////////
|
||||
//
|
||||
// Extensions, CAD, Media, etc.
|
||||
// Extensions, CAD, Media, etcetera etcetera :)
|
||||
//
|
||||
data.cad = await cad(item, ctx)
|
||||
|
||||
data.assets.renderings = await gallery('renderings', data.rel) as []
|
||||
data.assets.gallery = await gallery('media/gallery', data.rel) as []
|
||||
data.image = data.assets.renderings[0] || {}
|
||||
data.assets.showcase = await gallery('media/showcase', data.rel) as []
|
||||
data.assets.samples = await gallery('media/samples', data.rel) as []
|
||||
|
||||
data.checkout = await item_checkout(data)
|
||||
data.cad = await cad(item)
|
||||
|
||||
data.assets.renderings = await gallery('renderings', data.rel)
|
||||
data.assets.gallery = await gallery('media/gallery', data.rel)
|
||||
data.image = data.assets.renderings[0] || default_image()
|
||||
data.assets.showcase = await gallery('media/showcase', data.rel)
|
||||
data.assets.samples = await gallery('media/samples', data.rel)
|
||||
data.checkout = await item_checkout(data)
|
||||
}
|
||||
|
||||
export function loader(branch: string): Loader {
|
||||
@ -228,8 +211,7 @@ export function loader(branch: string): Loader {
|
||||
watcher,
|
||||
parseData,
|
||||
store,
|
||||
generateDigest,
|
||||
entryTypes }: ILoaderContextEx) => {
|
||||
generateDigest }: LoaderContext) => {
|
||||
|
||||
store.clear();
|
||||
let products = items(branch)
|
||||
@ -259,14 +241,10 @@ export function loader(branch: string): Loader {
|
||||
watcher,
|
||||
parseData,
|
||||
store,
|
||||
generateDigest,
|
||||
entryTypes
|
||||
generateDigest
|
||||
} as any)
|
||||
|
||||
storeItem.data['config'] = JSON.stringify({
|
||||
...storeItem.data
|
||||
}, null, 2)
|
||||
|
||||
storeItem.data['config'] = JSON.stringify(storeItem.data, null, 2)
|
||||
store.set(storeItem)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { IComponentNode, IComponentConfig } from '@polymech/commons'
|
||||
import config from "@/app/config.json" with { type: "json" }
|
||||
import config from "@/config/config.json" with { type: "json" }
|
||||
|
||||
interface ShippingDetails {
|
||||
"@type": "OfferShippingDetails";
|
||||
|
||||
@ -3,7 +3,7 @@ import type { IComponentNode, IComponentConfig } from '@polymech/commons'
|
||||
// https://developers.google.com/search/docs/appearance/structured-data/merchant-listing
|
||||
// https://search.google.com/test/rich-results/result/r%2Fmerchant-listings?id=zK-WH_1fFMHIUY3yHqHj6Q
|
||||
// import tag from "@types/google-publisher-tag"
|
||||
import config from "@/app/config.json" with { type: "json" }
|
||||
import config from "@/config/config.json" with { type: "json" }
|
||||
interface GoogleMerchantProduct {
|
||||
id: string
|
||||
title: string
|
||||
|
||||
@ -3,7 +3,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
import { LANGUAGES_PROD } from "config/config.js";
|
||||
import config from "config/config.json";
|
||||
import config from "@/config/config.json";
|
||||
import { getCollection } from "astro:content";
|
||||
import StoreEntries from "@/components/store/StoreEntries.astro";
|
||||
import CtaOne from "@/components/cta/CtaOne.astro";
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
{
|
||||
"debug": true,
|
||||
"matching": [
|
||||
"src/**",
|
||||
"*.md",
|
||||
"*.json",
|
||||
"*.cjs",
|
||||
"docs",
|
||||
"packages/imagetools/**",
|
||||
"scripts",
|
||||
"meta/**",
|
||||
"tests/**",
|
||||
"*.mjs",
|
||||
"src/",
|
||||
"!node_modules/**",
|
||||
"!ref/**"
|
||||
]
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"config/*": ["src/app/*"]
|
||||
"config/*": ["src/config/*"]
|
||||
}
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro", "src/content/resources/software-as.mdx"],
|
||||
|
||||
Reference in New Issue
Block a user