chore: home page and site config seeding scripts
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
"graphql:schema": "graphql-codegen -p introspect",
|
||||
"endpoint:generate": "node scripts/endpoint-generate.mjs",
|
||||
"seed:categories": "node scripts/seed-strapi-categories.mjs",
|
||||
"seed:vendors": "node scripts/seed-strapi-vendors.mjs"
|
||||
"seed:vendors": "node scripts/seed-strapi-vendors.mjs",
|
||||
"seed:home-page": "node scripts/seed-strapi-home-page.mjs",
|
||||
"seed:site-config": "node scripts/seed-strapi-site-config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.5",
|
||||
|
||||
241
scripts/seed-strapi-home-page.mjs
Normal file
241
scripts/seed-strapi-home-page.mjs
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const sourceFile = path.join(root, "src", "content", "home-page.json");
|
||||
|
||||
const defaultEndpoint = "http://localhost:1337/graphql";
|
||||
const endpoint =
|
||||
process.env.STRAPI_GRAPHQL_URL ??
|
||||
process.env.PUBLIC_GRAPHQL_URL ??
|
||||
defaultEndpoint;
|
||||
|
||||
const token =
|
||||
process.env.STRAPI_TOKEN ??
|
||||
process.env.STRAPI_API_TOKEN ??
|
||||
process.env.AUTH_TOKEN;
|
||||
const isDryRun = process.argv.includes("--dry-run");
|
||||
|
||||
function readSource() {
|
||||
if (!fs.existsSync(sourceFile)) {
|
||||
throw new Error(`Missing source file: ${path.relative(root, sourceFile)}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(sourceFile, "utf8"));
|
||||
}
|
||||
|
||||
// Drop null/undefined keys so optional Strapi fields (e.g. anchor) are omitted
|
||||
// rather than sent as explicit nulls.
|
||||
function compact(object) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object).filter(([, value]) => value !== null && value !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
// Maps a source section ("type") to its Strapi dynamic-zone component payload.
|
||||
// resolveCategory turns a category slug into the documentId Strapi expects for
|
||||
// the card -> category relation.
|
||||
const SECTION_BUILDERS = {
|
||||
hero: (section) =>
|
||||
compact({
|
||||
__component: "sections.home-hero",
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
anchor: section.anchor,
|
||||
badge: section.badge,
|
||||
stats: (section.stats ?? []).map((stat) => ({
|
||||
key: stat.key,
|
||||
value: stat.value,
|
||||
})),
|
||||
}),
|
||||
services: (section, resolveCategory) =>
|
||||
compact({
|
||||
__component: "sections.services",
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
anchor: section.anchor,
|
||||
cards: (section.cards ?? []).map((card) =>
|
||||
compact({
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
icon: { value: card.icon },
|
||||
color: { value: card.color },
|
||||
category: resolveCategory(card.category),
|
||||
})
|
||||
),
|
||||
}),
|
||||
solutions: (section, resolveCategory) =>
|
||||
compact({
|
||||
__component: "sections.solutions",
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
anchor: section.anchor,
|
||||
cards: (section.cards ?? []).map((card) =>
|
||||
compact({
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
footnote: card.footnote,
|
||||
icon: { value: card.icon },
|
||||
category: resolveCategory(card.category),
|
||||
})
|
||||
),
|
||||
}),
|
||||
cases: (section) =>
|
||||
compact({
|
||||
__component: "sections.cases",
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
anchor: section.anchor,
|
||||
cards: (section.cards ?? []).map((card) =>
|
||||
compact({
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
pretitle: card.pretitle,
|
||||
badge: card.badge,
|
||||
footnote: card.footnote,
|
||||
color: { value: card.color },
|
||||
icon: { value: card.icon },
|
||||
})
|
||||
),
|
||||
}),
|
||||
about: (section) =>
|
||||
compact({
|
||||
__component: "sections.about",
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
anchor: section.anchor,
|
||||
partners: (section.partners ?? []).map((value) => ({ value })),
|
||||
cards: (section.cards ?? []).map((card) =>
|
||||
compact({
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
color: { value: card.color },
|
||||
icon: { value: card.icon },
|
||||
})
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
async function gql(query, variables = {}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/graphql-response+json, application/json",
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GraphQL HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload.errors?.length) {
|
||||
const messages = payload.errors.map((err) => err.message).join("; ");
|
||||
throw new Error(`GraphQL error: ${messages}`);
|
||||
}
|
||||
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
async function fetchCategoryDocumentIds() {
|
||||
const query = `
|
||||
query CategoryIds {
|
||||
categories(pagination: { limit: 500 }) {
|
||||
documentId
|
||||
slug
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await gql(query);
|
||||
const map = new Map();
|
||||
for (const category of data?.categories ?? []) {
|
||||
if (!category?.slug || !category?.documentId) continue;
|
||||
map.set(category.slug, category.documentId);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildSections(source, categoryIdBySlug) {
|
||||
const sections = Array.isArray(source.sections) ? source.sections : [];
|
||||
|
||||
const resolveCategory = (slug) => {
|
||||
const documentId = categoryIdBySlug.get(slug);
|
||||
if (!documentId) {
|
||||
throw new Error(
|
||||
`Category "${slug}" is missing in Strapi. Seed categories before seeding the home page.`
|
||||
);
|
||||
}
|
||||
return documentId;
|
||||
};
|
||||
|
||||
return sections.map((section) => {
|
||||
const build = SECTION_BUILDERS[section.type];
|
||||
if (!build) {
|
||||
throw new Error(`Unknown home-page section type "${String(section.type)}".`);
|
||||
}
|
||||
return build(section, resolveCategory);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateHomePage(sections) {
|
||||
const mutation = `
|
||||
mutation UpdateHomePage($data: HomePageInput!) {
|
||||
updateHomePage(data: $data) {
|
||||
sections {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await gql(mutation, { data: { sections } });
|
||||
return data?.updateHomePage;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Seeding home page to ${endpoint}${isDryRun ? " (dry-run)" : ""}`);
|
||||
if (!token) {
|
||||
console.warn("No STRAPI token provided. If endpoint is protected, set STRAPI_TOKEN.");
|
||||
}
|
||||
|
||||
const source = readSource();
|
||||
|
||||
if (isDryRun && process.argv.includes("--offline")) {
|
||||
// Allow inspecting the payload shape without touching Strapi.
|
||||
const sections = buildSections(source, new Map(
|
||||
(source.sections ?? [])
|
||||
.flatMap((section) => section.cards ?? [])
|
||||
.map((card) => card.category)
|
||||
.filter(Boolean)
|
||||
.map((slug) => [slug, `dryrun-${slug}`])
|
||||
));
|
||||
console.log(JSON.stringify(sections, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryIdBySlug = await fetchCategoryDocumentIds();
|
||||
const sections = buildSections(source, categoryIdBySlug);
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(JSON.stringify(sections, null, 2));
|
||||
console.log(`DRY-RUN UPDATE homePage (${sections.length} sections)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await updateHomePage(sections);
|
||||
console.log(`UPDATE homePage (${updated.sections.length} sections)`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
115
scripts/seed-strapi-site-config.mjs
Normal file
115
scripts/seed-strapi-site-config.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const sourceFile = path.join(root, "src", "content", "site-config.json");
|
||||
|
||||
const defaultEndpoint = "http://localhost:1337/graphql";
|
||||
const endpoint =
|
||||
process.env.STRAPI_GRAPHQL_URL ??
|
||||
process.env.PUBLIC_GRAPHQL_URL ??
|
||||
defaultEndpoint;
|
||||
|
||||
const token =
|
||||
process.env.STRAPI_TOKEN ??
|
||||
process.env.STRAPI_API_TOKEN ??
|
||||
process.env.AUTH_TOKEN;
|
||||
const isDryRun = process.argv.includes("--dry-run");
|
||||
|
||||
function readSource() {
|
||||
if (!fs.existsSync(sourceFile)) {
|
||||
throw new Error(`Missing source file: ${path.relative(root, sourceFile)}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(sourceFile, "utf8"));
|
||||
}
|
||||
|
||||
function buildInput(source) {
|
||||
const contacts = Array.isArray(source.contacts) ? source.contacts : [];
|
||||
const input = {
|
||||
title: source.title,
|
||||
description: source.description ?? null,
|
||||
contacts: contacts.map((contact) => ({
|
||||
type: contact.type,
|
||||
value: contact.value,
|
||||
displayValue: contact.displayValue,
|
||||
})),
|
||||
};
|
||||
// privacyPolicy / termsOfUse are relations (single types) referenced by id.
|
||||
// They are seeded separately; only forward them when an id is provided.
|
||||
if (source.privacyPolicy?.id) input.privacyPolicy = source.privacyPolicy.id;
|
||||
if (source.termsOfUse?.id) input.termsOfUse = source.termsOfUse.id;
|
||||
return input;
|
||||
}
|
||||
|
||||
async function gql(query, variables = {}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/graphql-response+json, application/json",
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GraphQL HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload.errors?.length) {
|
||||
const messages = payload.errors.map((err) => err.message).join("; ");
|
||||
throw new Error(`GraphQL error: ${messages}`);
|
||||
}
|
||||
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
async function updateSiteConfig(input) {
|
||||
const mutation = `
|
||||
mutation UpdateSiteConfig($data: SiteConfigInput!) {
|
||||
updateSiteConfig(data: $data) {
|
||||
title
|
||||
contacts {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await gql(mutation, { data: input });
|
||||
return data?.updateSiteConfig;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Seeding site config to ${endpoint}${isDryRun ? " (dry-run)" : ""}`);
|
||||
if (!token) {
|
||||
console.warn("No STRAPI token provided. If endpoint is protected, set STRAPI_TOKEN.");
|
||||
}
|
||||
|
||||
const source = readSource();
|
||||
const input = buildInput(source);
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(JSON.stringify(input, null, 2));
|
||||
console.log(`DRY-RUN UPDATE siteConfig (${input.contacts.length} contacts)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await updateSiteConfig(input);
|
||||
console.log(
|
||||
`UPDATE siteConfig -> ${updated.title} (${updated.contacts.length} contacts)`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,5 +1,36 @@
|
||||
import { defineCollection, z } from 'astro:content'
|
||||
import { glob } from 'astro/loaders'
|
||||
import { file, glob } from 'astro/loaders'
|
||||
|
||||
const iconValue = z.enum([
|
||||
'cloud',
|
||||
'shield',
|
||||
'cpu',
|
||||
'database',
|
||||
'server',
|
||||
'settings',
|
||||
'building',
|
||||
'shopping_cart',
|
||||
'factory',
|
||||
'heart',
|
||||
'graduation_cap',
|
||||
'landmark',
|
||||
'rocket',
|
||||
'zap',
|
||||
'award',
|
||||
'users',
|
||||
'check_circle',
|
||||
'package',
|
||||
])
|
||||
|
||||
const colorValue = z.enum([
|
||||
'blue',
|
||||
'violet',
|
||||
'emerald',
|
||||
'orange',
|
||||
'pink',
|
||||
'indigo',
|
||||
'cyan',
|
||||
])
|
||||
|
||||
const vendorSchema = z.object({
|
||||
name: z.string(),
|
||||
@@ -18,4 +49,115 @@ const categories = defineCollection({
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { categories }
|
||||
const heroSection = z.object({
|
||||
type: z.literal('hero'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
anchor: z.string().nullable(),
|
||||
badge: z.string(),
|
||||
stats: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||
})
|
||||
|
||||
const servicesSection = z.object({
|
||||
type: z.literal('services'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
anchor: z.string().nullable(),
|
||||
cards: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
icon: iconValue,
|
||||
color: colorValue,
|
||||
category: z.string(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const solutionsSection = z.object({
|
||||
type: z.literal('solutions'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
anchor: z.string().nullable(),
|
||||
cards: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
footnote: z.string(),
|
||||
icon: iconValue,
|
||||
category: z.string(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const casesSection = z.object({
|
||||
type: z.literal('cases'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
anchor: z.string().nullable(),
|
||||
cards: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
pretitle: z.string(),
|
||||
badge: z.string(),
|
||||
footnote: z.string(),
|
||||
color: colorValue,
|
||||
icon: iconValue,
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const aboutSection = z.object({
|
||||
type: z.literal('about'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
anchor: z.string().nullable(),
|
||||
partners: z.array(z.string()),
|
||||
cards: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
color: colorValue,
|
||||
icon: iconValue,
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const homePage = defineCollection({
|
||||
loader: file('./src/content/home-page.json', {
|
||||
parser: (text) => ({ 'home-page': JSON.parse(text) }),
|
||||
}),
|
||||
schema: z.object({
|
||||
sections: z.array(
|
||||
z.discriminatedUnion('type', [
|
||||
heroSection,
|
||||
servicesSection,
|
||||
solutionsSection,
|
||||
casesSection,
|
||||
aboutSection,
|
||||
]),
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
const contactSchema = z.object({
|
||||
type: z.enum(['phone', 'working_hours', 'email', 'address']),
|
||||
value: z.string(),
|
||||
displayValue: z.string(),
|
||||
})
|
||||
|
||||
const siteConfig = defineCollection({
|
||||
loader: file('./src/content/site-config.json', {
|
||||
parser: (text) => ({ 'site-config': JSON.parse(text) }),
|
||||
}),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().nullable(),
|
||||
contacts: z.array(contactSchema),
|
||||
privacyPolicy: z.object({ url: z.string() }).nullable(),
|
||||
termsOfUse: z.object({ url: z.string() }).nullable(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { categories, homePage, siteConfig }
|
||||
|
||||
196
src/content/home-page.json
Normal file
196
src/content/home-page.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"type": "hero",
|
||||
"title": "IT-решения для роста вашего бизнеса",
|
||||
"description": "Проектируем, внедряем и сопровождаем корпоративные IT-системы. Облачная инфраструктура, безопасность, автоматизация процессов.",
|
||||
"anchor": null,
|
||||
"badge": "Надежный партнер с 2021 года",
|
||||
"stats": [
|
||||
{ "key": "клиентов", "value": "300+" },
|
||||
{ "key": "партнерств", "value": "50+" },
|
||||
{ "key": "повторных обращений", "value": "95%" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "services",
|
||||
"title": "Комплексные IT-решения для вашего бизнеса",
|
||||
"description": "Полный цикл: от стратегии до технической поддержки",
|
||||
"anchor": null,
|
||||
"cards": [
|
||||
{
|
||||
"title": "Облачные решения",
|
||||
"description": "Миграция и управление инфраструктурой на базе Яндекс.Облако и VK Cloud",
|
||||
"icon": "cloud",
|
||||
"color": "blue",
|
||||
"category": "cloud"
|
||||
},
|
||||
{
|
||||
"title": "Кибербезопасность",
|
||||
"description": "Комплексная защита периметра и данных с решениями Kaspersky",
|
||||
"icon": "shield",
|
||||
"color": "violet",
|
||||
"category": "cybersecurity"
|
||||
},
|
||||
{
|
||||
"title": "Цифровая трансформация",
|
||||
"description": "Автоматизация процессов на платформах 1С и Галактика",
|
||||
"icon": "cpu",
|
||||
"color": "emerald",
|
||||
"category": "implementation"
|
||||
},
|
||||
{
|
||||
"title": "Управление данными",
|
||||
"description": "Развертывание и поддержка СУБД Postgres Pro",
|
||||
"icon": "database",
|
||||
"color": "orange",
|
||||
"category": "integration"
|
||||
},
|
||||
{
|
||||
"title": "Серверная инфраструктура",
|
||||
"description": "Поставка и настройка оборудования Kraftway",
|
||||
"icon": "server",
|
||||
"color": "pink",
|
||||
"category": "cloud"
|
||||
},
|
||||
{
|
||||
"title": "Системная интеграция",
|
||||
"description": "Построение корпоративной ИТ-архитектуры на Astra Linux",
|
||||
"icon": "settings",
|
||||
"color": "indigo",
|
||||
"category": "industrial-solutions"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "solutions",
|
||||
"title": "Решения под ключ для разных отраслей",
|
||||
"description": "Глубокое понимание специфики бизнеса в каждой отрасли",
|
||||
"anchor": null,
|
||||
"cards": [
|
||||
{
|
||||
"title": "Корпоративный сектор",
|
||||
"description": "ERP, CRM, автоматизация документооборота",
|
||||
"footnote": "1200+ проектов",
|
||||
"icon": "building",
|
||||
"category": "business"
|
||||
},
|
||||
{
|
||||
"title": "Ритейл и e-commerce",
|
||||
"description": "Омниканальные платформы продаж",
|
||||
"footnote": "850+ проектов",
|
||||
"icon": "shopping_cart",
|
||||
"category": "business"
|
||||
},
|
||||
{
|
||||
"title": "Производство",
|
||||
"description": "IoT, системы управления производством",
|
||||
"footnote": "650+ проектов",
|
||||
"icon": "factory",
|
||||
"category": "specialized-services"
|
||||
},
|
||||
{
|
||||
"title": "Здравоохранение",
|
||||
"description": "Медицинские информационные системы",
|
||||
"footnote": "420+ проектов",
|
||||
"icon": "heart",
|
||||
"category": "implementation"
|
||||
},
|
||||
{
|
||||
"title": "Образование",
|
||||
"description": "Платформы дистанционного обучения",
|
||||
"footnote": "380+ проектов",
|
||||
"icon": "graduation_cap",
|
||||
"category": "specialized-services"
|
||||
},
|
||||
{
|
||||
"title": "Государственный сектор",
|
||||
"description": "Цифровизация госуслуг",
|
||||
"footnote": "500+ проектов",
|
||||
"icon": "landmark",
|
||||
"category": "government"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "cases",
|
||||
"title": "Реализованные проекты",
|
||||
"description": "Примеры успешного внедрения с конкретными результатами",
|
||||
"anchor": null,
|
||||
"cards": [
|
||||
{
|
||||
"title": "Миграция в Яндекс.Облако",
|
||||
"description": "Перенос корпоративной инфраструктуры 500+ серверов с нулевым простоем",
|
||||
"pretitle": "Торговая сеть",
|
||||
"badge": "Облако",
|
||||
"footnote": "−40% TCO",
|
||||
"color": "blue",
|
||||
"icon": "cloud"
|
||||
},
|
||||
{
|
||||
"title": "Внедрение 1С:ERP",
|
||||
"description": "Автоматизация производственного и финансового учета для 15 предприятий",
|
||||
"pretitle": "Производственный холдинг",
|
||||
"badge": "Автоматизация",
|
||||
"footnote": "+25% эффективность",
|
||||
"color": "emerald",
|
||||
"icon": "cpu"
|
||||
},
|
||||
{
|
||||
"title": "Защита периметра Kaspersky",
|
||||
"description": "Построение многоуровневой системы защиты критичной инфраструктуры",
|
||||
"pretitle": "Финансовая группа",
|
||||
"badge": "Безопасность",
|
||||
"footnote": "99.9% uptime",
|
||||
"color": "violet",
|
||||
"icon": "shield"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "about",
|
||||
"title": "Сотрудничаем с",
|
||||
"description": "Комплексные решения для цифровой трансформации бизнеса. Работаем с 2021 года.",
|
||||
"anchor": null,
|
||||
"partners": ["Видеомост", "Jazz Telecom", "СКБ Контур"],
|
||||
"cards": [
|
||||
{
|
||||
"title": "Поставка оборудования",
|
||||
"description": "Комплексные поставки серверов, СХД и сетевого оборудования от российских производителей.",
|
||||
"color": "blue",
|
||||
"icon": "package"
|
||||
},
|
||||
{
|
||||
"title": "Официальное партнерство",
|
||||
"description": "Статус золотого партнера ведущих российских вендоров. Прямые контракты и поддержка.",
|
||||
"color": "emerald",
|
||||
"icon": "award"
|
||||
},
|
||||
{
|
||||
"title": "Сертифицированные специалисты",
|
||||
"description": "Инженеры с подтвержденной квалификацией по всем внедряемым решениям и платформам.",
|
||||
"color": "violet",
|
||||
"icon": "check_circle"
|
||||
},
|
||||
{
|
||||
"title": "Оперативный отклик",
|
||||
"description": "Быстрое реагирование на запросы клиента. Четкие сроки решения задач и прозрачная коммуникация.",
|
||||
"color": "orange",
|
||||
"icon": "zap"
|
||||
},
|
||||
{
|
||||
"title": "Выделенная команда",
|
||||
"description": "Персональный менеджер проекта и команда сертифицированных специалистов.",
|
||||
"color": "cyan",
|
||||
"icon": "users"
|
||||
},
|
||||
{
|
||||
"title": "Быстрый старт",
|
||||
"description": "Запуск типовых проектов за 2 недели. Пилотное внедрение перед полным развертываием.",
|
||||
"color": "pink",
|
||||
"icon": "rocket"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
28
src/content/site-config.json
Normal file
28
src/content/site-config.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"title": "СОФТКЛИК",
|
||||
"description": null,
|
||||
"contacts": [
|
||||
{
|
||||
"type": "phone",
|
||||
"value": "+74951234567",
|
||||
"displayValue": "+7 (495) 123-45-67"
|
||||
},
|
||||
{
|
||||
"type": "working_hours",
|
||||
"value": "Пн-Пт 9:00-18:00",
|
||||
"displayValue": "Пн-Пт 9:00-18:00"
|
||||
},
|
||||
{
|
||||
"type": "email",
|
||||
"value": "mailto:info@softclick.ru",
|
||||
"displayValue": "info@softclick.ru"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"value": "Москва, Пресненская наб., 12",
|
||||
"displayValue": "Москва, Пресненская наб., 12"
|
||||
}
|
||||
],
|
||||
"privacyPolicy": null,
|
||||
"termsOfUse": null
|
||||
}
|
||||
Reference in New Issue
Block a user