diff --git a/Makefile b/Makefile
index 1faecb84e..bd618699d 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ install:
-cp -n .env.dist .env
if [ ! -d "${KEYS_DIR}" ]; then mkdir -p ${KEYS_DIR}; openssl genrsa -out ${KEYS_DIR}/private.pem 2048; openssl rsa -in ${KEYS_DIR}/private.pem -outform PEM -pubout -out ${KEYS_DIR}/public.pem; fi
mkdir -p moderation
- touch moderation/sus.txt moderation/rules-users.md moderation/rules-terminology.md moderation/rules-sources.md moderation/timesheets.md moderation/expenses.md
+ touch moderation/sus.txt moderation/rules-users.md moderation/rules-terminology.md moderation/rules-sources.md moderation/timesheets.md moderation/expenses.md moderation/blog.md
pnpm install
pnpm run-file server/migrate.ts
diff --git a/components/blog/BlogEntriesList.vue b/components/blog/BlogEntriesList.vue
index 0c9623c04..15ac3d2ad 100644
--- a/components/blog/BlogEntriesList.vue
+++ b/components/blog/BlogEntriesList.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
+
+ admin.header
+
+
+
+
+ Blog editor
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/admin/index.vue b/pages/admin/index.vue
index 1fdd3b779..27f55011c 100644
--- a/pages/admin/index.vue
+++ b/pages/admin/index.vue
@@ -263,6 +263,14 @@ const impersonate = async (email: string) => {
{ name: 'awaiting', count: stats.locales[locale].names.awaiting, warning: 1, danger: 16 },
]"
/>
+
+ Editor
+
{
rulesSources: md.render(await fs.readFile(`${dir}/rules-sources.md`, 'utf-8')),
timesheets: md.render(await fs.readFile(`${dir}/timesheets.md`, 'utf-8')),
expenses: md.render(await fs.readFile(`${dir}/expenses.md`, 'utf-8')),
+ blog: md.render(await fs.readFile(`${dir}/blog.md`, 'utf-8')),
};
});
diff --git a/server/blog.ts b/server/blog.ts
index 42c2e7cb0..d2e49c237 100644
--- a/server/blog.ts
+++ b/server/blog.ts
@@ -3,68 +3,10 @@ import fs from 'node:fs/promises';
import { defineCachedFunction } from 'nitropack/runtime';
import SQL from 'sql-template-strings';
-import type { Config } from '~/locale/config.ts';
import type { Database } from '~/server/db.ts';
import type { UserRow } from '~/server/express/user.ts';
import { rootDir } from '~/server/paths.ts';
-
-export interface PostMetadata {
- title: string;
- date: string;
- authors: string[];
- hero: { src: string; alt: string; class: string } | undefined;
-}
-
-export interface Post extends PostMetadata {
- slug: string;
-}
-
-export const extractMetadata = (config: Config, content: string): PostMetadata | undefined => {
- const lines = content.split('\n').filter((l) => !!l);
-
- const title = lines[0].match(/^# (.*)$/)?.[1];
-
- const secondLineMatch = lines[1].match(/^(\d\d\d\d-\d\d-\d\d) \| ([^|]*).*<\/small>$/);
- const date = secondLineMatch?.[1];
- const authors = secondLineMatch?.[2].split(',').map((a) => {
- a = a.trim();
- const teamName = config.contact.team?.route;
- if (teamName && a.startsWith(teamName)) {
- return teamName;
- }
- const m = a.match(/^\[([^\]]+)]/);
- if (m) {
- return m[1];
- }
- return a;
- }) ?? [];
-
- let hero = undefined;
- const classHeroImages = lines
- .map((x) => x.match(/
!!x);
- if (classHeroImages.length) {
- hero = {
- src: classHeroImages[0][1],
- alt: classHeroImages[0][3],
- class: classHeroImages[0][2].replace('d-none', ''),
- };
- } else {
- const images = lines.map((x) => x.match(/^!\[([^\]]*)]\(([^)]+)\)$/)).filter((x) => !!x);
- if (images.length) {
- hero = {
- src: images[0][2],
- alt: images[0][1],
- class: '',
- };
- }
- }
-
- if (title === undefined || date === undefined) {
- return undefined;
- }
- return { title, date, authors, hero };
-};
+import { extractMetadata, type Post } from '~/src/blog/metadata.ts';
export const getPosts = defineCachedFunction(async (): Promise => {
const dir = `${rootDir}/data/blog`;
diff --git a/src/blog/metadata.ts b/src/blog/metadata.ts
new file mode 100644
index 000000000..25d27be66
--- /dev/null
+++ b/src/blog/metadata.ts
@@ -0,0 +1,59 @@
+import type { Config } from '~/locale/config.ts';
+
+export interface PostMetadata {
+ title: string;
+ date: string;
+ authors: string[];
+ hero: { src: string; alt: string; class: string } | undefined;
+}
+
+export interface Post extends PostMetadata {
+ slug: string;
+}
+
+export const extractMetadata = (config: Config, content: string): PostMetadata | undefined => {
+ const lines = content.split('\n').filter((l) => !!l);
+
+ const title = lines[0]?.match(/^# (.*)$/)?.[1];
+
+ const secondLineMatch = lines[1]?.match(/^(\d\d\d\d-\d\d-\d\d) \| ([^|]*).*<\/small>$/);
+ const date = secondLineMatch?.[1];
+ const authors = secondLineMatch?.[2].split(',').map((a) => {
+ a = a.trim();
+ const teamName = config.contact.team?.route;
+ if (teamName && a.startsWith(teamName)) {
+ return teamName;
+ }
+ const m = a.match(/^\[([^\]]+)]/);
+ if (m) {
+ return m[1];
+ }
+ return a;
+ }) ?? [];
+
+ let hero = undefined;
+ const classHeroImages = lines
+ .map((x) => x.match(/
!!x);
+ if (classHeroImages.length) {
+ hero = {
+ src: classHeroImages[0][1],
+ alt: classHeroImages[0][3],
+ class: classHeroImages[0][2].replace('d-none', ''),
+ };
+ } else {
+ const images = lines.map((x) => x.match(/^!\[([^\]]*)]\(([^)]+)\)$/)).filter((x) => !!x);
+ if (images.length) {
+ hero = {
+ src: images[0][2],
+ alt: images[0][1],
+ class: '',
+ };
+ }
+ }
+
+ if (title === undefined || date === undefined) {
+ return undefined;
+ }
+ return { title, date, authors, hero };
+};
diff --git a/test/locales/blog.test.ts b/test/locales/blog.test.ts
index 8af9f5e6a..da3c264b2 100644
--- a/test/locales/blog.test.ts
+++ b/test/locales/blog.test.ts
@@ -13,7 +13,7 @@ import { loadSumlFromBase } from '../../server/loader.ts';
import parseMarkdown from '../../src/parseMarkdown.ts';
import { Translator } from '../../src/translator.ts';
-import { extractMetadata } from '~/server/blog.ts';
+import { extractMetadata } from '~/src/blog/metadata.ts';
const validator = new HtmlValidate({
extends: [