directus setup

This commit is contained in:
Sergey Yarkov 2025-11-16 21:09:06 +03:00
parent 8b26bc8bbc
commit a9061822f0
19 changed files with 10908 additions and 14399 deletions

View File

@ -1,3 +1,4 @@
REDIS_HOST=redis-1234.p124.eu-central-5-5.ec2.cloud.redislabs.com REDIS_HOST=redis-1234.p124.eu-central-5-5.ec2.cloud.redislabs.com
REDIS_PASSWORD=superpassword REDIS_PASSWORD=superpassword
REDIS_PORT=1234 REDIS_PORT=1234
DIRECTUS_TOKEN=

View File

@ -1,8 +1,9 @@
import { defineConfig } from "astro/config"; import { defineConfig, envField } from "astro/config";
/** /**
* Integrations * Integrations
*/ */
import node from "@astrojs/node";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import solidJs from "@astrojs/solid-js"; import solidJs from "@astrojs/solid-js";
import icon from "astro-icon"; import icon from "astro-icon";
@ -32,6 +33,15 @@ const createAnchorLabel = (heading) => {
export default defineConfig({ export default defineConfig({
site: "https://yarkov.tech", site: "https://yarkov.tech",
compressHTML: true, compressHTML: true,
output: "server",
adapter: node({
mode: "standalone",
}),
env: {
schema: {
DIRECTUS_TOKEN: envField.string({ context: "server", access: "secret", default: "" }),
},
},
integrations: [ integrations: [
sitemap({ sitemap({
i18n: { i18n: {

578
directus-schema.ts Normal file
View File

@ -0,0 +1,578 @@
export interface Article {
/** @primaryKey */
id: number;
tags?: ArticleTag[] | string[];
/** @required */
translations: ArticleTranslation[] | null;
}
export interface ArticleTag {
/** @primaryKey */
id: number;
article_id?: Article | string | null;
tag_id?: Tag | string | null;
}
export interface ArticleTranslation {
/** @primaryKey */
id: number;
article_id?: Article | string | null;
languages_code?: Language | string | null;
title?: string | null;
description?: string | null;
slug?: string | null;
content?: string | null;
status?: 'published' | 'draft' | 'archived' | null;
author?: DirectusUser | string | null;
pub_date?: string | null;
views?: number | null;
}
export interface Global {
/** @primaryKey */
id: number;
translations?: GlobalTranslation[] | null;
}
export interface GlobalTranslation {
/** @primaryKey */
id: number;
global_id?: Global | string | null;
languages_code?: Language | string | null;
description?: string | null;
}
export interface Language {
/** @primaryKey */
code: string;
name?: string | null;
direction?: 'ltr' | 'rtl' | null;
}
export interface Tag {
/** @primaryKey */
id: number;
/** @required */
title: string;
}
export interface DirectusAccess {
/** @primaryKey */
id: string;
role?: DirectusRole | string | null;
user?: DirectusUser | string | null;
policy?: DirectusPolicy | string;
sort?: number | null;
}
export interface DirectusActivity {
/** @primaryKey */
id: number;
action?: string;
user?: DirectusUser | string | null;
timestamp?: string;
ip?: string | null;
user_agent?: string | null;
collection?: string;
item?: string;
origin?: string | null;
revisions?: DirectusRevision[] | string[];
}
export interface DirectusCollection {
/** @primaryKey */
collection: string;
icon?: string | null;
note?: string | null;
display_template?: string | null;
hidden?: boolean;
singleton?: boolean;
translations?: Array<{ language: string; translation: string; singular: string; plural: string }> | null;
archive_field?: string | null;
archive_app_filter?: boolean;
archive_value?: string | null;
unarchive_value?: string | null;
sort_field?: string | null;
accountability?: 'all' | 'activity' | null | null;
color?: string | null;
item_duplication_fields?: 'json' | null;
sort?: number | null;
group?: DirectusCollection | string | null;
collapse?: string;
preview_url?: string | null;
versioning?: boolean;
}
export interface DirectusComment {
/** @primaryKey */
id: string;
collection?: DirectusCollection | string;
item?: string;
comment?: string;
date_created?: string | null;
date_updated?: string | null;
user_created?: DirectusUser | string | null;
user_updated?: DirectusUser | string | null;
}
export interface DirectusField {
/** @primaryKey */
id: number;
collection?: DirectusCollection | string;
field?: string;
special?: string[] | null;
interface?: string | null;
options?: 'json' | null;
display?: string | null;
display_options?: 'json' | null;
readonly?: boolean;
hidden?: boolean;
sort?: number | null;
width?: string | null;
translations?: 'json' | null;
note?: string | null;
conditions?: 'json' | null;
required?: boolean | null;
group?: DirectusField | string | null;
validation?: 'json' | null;
validation_message?: string | null;
searchable?: boolean;
}
export interface DirectusFile {
/** @primaryKey */
id: string;
storage?: string;
filename_disk?: string | null;
filename_download?: string;
title?: string | null;
type?: string | null;
folder?: DirectusFolder | string | null;
uploaded_by?: DirectusUser | string | null;
created_on?: string;
modified_by?: DirectusUser | string | null;
modified_on?: string;
charset?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
duration?: number | null;
embed?: string | null;
description?: string | null;
location?: string | null;
tags?: string[] | null;
metadata?: 'json' | null;
focal_point_x?: number | null;
focal_point_y?: number | null;
tus_id?: string | null;
tus_data?: 'json' | null;
uploaded_on?: string | null;
}
export interface DirectusFolder {
/** @primaryKey */
id: string;
name?: string;
parent?: DirectusFolder | string | null;
}
export interface DirectusMigration {
/** @primaryKey */
version: string;
name?: string;
timestamp?: string | null;
}
export interface DirectusPermission {
/** @primaryKey */
id: number;
collection?: string;
action?: string;
permissions?: 'json' | null;
validation?: 'json' | null;
presets?: 'json' | null;
fields?: string[] | null;
policy?: DirectusPolicy | string;
}
export interface DirectusPolicy {
/** @primaryKey */
id: string;
/** @required */
name: string;
icon?: string;
description?: string | null;
ip_access?: string[] | null;
enforce_tfa?: boolean;
admin_access?: boolean;
app_access?: boolean;
permissions?: DirectusPermission[] | string[];
users?: DirectusAccess[] | string[];
roles?: DirectusAccess[] | string[];
}
export interface DirectusPreset {
/** @primaryKey */
id: number;
bookmark?: string | null;
user?: DirectusUser | string | null;
role?: DirectusRole | string | null;
collection?: string | null;
search?: string | null;
layout?: string | null;
layout_query?: 'json' | null;
layout_options?: 'json' | null;
refresh_interval?: number | null;
filter?: 'json' | null;
icon?: string | null;
color?: string | null;
}
export interface DirectusRelation {
/** @primaryKey */
id: number;
many_collection?: string;
many_field?: string;
one_collection?: string | null;
one_field?: string | null;
one_collection_field?: string | null;
one_allowed_collections?: string[] | null;
junction_field?: string | null;
sort_field?: string | null;
one_deselect_action?: string;
}
export interface DirectusRevision {
/** @primaryKey */
id: number;
activity?: DirectusActivity | string;
collection?: string;
item?: string;
data?: 'json' | null;
delta?: 'json' | null;
parent?: DirectusRevision | string | null;
version?: DirectusVersion | string | null;
}
export interface DirectusRole {
/** @primaryKey */
id: string;
/** @required */
name: string;
icon?: string;
description?: string | null;
parent?: DirectusRole | string | null;
children?: DirectusRole[] | string[];
policies?: DirectusAccess[] | string[];
users?: DirectusUser[] | string[];
}
export interface DirectusSession {
/** @primaryKey */
token: string;
user?: DirectusUser | string | null;
expires?: string;
ip?: string | null;
user_agent?: string | null;
share?: DirectusShare | string | null;
origin?: string | null;
next_token?: string | null;
}
export interface DirectusSettings {
/** @primaryKey */
id: number;
project_name?: string;
project_url?: string | null;
project_color?: string;
project_logo?: DirectusFile | string | null;
public_foreground?: DirectusFile | string | null;
public_background?: DirectusFile | string | null;
public_note?: string | null;
auth_login_attempts?: number | null;
auth_password_policy?: null | `/^.{8,}$/` | `/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/` | null;
storage_asset_transform?: 'all' | 'none' | 'presets' | null;
storage_asset_presets?: Array<{ key: string; fit: 'contain' | 'cover' | 'inside' | 'outside'; width: number; height: number; quality: number; withoutEnlargement: boolean; format: 'auto' | 'jpeg' | 'png' | 'webp' | 'tiff' | 'avif'; transforms: 'json' }> | null;
custom_css?: string | null;
storage_default_folder?: DirectusFolder | string | null;
basemaps?: Array<{ name: string; type: 'raster' | 'tile' | 'style'; url: string; tileSize: number; attribution: string }> | null;
mapbox_key?: string | null;
module_bar?: 'json' | null;
project_descriptor?: string | null;
default_language?: string;
custom_aspect_ratios?: Array<{ text: string; value: number }> | null;
public_favicon?: DirectusFile | string | null;
default_appearance?: 'auto' | 'light' | 'dark';
default_theme_light?: string | null;
theme_light_overrides?: 'json' | null;
default_theme_dark?: string | null;
theme_dark_overrides?: 'json' | null;
report_error_url?: string | null;
report_bug_url?: string | null;
report_feature_url?: string | null;
public_registration?: boolean;
public_registration_verify_email?: boolean;
public_registration_role?: DirectusRole | string | null;
public_registration_email_filter?: 'json' | null;
visual_editor_urls?: Array<{ url: string }> | null;
project_id?: string | null;
mcp_enabled?: boolean;
mcp_allow_deletes?: boolean;
mcp_prompts_collection?: string | null;
mcp_system_prompt_enabled?: boolean;
mcp_system_prompt?: string | null;
project_owner?: string | null;
project_usage?: string | null;
org_name?: string | null;
product_updates?: boolean | null;
project_status?: string | null;
}
export interface DirectusUser {
/** @primaryKey */
id: string;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
password?: string | null;
location?: string | null;
title?: string | null;
description?: string | null;
tags?: string[] | null;
avatar?: DirectusFile | string | null;
language?: string | null;
tfa_secret?: string | null;
status?: 'draft' | 'invited' | 'unverified' | 'active' | 'suspended' | 'archived';
role?: DirectusRole | string | null;
token?: string | null;
last_access?: string | null;
last_page?: string | null;
provider?: string;
external_identifier?: string | null;
auth_data?: 'json' | null;
email_notifications?: boolean | null;
appearance?: null | 'auto' | 'light' | 'dark' | null;
theme_dark?: string | null;
theme_light?: string | null;
theme_light_overrides?: 'json' | null;
theme_dark_overrides?: 'json' | null;
text_direction?: 'auto' | 'ltr' | 'rtl';
policies?: DirectusAccess[] | string[];
}
export interface DirectusWebhook {
/** @primaryKey */
id: number;
name?: string;
method?: null;
url?: string;
status?: 'active' | 'inactive';
data?: boolean;
actions?: 'create' | 'update' | 'delete';
collections?: string[];
headers?: Array<{ header: string; value: string }> | null;
was_active_before_deprecation?: boolean;
migrated_flow?: DirectusFlow | string | null;
}
export interface DirectusDashboard {
/** @primaryKey */
id: string;
name?: string;
icon?: string;
note?: string | null;
date_created?: string | null;
user_created?: DirectusUser | string | null;
color?: string | null;
panels?: DirectusPanel[] | string[];
}
export interface DirectusPanel {
/** @primaryKey */
id: string;
dashboard?: DirectusDashboard | string;
name?: string | null;
icon?: string | null;
color?: string | null;
show_header?: boolean;
note?: string | null;
type?: string;
position_x?: number;
position_y?: number;
width?: number;
height?: number;
options?: 'json' | null;
date_created?: string | null;
user_created?: DirectusUser | string | null;
}
export interface DirectusNotification {
/** @primaryKey */
id: number;
timestamp?: string | null;
status?: string | null;
recipient?: DirectusUser | string;
sender?: DirectusUser | string | null;
subject?: string;
message?: string | null;
collection?: string | null;
item?: string | null;
}
export interface DirectusShare {
/** @primaryKey */
id: string;
name?: string | null;
collection?: DirectusCollection | string;
item?: string;
role?: DirectusRole | string | null;
password?: string | null;
user_created?: DirectusUser | string | null;
date_created?: string | null;
date_start?: string | null;
date_end?: string | null;
times_used?: number | null;
max_uses?: number | null;
}
export interface DirectusFlow {
/** @primaryKey */
id: string;
name?: string;
icon?: string | null;
color?: string | null;
description?: string | null;
status?: string;
trigger?: string | null;
accountability?: string | null;
options?: 'json' | null;
operation?: DirectusOperation | string | null;
date_created?: string | null;
user_created?: DirectusUser | string | null;
operations?: DirectusOperation[] | string[];
}
export interface DirectusOperation {
/** @primaryKey */
id: string;
name?: string | null;
key?: string;
type?: string;
position_x?: number;
position_y?: number;
options?: 'json' | null;
resolve?: DirectusOperation | string | null;
reject?: DirectusOperation | string | null;
flow?: DirectusFlow | string;
date_created?: string | null;
user_created?: DirectusUser | string | null;
}
export interface DirectusTranslation {
/** @primaryKey */
id: string;
/** @required */
language: string;
/** @required */
key: string;
/** @required */
value: string;
}
export interface DirectusVersion {
/** @primaryKey */
id: string;
key?: string;
name?: string | null;
collection?: DirectusCollection | string;
item?: string;
hash?: string | null;
date_created?: string | null;
date_updated?: string | null;
user_created?: DirectusUser | string | null;
user_updated?: DirectusUser | string | null;
delta?: 'json' | null;
}
export interface DirectusExtension {
enabled?: boolean;
/** @primaryKey */
id: string;
folder?: string;
source?: string;
bundle?: string | null;
}
export interface Schema {
article: Article[];
article_tag: ArticleTag[];
article_translations: ArticleTranslation[];
global: Global;
global_translations: GlobalTranslation[];
languages: Language[];
tag: Tag[];
directus_access: DirectusAccess[];
directus_activity: DirectusActivity[];
directus_collections: DirectusCollection[];
directus_comments: DirectusComment[];
directus_fields: DirectusField[];
directus_files: DirectusFile[];
directus_folders: DirectusFolder[];
directus_migrations: DirectusMigration[];
directus_permissions: DirectusPermission[];
directus_policies: DirectusPolicy[];
directus_presets: DirectusPreset[];
directus_relations: DirectusRelation[];
directus_revisions: DirectusRevision[];
directus_roles: DirectusRole[];
directus_sessions: DirectusSession[];
directus_settings: DirectusSettings;
directus_users: DirectusUser[];
directus_webhooks: DirectusWebhook[];
directus_dashboards: DirectusDashboard[];
directus_panels: DirectusPanel[];
directus_notifications: DirectusNotification[];
directus_shares: DirectusShare[];
directus_flows: DirectusFlow[];
directus_operations: DirectusOperation[];
directus_translations: DirectusTranslation[];
directus_versions: DirectusVersion[];
directus_extensions: DirectusExtension[];
}
export enum CollectionNames {
article = 'article',
article_tag = 'article_tag',
article_translations = 'article_translations',
global = 'global',
global_translations = 'global_translations',
languages = 'languages',
tag = 'tag',
directus_access = 'directus_access',
directus_activity = 'directus_activity',
directus_collections = 'directus_collections',
directus_comments = 'directus_comments',
directus_fields = 'directus_fields',
directus_files = 'directus_files',
directus_folders = 'directus_folders',
directus_migrations = 'directus_migrations',
directus_permissions = 'directus_permissions',
directus_policies = 'directus_policies',
directus_presets = 'directus_presets',
directus_relations = 'directus_relations',
directus_revisions = 'directus_revisions',
directus_roles = 'directus_roles',
directus_sessions = 'directus_sessions',
directus_settings = 'directus_settings',
directus_users = 'directus_users',
directus_webhooks = 'directus_webhooks',
directus_dashboards = 'directus_dashboards',
directus_panels = 'directus_panels',
directus_notifications = 'directus_notifications',
directus_shares = 'directus_shares',
directus_flows = 'directus_flows',
directus_operations = 'directus_operations',
directus_translations = 'directus_translations',
directus_versions = 'directus_versions',
directus_extensions = 'directus_extensions'
}

9
directus.ts Normal file
View File

@ -0,0 +1,9 @@
import { createDirectus, graphql, staticToken } from "@directus/sdk";
import { DIRECTUS_TOKEN } from "astro:env/server";
import type { Schema } from "./directus-schema";
const directus = createDirectus<Schema>("https://directus.yarkov.tech")
.with(staticToken(DIRECTUS_TOKEN))
.with(graphql());
export default directus;

View File

@ -1,10 +0,0 @@
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[[redirects]]
from = "/api/*" # simplify all calls to serverless functions
to = "/.netlify/functions/:splat" # all function calls will go to this path
status = 200 # ok code
force = true # ensure to always redirect

View File

@ -1,35 +0,0 @@
import type { Handler } from "@netlify/functions";
import { redisClient } from "../../redis";
const getViewsBySlug = async (slug: string): Promise<number> => {
let views = 1;
const data = await redisClient.get(`views:${slug}`);
if (data) views = Number.parseInt(data, 10);
if (Number.isNaN(views)) views = 1;
return views;
};
const handler: Handler = async (event) => {
const slug = event.queryStringParameters?.slug;
switch (event.httpMethod) {
case "GET": {
if (!slug) return { statusCode: 400, body: "Bad request." };
const views = await getViewsBySlug(slug);
return { statusCode: 200, body: views.toString() };
}
case "POST": {
if (!slug) return { statusCode: 400, body: "Bad request." };
const views = await redisClient.incr(`views:${slug}`);
return { statusCode: 200, body: views.toString() };
}
default: {
return {
statusCode: 500,
body: "Unrecognized HTTP Method, must be one of GET/POST",
};
}
}
};
export { handler };

24267
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,20 +17,20 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.5",
"@astrojs/rss": "^3.0.0", "@astrojs/node": "^9.5.0",
"@astrojs/sitemap": "^3.0.0", "@astrojs/rss": "^4.0.13",
"@astrojs/solid-js": "^3.0.1", "@astrojs/sitemap": "^3.6.0",
"@astrojs/solid-js": "^5.1.3",
"@directus/sdk": "^20.1.1",
"@iconify-json/mdi": "^1.2.3", "@iconify-json/mdi": "^1.2.3",
"@netlify/functions": "^2.1.0", "@netlify/functions": "^2.1.0",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"astro": "^3.2.0", "astro": "^5.15.8",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"decap-cms-app": "^3.6.3",
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"hastscript": "^8.0.0", "hastscript": "^8.0.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"ioredis": "^5.3.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"quicklink": "^3.0.1", "quicklink": "^3.0.1",
"rehype-autolink-headings": "^7.0.0", "rehype-autolink-headings": "^7.0.0",

View File

@ -1,249 +0,0 @@
editor:
preview: false
publish_mode: editorial_workflow
site_url: https://yarkov.tech
display_url: https://yarkov.tech
i18n:
locales: [ru, en]
default_locale: ru
structure: multiple_folders
locale: en
backend:
name: github
repo: sergeyyarkov/yarkov.tech
branch: main
media_folder: public/media
public_folder: "/media"
collections:
- name: articles
label: Публикации
label_singular: публикацию
description: Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой
folder: src/content/blog
slug: "{{slug}}"
create: true
delete: true
i18n: true
fields:
- name: coverImage
widget: image
label: Постер
required: false
i18n: true
- name: draft
label: В черновике
widget: boolean
hint: Опубликована ли статья
i18n: true
required: false
- name: author
widget: relation
value_field: "{{firstName}} {{lastName}}"
search_fields:
- firstName
- lastName
collection: authors
label: Автор
i18n: true
- name: title
widget: string
label: Заголовок
i18n: true
- name: description
widget: string
label: Описание
i18n: true
- name: tags
widget: relation
label: Тэги
collection: tags
multiple: true
value_field: title
search_fields:
- title
i18n: true
- name: pubDate
widget: datetime
label: Дата публикации
picker_utc: true
i18n: true
- name: updatedDate
widget: datetime
label: Дата обновления
picker_utc: true
i18n: true
- name: body
widget: markdown
label: Содержание
i18n: true
- name: tags
label: Тэги
label_singular: тэг
description: Используются для тегирования контента
folder: src/content/tags
create: true
delete: true
fields:
- name: title
widget: string
label: Название
required: true
- name: authors
label: Авторы
label_singular: автора
description: На странице статьи отображается имя автора
slug: "{{firstName}}-{{lastName}}"
folder: src/content/authors
create: true
delete: true
i18n: true
fields:
- label: Имя
name: firstName
widget: string
required: true
i18n: true
- label: Фамилия
name: lastName
widget: string
required: true
i18n: true
- label: Об авторе
name: body
widget: markdown
required: false
i18n: true
- name: projects
label: Проекты
label_singular: проект
description: Основные проекты, которые были разработаны в выложены в открытый
доступ
folder: src/content/projects
create: true
delete: true
i18n: true
fields:
- name: icon
label: Иконка
widget: string
required: true
i18n: true
- name: title
label: Название
widget: string
required: true
i18n: true
- name: year
label: В каком году разработал
widget: number
required: true
i18n: true
- name: description
label: Описание
widget: string
required: true
i18n: true
- name: sourceUrl
label: Ссылка на исходник
widget: string
required: true
i18n: true
- name: demoUrl
label: Ссылка на демонстрацию
widget: string
required: false
i18n: true
- name: articleUrl
label: Ссылка на статью
widget: string
required: false
i18n: true
- name: skills
label: Навыки
label_singular: навык
folder: src/content/skills
create: true
delete: true
description: Навыки отображаются на странице Обо мне
i18n: true
fields:
- name: title
label: Категория
widget: string
required: true
i18n: true
- name: list
label: Записи
required: true
widget: list
allow_add: true
min: 1
i18n: true
fields:
- name: icon
label: Иконка
widget: string
i18n: true
required: false
- name: title
label: Название
widget: string
i18n: true
- name: settings
label: Настройки
files:
- name: siteconfig
label: Настройки веб-сайта
file: siteconfig.json
fields:
- label: Название сайта
name: name
widget: string
hint: Отображается в шапке.
default: yarkov.tech
- label: Автор веб-сайта
name: author
widget: string
hint: Отображается в подвале.
- label: Глобальный заголовок
name: globalTitle
widget: string
hint: Отображется во вкладке браузера.
- label: В разработке
name: isSiteUnderConstruction
widget: boolean
required: false
default: false
hint: Отображает баннер на всех старницах о том, что веб-сайт находится в
разработке
- label: Счетчик Яндекс.Метрики
name: ymCounter
widget: number
hint: "Например: 90034656"
required: false
- label: Статьи
name: articles
widget: object
fields:
- name: recentLimit
label: Лимит на отображение
widget: number
hint: Кол-во публикаций на гл.странице
default: 5
- label: Контакты
name: contacts
widget: object
required: false
fields:
- label: Telegram
name: telegram
widget: string
required: false
- label: Почта
name: email
widget: string
required: false
- label: Github
name: github
widget: string
required: false

View File

@ -1,9 +0,0 @@
import Redis from "ioredis";
const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: Number.parseInt(process.env.REDIS_PORT || "6379", 10),
password: process.env.REDIS_PASSWORD,
});
export { redisClient };

View File

@ -4,18 +4,18 @@ import { URL_BLOG_PREFIX } from "@root/constants";
import "./ArticleItem.scss"; import "./ArticleItem.scss";
export type ArticleItemProps = { export type ArticleItemProps = {
id: string; id: number;
title: string; title: string;
pubDate: Date; pubDate: Date;
pageLang: string; pageLang: string;
}; };
const ArticleItem: Component<ArticleItemProps> = (props) => { const ArticleItem: Component<ArticleItemProps> = (props) => {
const articleLang = props.id.split("/")[0]; const articleLang = "ru";
const href = createRelativeArticleUrl( // const href = createRelativeArticleUrl(
{ id: props.id, title: props.title, pubDate: props.pubDate }, // { id: props.id, title: props.title, pubDate: props.pubDate },
URL_BLOG_PREFIX // URL_BLOG_PREFIX
); // );
return ( return (
<article <article
@ -24,14 +24,14 @@ const ArticleItem: Component<ArticleItemProps> = (props) => {
itemtype="http://schema.org/BlogPosting" itemtype="http://schema.org/BlogPosting"
class="article-item" class="article-item"
> >
<a href={href}> <a href={""}>
<div class="flex"> <div class="flex">
<h3 itemprop="headline">{props.title}</h3> <h3 itemprop="headline">{props.title}</h3>
{articleLang !== props.pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>} {articleLang !== props.pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
</div> </div>
<p> <p>
<time datetime={props.pubDate.toISOString()}> <time datetime={`props.pubDate.toISOString()`}>
{createDateFormatter(props.pageLang).format(props.pubDate)} {/* {createDateFormatter(props.pageLang).format(props.pubDate)} */}
</time> </time>
</p> </p>
</a> </a>

View File

@ -9,13 +9,13 @@ import TranslatedLink from "@components/TranslatedLink.astro";
--- ---
<header {...IS_SITE_UNDER_CONSTRUCTION ? { style: "top: 45px;" } : {}}> <header {...IS_SITE_UNDER_CONSTRUCTION ? { style: "top: 45px;" } : {}}>
<div class="container"> <div class='container'>
<div class="logo"> <div class='logo'>
<TranslatedLink to="/"> <TranslatedLink to='/'>
{SITE_METADATA.name} {SITE_METADATA.name}
</TranslatedLink> </TranslatedLink>
</div> </div>
<nav itemscope itemtype="http://schema.org/SiteNavigationElement"> <nav itemscope itemtype='http://schema.org/SiteNavigationElement'>
<ul> <ul>
{ {
SITE_METADATA.nav.map((link) => ( SITE_METADATA.nav.map((link) => (
@ -23,8 +23,7 @@ import TranslatedLink from "@components/TranslatedLink.astro";
<TranslatedLink <TranslatedLink
itemprop={"url"} itemprop={"url"}
to={link.url} to={link.url}
class:list={[{ active: Astro.url.pathname.includes(link.url) }]} class:list={[{ active: Astro.url.pathname.includes(link.url) }]}>
>
<UIString key={link.key} /> <UIString key={link.key} />
</TranslatedLink> </TranslatedLink>
</li> </li>
@ -33,23 +32,23 @@ import TranslatedLink from "@components/TranslatedLink.astro";
</ul> </ul>
{ {
SITE_METADATA.contacts && ( SITE_METADATA.contacts && (
<div class="contacts"> <div class='contacts'>
<a target="_blank" href={"mailto:" + SITE_METADATA.contacts.email}> <a target='_blank' href={"mailto:" + SITE_METADATA.contacts.email}>
Email Email
<Icon name="email" width="18" height="18" /> <Icon name='email' width='18' height='18' />
</a> </a>
<a target="_blank" href={SITE_METADATA.contacts.github}> <a target='_blank' href={SITE_METADATA.contacts.github}>
Github Github
<Icon name="github_filled" width="18" height="18" /> <Icon name='github_filled' width='18' height='18' />
</a> </a>
<a target="_blank" href={SITE_METADATA.contacts.telegram}> <a target='_blank' href={SITE_METADATA.contacts.telegram}>
Telegram Telegram
<Icon name="telegram" width="18" height="18" /> <Icon name='telegram' width='18' height='18' />
</a> </a>
</div> </div>
) )
} }
<div class="flex settings"> <div class='flex settings'>
<LanguageSelect /> <LanguageSelect />
<ThemeSwitcher client:visible /> <ThemeSwitcher client:visible />
</div> </div>
@ -58,7 +57,7 @@ import TranslatedLink from "@components/TranslatedLink.astro";
</div> </div>
</header> </header>
<style lang="scss"> <style lang='scss'>
header { header {
position: sticky; position: sticky;
top: 0; top: 0;

View File

@ -1,5 +1,4 @@
--- ---
import type { CollectionEntry } from "astro:content";
import siteconfig from "siteconfig.json"; import siteconfig from "siteconfig.json";
import { getLanguageFromURL } from "@i18n/utils"; import { getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index"; import * as utils from "@utils/index";
@ -7,13 +6,32 @@ import ArticleItem from "@components/ArticleItem";
import EmptyList from "@components/EmptyList.astro"; import EmptyList from "@components/EmptyList.astro";
import UIString from "@components/UIString.astro"; import UIString from "@components/UIString.astro";
import TranslatedLink from "@components/TranslatedLink.astro"; import TranslatedLink from "@components/TranslatedLink.astro";
import directus from "@/directus";
import type { ArticleTranslation } from "@/directus-schema";
export interface Props { export interface Props {
articles: CollectionEntry<"blog">[];
length?: number | undefined; length?: number | undefined;
} }
const { articles, length = siteconfig.articles.recentLimit } = Astro.props as Props; type RecentArticlesQueryResult = {
article_translations: Pick<ArticleTranslation, "id" | "title" | "pub_date">[];
};
const data = await directus.query<RecentArticlesQueryResult>(`
query Article_translations {
article_translations {
id
title
pub_date
}
}
`);
const { article_translations: articles } = data;
console.log(data);
const { length = siteconfig.articles.recentLimit } = Astro.props as Props;
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
const filtered = utils.removeDuplicates(articles.slice(0, length), pageLang); const filtered = utils.removeDuplicates(articles.slice(0, length), pageLang);
const isEmpty = articles.length === 0; const isEmpty = articles.length === 0;
@ -24,18 +42,18 @@ const isEmpty = articles.length === 0;
<h2> <h2>
<UIString key={"recentArticles.heading"} /> <UIString key={"recentArticles.heading"} />
</h2> </h2>
<TranslatedLink to="/articles/"> <TranslatedLink to='/articles/'>
<UIString key={"recentArticles.all"} /> <UIString key={"recentArticles.all"} />
</TranslatedLink> </TranslatedLink>
</div> </div>
<div itemscope itemtype="http://schema.org/Blog"> <div itemscope itemtype='http://schema.org/Blog'>
{ {
!isEmpty ? ( !isEmpty ? (
filtered.map((article) => ( articles.map((article) => (
<ArticleItem <ArticleItem
id={article.id} id={article.id}
title={article.data.title} title={article.title}
pubDate={article.data.pubDate} pubDate={article.pub_date}
pageLang={pageLang} pageLang={pageLang}
/> />
)) ))
@ -46,7 +64,7 @@ const isEmpty = articles.length === 0;
</div> </div>
</section> </section>
<style lang="scss"> <style lang='scss'>
section { section {
& > div:nth-child(1) { & > div:nth-child(1) {
display: flex; display: flex;

View File

@ -1,6 +1,7 @@
--- ---
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content"; import { getCollection, getEntry, render } from "astro:content";
import slugify from "@sindresorhus/slugify";
import ArticleLayout from "@root/layouts/article.astro"; import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils"; import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
@ -9,37 +10,41 @@ import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/util
* The final url will be `http://website.com/blog/2023-01-01/my-article` with default language * The final url will be `http://website.com/blog/2023-01-01/my-article` with default language
* and `http://website.com/{language-code}/blog/2023-01-01/my-article` with other languages. * and `http://website.com/{language-code}/blog/2023-01-01/my-article` with other languages.
*/ */
export async function getStaticPaths() { // export async function getStaticPaths() {
const collection = await getCollection("blog", (e) => !e.data.draft); // const collection = await getCollection("blog", (e) => !e.data.draft);
return collection.map((article) => { // return collection.map((article) => {
const { // const {
id, // id,
data: { title, pubDate }, // data: { title, pubDate },
} = article; // } = article;
const url = createRelativeArticleUrl({ id, title, pubDate }); // const url = createRelativeArticleUrl({ id, title, pubDate });
return { // return {
params: { blog: url.slice(1) }, // params: { blog: url.slice(1) },
props: { // props: {
article, // article,
availableOnLangs: collection.filter((e) => // availableOnLangs: collection.filter((e) =>
e.id.includes(removeLanguageCodeFromPath(article.id)) // e.id.includes(removeLanguageCodeFromPath(article.id))
), // ),
}, // },
}; // };
}); // });
} // }
type Props = { article: CollectionEntry<"blog">; availableOnLangs: CollectionEntry<"blog">[] }; type Props = { article: CollectionEntry<"blog">; availableOnLangs: CollectionEntry<"blog">[] };
const { Content, headings } = await Astro.props.article.render(); const { blog } = Astro.params as { blog: string };
const { article, availableOnLangs } = Astro.props; const collection = await getCollection("blog", (e) => !e.data.draft);
const slug = blog.split("/").at(-1);
const article = collection.find(
(a) => slugify(a.slug.split("/").at(-1) || "", { lowercase: true }) === slug
);
if (article === undefined) return Astro.redirect("/404");
const { Content, headings } = await render(article);
// const { article, availableOnLangs } = Astro.props;
--- ---
<ArticleLayout <ArticleLayout lang={article.id.split("/")[0]} article={article} headings={headings}>
lang={article.id.split("/")[0]}
article={article}
headings={headings}
availableOnLangs={availableOnLangs}
>
<Content /> <Content />
</ArticleLayout> </ArticleLayout>

View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<link href="/admin/config.yml" type="text/yaml" rel="cms-config-url" />
<title>Content Manager</title>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.1.2/dist/decap-cms.js"></script>
</body>
</html>

View File

@ -5,13 +5,27 @@ import * as utils from "@utils/index";
import PageLayout from "@layouts/default.astro"; import PageLayout from "@layouts/default.astro";
import PageHeading from "@components/PageHeading.astro"; import PageHeading from "@components/PageHeading.astro";
import ArticlesList from "@root/components/ArticlesList"; import ArticlesList from "@root/components/ArticlesList";
import directus from "@/directus";
const t = useTranslation(Astro); const t = useTranslation(Astro);
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
const articles = await getCollection("blog", (e) => !e.data.draft); // const articles = await getCollection("blog", (e) => !e.data.draft);
const tags = Array.from(new Set((await getCollection("tags")).map((t) => t.data.title))); // const filtered = utils.removeDuplicates(articles, pageLang);
const filtered = utils.removeDuplicates(articles, pageLang);
const canonicalUrl = new URL(Astro.url.pathname, Astro.site); const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const tags = (await directus.query(`query Tag { tag { title } }`)).tag.map((t) => t.title);
const { article_translations } = await directus.query(`
query Article_translations {
article_translations {
languages_code {
code
}
slug
status
pub_date
}
}
`);
--- ---
<PageLayout <PageLayout
@ -25,7 +39,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
<ArticlesList <ArticlesList
client:load client:load
i18n={{ "articles.empty": t("articles.empty"), "input.search": t("input.search") }} i18n={{ "articles.empty": t("articles.empty"), "input.search": t("input.search") }}
articles={utils.formatToArticleBlocks(filtered)} articles={utils.formatToArticleBlocks(article_translations)}
tags={tags} tags={tags}
pageLang={pageLang} pageLang={pageLang}
/> />

View File

@ -11,9 +11,6 @@ import { getLanguageFromURL, useTranslation } from "@root/i18n/utils";
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
const t = useTranslation(Astro); const t = useTranslation(Astro);
const articles = (await getCollection("blog", (e) => !e.data.draft)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
--- ---
<PageLayout description={t("site.description")} lang={pageLang}> <PageLayout description={t("site.description")} lang={pageLang}>
@ -26,11 +23,11 @@ const articles = (await getCollection("blog", (e) => !e.data.draft)).sort(
<p><UIString key={"splash.description"} /></p> <p><UIString key={"splash.description"} /></p>
</SplashHeading> </SplashHeading>
<SplashLinks> <SplashLinks>
<SplashLink icon="information" href="/about/"><UIString key={"splash.about"} /></SplashLink> <SplashLink icon='information' href='/about/'><UIString key={"splash.about"} /></SplashLink>
<SplashLink icon="email_filled" href="/contacts/"> <SplashLink icon='email_filled' href='/contacts/'>
<UIString key={"splash.contact"} /> <UIString key={"splash.contact"} />
</SplashLink> </SplashLink>
</SplashLinks> </SplashLinks>
</Splash> </Splash>
<RecentArticles articles={articles} /> <RecentArticles />
</PageLayout> </PageLayout>

View File

@ -48,13 +48,13 @@ export function createRelativeArticleUrl(
export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) { export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) {
const blocks: Record<string, any[]> = {}; const blocks: Record<string, any[]> = {};
articles.forEach((a) => { articles.forEach((a) => {
const year = a.data.pubDate.getFullYear(); const year = new Date(a.pub_date).getFullYear();
if (!blocks[year]) blocks[year] = []; if (!blocks[year]) blocks[year] = [];
blocks[year].push({ blocks[year].push({
id: a.id, id: a.id,
title: a.data.title, title: a.title,
tags: a.data.tags, // tags: a.tags,
pubDate: a.data.pubDate, pubDate: a.pub_date,
}); });
}); });
return blocks; return blocks;

View File

@ -6,6 +6,7 @@
"strictNullChecks": true, "strictNullChecks": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*"],
"@root/*": ["./src/*"], "@root/*": ["./src/*"],
"@styles/*": ["./src/styles/*"], "@styles/*": ["./src/styles/*"],
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
@ -19,5 +20,5 @@
"typeRoots": ["./node_modules/@types", "./src/types"], "typeRoots": ["./node_modules/@types", "./src/types"],
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "solid-js" "jsxImportSource": "solid-js"
}, }
} }