Compare commits

..
29 Commits
Author SHA1 Message Date
Hare 3e32df5761 chore: optimize Dockerfile for pnpm usage and build process 2024-12-18 07:14:14 +09:00
Hare 992c99d2fb update: refine layout and spacing in blog styles and article page 2024-12-18 07:14:01 +09:00
Hare 5c05db4580 update: layout styles in overview and article pages 2024-12-18 06:34:55 +09:00
Hare bafa8e4d18 change: add rehypeAutolinkHeadings plugin to article compiler 2024-12-18 06:07:46 +09:00
Hare b529e96a6b update: add headings overview 2024-12-18 06:02:28 +09:00
Hare 7f395bcfaa Merge branch 'master' into develop 2024-10-23 21:39:13 +09:00
Hare 97600ed402 change: svelte default to temp logo 2024-10-23 21:38:33 +09:00
Hare ae48a85ece change: discord invite link 2024-10-23 20:17:39 +09:00
Hare 9f162e83c4 Merge pull request 'fix: Clone and analyze the repository correctly' (#15) from develop into master
Reviewed-on: #15
2024-09-27 00:49:42 +09:00
Hare e4548bf10b Merge branch 'master' into develop 2024-09-27 00:49:35 +09:00
Hare 5b441ff0cf fix: Clone and analyze the repository correctly 2024-09-27 00:48:22 +09:00
Hare 76bd2a5e1b Merge pull request 'develop' (#14) from develop into master
Reviewed-on: #14
2024-09-27 00:17:58 +09:00
Hare e3a83ee561 Merge branch 'master' into develop 2024-09-27 00:17:51 +09:00
Hare dd057dd335 Refactor: Update imports for server modules 2024-09-27 00:16:57 +09:00
Hare f11bdf15fc fix typo #2 2024-09-26 23:44:12 +09:00
Hare 5ec1839ec7 fix typo 2024-09-26 23:43:51 +09:00
Hare 2fdcc1633d Merge pull request 'fix: initializing git during build' (#13) from develop into master
Reviewed-on: #13
2024-09-26 23:37:15 +09:00
Hare 6173dffba3 fix: initializing git during build 2024-09-26 23:36:12 +09:00
Hare e389a3d2f9 Merge branch 'develop' 2024-09-26 23:12:09 +09:00
Hare 4e2422fa09 Merge pull request 'feat-google-analytics' (#11) from feat-google-analytics into develop
Reviewed-on: #11
2024-09-26 23:07:52 +09:00
Hare 8d09641e5d Remove articles subproject 2024-09-26 23:06:41 +09:00
Hare 2b6e3bfa9f feat: Add Google Analytics tracking code to layout component 2024-09-26 23:05:42 +09:00
Hare db831b0683 Merge pull request 'feat-git' (#10) from feat-git into master
Reviewed-on: #10
2024-09-25 23:30:08 +09:00
Hare c08bb9dcef Merge branch 'master' into feat-git 2024-09-25 23:29:55 +09:00
Hare 36237f0708 feat: apply author and email fields to Articles 2024-09-25 23:29:21 +09:00
Hare 3145073381 feat: add a column to store datafrom git 2024-09-21 14:51:15 +09:00
Hare 7032c552a8 feat: Refactor formatted_date component and add format function 2024-09-18 17:32:02 +09:00
Hare 7cd8db36e2 feat: Add simple-git dependency 2024-09-18 17:31:06 +09:00
Hare 58ce0f28c7 Merge pull request 'feat: codeblock highlighter and styling' (#9) from develop into master
Reviewed-on: #9
2024-09-07 02:49:35 +09:00
31 changed files with 3928 additions and 4648 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# blog.hareworks.net # blog.hareworks.net
<img src="https://blog.hareworks.net/img/logo.png" width="200"> <img src="./static/img/logo.png" width="200">
## About ## About
Submodule articles deleted from 3744bd7899
+11 -6
View File
@@ -1,19 +1,24 @@
# For Build # For Build
FROM node:22-slim as builder FROM node:22-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json ./
COPY package-lock.json ./ COPY pnpm-lock.yaml ./
COPY tsconfig.json ./ COPY tsconfig.json ./
RUN npm ci
FROM base AS builder
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY . . COPY . .
RUN pnpm run build
RUN npm run build
# For Run FROM base
FROM node:22-slim RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
WORKDIR /app WORKDIR /app
-4120
View File
File diff suppressed because it is too large Load Diff
+25 -22
View File
@@ -1,6 +1,6 @@
{ {
"name": "blog.hareworks.net", "name": "blog.hareworks.net",
"version": "0.0.1", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -13,31 +13,34 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.28.1", "@playwright/test": "^1.49.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.2", "@sveltejs/adapter-node": "^5.2.10",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.12.1",
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@sveltejs/vite-plugin-svelte": "^5.0.2",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.1",
"@types/pg": "^8.11.6", "@types/pg": "^8.11.10",
"eslint": "^9.0.0", "eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.0.0", "globals": "^15.13.0",
"prettier": "^3.1.1", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.3.2",
"shiki": "^1.16.2", "shiki": "^1.24.2",
"svelte": "^5.0.0-next.1", "svelte": "^5.14.1",
"svelte-check": "^3.6.0", "svelte-check": "4.1.1",
"typescript": "^5.0.0", "typescript": "^5.7.2",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.18.1",
"vite": "^5.0.3" "vite": "^6.0.3"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dotenv": "^16.4.5", "dotenv": "^16.4.7",
"mdsvex": "^0.12.3", "mdsvex": "^0.12.3",
"pg": "^8.12.0", "pg": "^8.13.1",
"sass": "^1.77.8" "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"sass": "^1.83.0",
"simple-git": "^3.27.0"
} }
} }
+3127
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -1,9 +1,11 @@
import type { Handle } from '@sveltejs/kit' import type { Handle } from '@sveltejs/kit'
import { getConnection } from '$lib/server/database/get_connection' import { getPool } from '$lib/server/database/pool';
import git from '$lib/server/git';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const pg = getConnection(); const pg = getPool;
event.locals.db = pg; event.locals.db = pg;
event.locals.git = git;
const result = await resolve(event); const result = await resolve(event);
return result; return result;
+4 -4
View File
@@ -1,5 +1,5 @@
@import './ress.css'; @forward './ress.css';
@import '../variables.scss'; @forward '../variables.scss';
@font-face { @font-face {
font-family: 'ZenKakuGothicNew-Regular'; font-family: 'ZenKakuGothicNew-Regular';
@@ -7,7 +7,7 @@
} }
:root { :root {
font-size: 16px; font-size: 18px;
} }
body { body {
@@ -16,5 +16,5 @@ body {
color: #ffffff; color: #ffffff;
font-family: 'ZenKakuGothicNew-Regular', '游ゴシック体', 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro', font-family: 'ZenKakuGothicNew-Regular', '游ゴシック体', 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro',
'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, Osaka, 'MS PGothic', sans-serif; 'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, Osaka, 'MS PGothic', sans-serif;
font-size: 16px; font-size: 1rem;
} }
+2
View File
@@ -7,6 +7,8 @@ export type Article = {
category: string; category: string;
released_at: Date; released_at: Date;
updated_at: Date; updated_at: Date;
author: string;
email: string;
tags: string[]; tags: string[];
image: string; image: string;
publish: Publish; publish: Publish;
+60 -23
View File
@@ -7,12 +7,16 @@
color: var(--color-text); color: var(--color-text);
padding-left: 0rem;
box-sizing: border-box;
h1 { h1 {
position: relative; position: relative;
width: auto; width: auto;
font-size: 2rem; font-size: 1.8rem;
margin-top: 1rem; padding: 1.5rem 0 0.25rem 0;
margin-bottom: 0.5rem; margin: 0 0 0.75rem 0rem;
border-bottom: 1px solid var(--color-outline);
} }
hr { hr {
@@ -20,26 +24,22 @@
margin: 0; margin: 0;
border: none; border: none;
border-top: 1px solid var(--color-outline); border-top: 1px solid var(--color-outline);
} margin-left: -1rem;
>*:not(h1, hr) {
margin-left: 2rem;
} }
h2 { h2 {
padding: 1rem 0 0.25rem 0;
font-size: 1.5rem; font-size: 1.5rem;
position: relative; position: relative;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
&::before { // &::before {
content: '#'; // content: '#';
position: absolute; // position: absolute;
top: 0; // top: 0;
left: -1.5rem; // left: -1.5rem;
font-size: 1.75rem; // font-size: 1.75rem;
color: rgb(131, 131, 131); // color: rgb(131, 131, 131);
} // }
} }
h3 { h3 {
@@ -73,9 +73,35 @@
} }
} }
p { ul,
margin-top: 0.25rem; ol {
margin-bottom: 0.25rem; list-style: none;
padding: 0;
margin: 0;
}
li {
position: relative;
padding-left: 1rem;
margin-bottom: 0.5rem;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
color: var(--color-concept);
}
}
ul > li {
&::before {
content: '';
}
}
ol > li {
counter-increment: item;
&::before {
content: counter(item) '.';
}
} }
blockquote { blockquote {
@@ -119,24 +145,35 @@
} }
} }
code:not(pre > code) {
display: inline-block;
font-family: monospace;
font-size: 0.9rem;
background: #66666666;
padding: 0 0.3rem;
border-radius: 0.5rem;
}
pre { pre {
font-family: monospace; font-family: monospace;
position: relative; position: relative;
display: block; display: block;
margin-top: 0.75rem; margin-top: 0.75rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
border: 2px solid #ffffff40; // border: 2px solid #ffffff40;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow-x: scroll; overflow-x: auto;
font-size: 0.9rem;
&::-webkit-scrollbar { &::-webkit-scrollbar {
border-radius: 10px; border-radius: 10px;
height: 10px; height: 10px;
width: 8px; width: 8px;
background: #3d3d3d;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #999; background: #6f6f6f;
border-radius: 10px; border-radius: 10px;
} }
+2 -15
View File
@@ -3,22 +3,9 @@
if (typeof date === 'string') date = new Date(date); if (typeof date === 'string') date = new Date(date);
const pad = function (str: string): string { import { format } from "$lib/scripts/formatted_date";
return ('0' + str).slice(-2);
};
const year = date.getFullYear().toString(); const formattedDate = format(date);
const month = pad((date.getMonth() + 1).toString());
const day = pad(date.getDate().toString());
const hour = pad(date.getHours().toString());
const min = pad(date.getMinutes().toString());
const sec = pad(date.getSeconds().toString());
const tz = -date.getTimezoneOffset();
const sign = tz >= 0 ? '+' : '-';
const tzHour = pad((tz / 60).toString());
const tzMin = pad((tz % 60).toString());
let formattedDate = `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
</script> </script>
{formattedDate} {formattedDate}
+68
View File
@@ -0,0 +1,68 @@
<script lang="ts">
export let html: string;
const headings: {
level: number;
id: string;
text: string;
}[] = [];
html.match(/<h(\d) id="([^"]+)">([^<]+)<\/h\d>/g)?.forEach((match) => {
console.log(match);
const [, level, id, text] = match.match(/<h(\d) id="([^"]+)">([^<]+)<\/h\d>/)!;
headings.push({ level: +level, id, text });
});
let output = '';
let lastLevel = 1;
headings.forEach(({ level, id, text }) => {
console.log(level, id, text);
if (level > lastLevel) {
output += '<ul>';
} else if (level < lastLevel) {
output += '</li></ul>'.repeat(lastLevel - level);
} else {
if (output !== '') {
}
}
output += `<li><a href="#${id}">${text}</a>`;
lastLevel = level;
});
output += '</li>'.repeat(lastLevel);
</script>
<div class="overview">
<ul>
{@html output}
</ul>
</div>
<style lang="scss">
.overview {
margin: 0;
padding: 0;
:global(ul) {
list-style-type: none;
margin-left: 0.5em;
padding-left: 0.25em;
border-left: 1px solid var(--line-primary);
box-sizing: border-box;
}
ul {
border-left: none;
}
:global(li) {
padding: 0.5em 0.5em 0;
box-sizing: border-box;
}
:global(a) {
text-decoration: none;
color: var(--text-primary);
padding: 0.15em 0.4em;
border-radius: 0.25em;
text-wrap: nowrap;
box-sizing: border-box;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
</style>
+75 -9
View File
@@ -5,6 +5,7 @@
import Footer from '$lib/components/footer.svelte'; import Footer from '$lib/components/footer.svelte';
import * as publish from '$lib/publish'; import * as publish from '$lib/publish';
import FormattedDate from '$lib/components/formatted_date.svelte'; import FormattedDate from '$lib/components/formatted_date.svelte';
import Overview from '$lib/components/overview.svelte';
import type { Article } from '$lib/article'; import type { Article } from '$lib/article';
export let data: Article; export let data: Article;
@@ -21,7 +22,7 @@
{#if isUpdated} {#if isUpdated}
<meta property="article:modified_time" content={data.updated_at.toISOString()} /> <meta property="article:modified_time" content={data.updated_at.toISOString()} />
{/if} {/if}
<meta property="article:author" content="HareWorks" /> <meta property="article:author" content={data.author} />
<meta property="article:section" content="Blog" /> <meta property="article:section" content="Blog" />
<meta property="article:tag" content={data.tags.join(',')} /> <meta property="article:tag" content={data.tags.join(',')} />
<meta name="robots" content={data.publish as publish.Publish} /> <meta name="robots" content={data.publish as publish.Publish} />
@@ -32,9 +33,11 @@
</div> --> </div> -->
<div class="container"> <div class="container">
<main> <main>
<div class="content">
<div class="title"> <div class="title">
<h1>{data.title}</h1> <h1>{data.title}</h1>
<div class="meta"> <div class="meta">
<div class="left">
<span> <span>
released <FormattedDate date={data.released_at} /> released <FormattedDate date={data.released_at} />
{#if isUpdated}<br /> {#if isUpdated}<br />
@@ -50,6 +53,14 @@
{/each} {/each}
</span> </span>
</div> </div>
<div class="right">
<img
src="https://gitea.hareworks.net/avatars/e595ec10f8052671deab92caca78220fc838f63259bcd542a0d93f53e2371d52?size=512"
alt="Author"
/>
<div>@{data.author}</div>
</div>
</div>
</div> </div>
<div class="panel"> <div class="panel">
<div class="document"> <div class="document">
@@ -57,6 +68,12 @@
</div> </div>
<Footer /> <Footer />
</div> </div>
</div>
<div class="card-container">
<div class="card">
<Overview html={data.content} />
</div>
</div>
</main> </main>
</div> </div>
@@ -91,26 +108,48 @@
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
main { main {
box-sizing: border-box;
color: white;
display: flex;
flex-direction: row;
justify-content: center;
align-items: start;
.content {
border: 1px solid var(--line-primary); border: 1px solid var(--line-primary);
border-top: none; border-top: none;
border-bottom: none; border-bottom: none;
box-sizing: border-box;
color: white;
min-height: 100%; min-height: 100%;
padding: 100px 100px 0 100px;
max-width: 900px;
margin: 0;
}
}
.card-container {
position: sticky;
top: 0;
background-color: var(--background-primary);
box-sizing: border-box;
padding: 20px; padding: 20px;
padding-top: 100px;
max-width: 1000px;
margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
overflow: visible;
.card {
width: 100px;
padding: 0.5em 0;
overflow: visible;
} }
}
.title { .title {
h1 { h1 {
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
margin: 0; margin: 0 -30px 0 -30px;
padding: 12px 100px 12px 0; padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3); border-bottom: 1px solid rgba(255, 255, 255, 0.3);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@@ -120,8 +159,14 @@
.meta { .meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-direction: row;
padding: 0 20px; padding: 0 20px;
font-size: 0.8rem; font-size: 1rem;
> .left {
display: flex;
flex-direction: column;
justify-content: center;
align-items: left;
span { span {
opacity: 0.6; opacity: 0.6;
} }
@@ -134,6 +179,8 @@
text-decoration: underline; text-decoration: underline;
} }
&:first-child { &:first-child {
margin-left: 0;
padding-left: 0;
&::before { &::before {
content: '🗀 '; content: '🗀 ';
} }
@@ -144,6 +191,25 @@
} }
} }
} }
> .right {
display: flex;
flex-direction: row;
justify-content: right;
align-items: center;
gap: 10px;
height: 50px;
margin: 10px;
img {
width: 50px;
height: 50px;
border-radius: 15%;
}
div {
font-size: 1rem;
}
}
}
} }
.panel { .panel {
flex: 1; flex: 1;
+20
View File
@@ -0,0 +1,20 @@
export const format = (
date: Date
) => {
const pad = function (str: string): string {
return ('0' + str).slice(-2);
};
const year = date.getFullYear().toString();
const month = pad((date.getMonth() + 1).toString());
const day = pad(date.getDate().toString());
const hour = pad(date.getHours().toString());
const min = pad(date.getMinutes().toString());
const sec = pad(date.getSeconds().toString());
const tz = -date.getTimezoneOffset();
const sign = tz >= 0 ? '+' : '-';
const tzHour = pad((tz / 60).toString());
const tzMin = pad((tz % 60).toString());
return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
};
@@ -1,7 +1,9 @@
//@ts-ignore
import { compile, escapeSvelte } from "mdsvex"; import { compile, escapeSvelte } from "mdsvex";
import { createHighlighter } from "shiki"; import { createHighlighter } from "shiki";
import { kanagawa_dark as theme } from "$lib/kanagawa" import { kanagawa_dark as theme } from "$lib/kanagawa"
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
const highlighter = await createHighlighter({ const highlighter = await createHighlighter({
themes: [theme], themes: [theme],
@@ -18,5 +20,6 @@ export default async function compileArticle(code: string) {
return html; return html;
}, },
}, },
rehypePlugins: [rehypeAutolinkHeadings, rehypeSlug],
}); });
} }
+144
View File
@@ -0,0 +1,144 @@
import type { Postgres } from '$lib/server/database/postgres';
import { type SimpleGit, CheckRepoActions } from 'simple-git';
import fs from 'fs';
import compileArticle from '$lib/server/article_compiler';
import type { TableSchema } from '$lib/server/database/table';
import server_table from '$lib/server/database/table';
import { format } from '$lib/scripts/formatted_date';
const load = async (db: Postgres, git: SimpleGit) => {
const articleFiles = await crawlArticles(db);
await db.query('update tag set ref_count = 0');
await db.begin();
let i = 0;
for (const { path, id } of articleFiles) {
await db.savepoint(`load${i}`);
console.log(`Processing ${id}...`);
try {
const gitlog = await git.log({
file: path.slice(11),
strictDate: true,
});
const res = await db.query('select * from article where id = $1', [id]);
// console.log(gitlog);
const latest = gitlog.latest!;
const oldest = gitlog.all[gitlog.all.length - 1];
const author = oldest.author_name;
const email = oldest.author_email;
const released_at = new Date(oldest.date);
const updated_at = new Date(latest.date);
console.log(`Author: ${author} <${email}>\nReleased at: ${format(released_at)}\nUpdated at: ${format(updated_at)}`);
const category = path.split('/')[3];
const compiled = await compileArticle(fs.readFileSync(path, 'utf-8'));
const title = compiled.data.fm.title;
const tags: string[] = compiled.data.fm.tags;
const image = compiled.data.fm.image;
const publish = compiled.data.fm.publish;
const content = compiled.code
.replace(/>{@html `<code class="language-/g, '><code class="language-')
.replace(/<\/code>`}<\/pre>/g, '</code></pre>')
if (res.rowCount == 0) {
console.log(`New article: ${id}`);
await db.query(
'insert into article (id, released_at, updated_at, author, email, title, category, tags, image, publish, content) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)',
[id, released_at, updated_at, author, email, title, category, tags, image, publish, content]
);
// } else if (res.rows[0].updated_at < updated_at) {
} else if (true) {
console.log(`Update article: ${id}`);
await db.query(
'update article set released_at = $2, updated_at = $3, author = $4, email = $5, title = $6, category = $7, tags = $8, image = $9, publish = $10, content = $11 where id = $1',
[id, released_at, updated_at, author, email, title, category, tags, image, publish, content]
);
} else {
console.log(`Article ${id} is already up-to-date`);
}
for (const tag of tags) {
if ((await db.query('select * from tag where name = $1', [tag])).rowCount == 0) {
db.query('insert into tag (name, ref_count) values ($1, 1)', [tag]);
} else {
db.query('update tag set ref_count = ref_count + 1 where name = $1', [tag]);
}
}
} catch (err) {
console.log(err);
await db.rollbackTo(`load${i}`);
} finally {
console.log('');
await db.releaseSavepoint(`load${i}`);
i++;
}
}
await db.commit();
}
type ArticleFileItem = {
path: string,
id: string,
}
const cloneRepo = async (git: SimpleGit) => {
const isRepoRoot = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
if (isRepoRoot) {
console.log('Pulling articles from git..');
await git.pull();
} else {
console.log('Cloning articles from git..');
await git.clone('git@gitea.hareworks.net:Hare/blog-articles.git', './');
}
}
const createTable = async (db: Postgres, schemas: TableSchema[]) => {
await db.begin();
try {
for (const schema of schemas) {
const res = await db.query(`select * from information_schema.tables where table_name = '${schema.name}'`)
if (res.rowCount == 0) {
console.log(`Creating table ${schema.name}`);
const columnStr = schema.columns.map(c => `${c.name} ${c.type} ${c.constraint}`).join(', ');
await db.query(`create table ${schema.name} (${columnStr})`);
} else {
console.log(`Table ${schema.name} already exists`);
}
}
} catch (err) {
console.error(err);
await db.rollback();
} finally {
await db.commit();
}
}
const crawlArticles = async (db: Postgres): Promise<ArticleFileItem[]> => {
const articleFiles: ArticleFileItem[] = [];
function scanDir(path: string) {
const files = fs.readdirSync(path);
for (const file of files) {
const dir = `${path}/${file}`;
const stat = fs.statSync(dir);
if (stat.isDirectory()) {
scanDir(dir);
} else {
articleFiles.push({ path: `${path}/${file}`, id: file.replace('.md', '') });
}
}
}
scanDir('./articles/article');
return articleFiles;
}
export async function init(db: Postgres, git: SimpleGit) {
await createTable(db, server_table);
await cloneRepo(git);
await load(db, git);
}
export async function reload(db: Postgres, git: SimpleGit) {
await cloneRepo(git);
await load(db, git);
}
-19
View File
@@ -1,19 +0,0 @@
import pg from 'pg';
const { Pool } = pg;
import {
PG_USER,
PG_PASS,
PG_HOST,
PG_PORT,
PG_DB,
} from '$env/static/private'
const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`;
const pool = new Pool({ connectionString });
export const getConnection = () => pool;
import init_db from '$lib/server/database/init';
import PG from '$lib/server/database';
await init_db(await PG(pool));
-183
View File
@@ -1,183 +0,0 @@
// initialize
import type { Postgres } from '$lib/server/database';
import fs from 'fs';
import { execSync } from 'child_process';
import compile from '$lib/server/article';
export default async function init(db: Postgres) {
if (fs.existsSync('./articles/')) {
console.log('Pulling articles from git..');
const stdout = execSync('git -c core.sshCommand="ssh -i ../key -F /dev/null" pull', { cwd: './articles/' });
console.log(stdout.toString());
} else {
console.log('Cloning articles from git..');
const stdout = execSync('git -c core.sshCommand="ssh -i ./key -F /dev/null" clone git@gitea.hareworks.net:Hare/blog-articles.git articles', { cwd: './' });
console.log(stdout.toString());
}
const schemas = [
{
name: 'article',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'id', type: 'text', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'category', type: 'text', constraint: 'not null' },
{ name: 'released_at', type: 'timestamp', constraint: 'not null' },
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
{ name: 'tags', type: 'text[]', constraint: 'not null' },
{ name: 'image', type: 'text', constraint: '' },
{ name: 'publish', type: 'text', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'article_comment',
columns: [
{ name: 'id', type: 'serial', constraint: 'primary key' },
{ name: 'article', type: 'integer', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'id', type: 'text', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'category', type: 'text', constraint: 'not null' },
{ name: 'created_at', type: 'timestamp', constraint: 'not null' },
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
{ name: 'tags', type: 'text[]', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread_post',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'thread_id', type: 'integer', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread_comment',
columns: [
{ name: 'id', type: 'serial', constraint: 'primary key' },
{ name: 'thread', type: 'integer', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'tag',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'name', type: 'text', constraint: 'not null' },
{ name: 'ref_count', type: 'integer', constraint: 'not null' },
],
}
];
await db.begin();
try {
// Create tables
for (const schema of schemas) {
const res = await db.query(`select * from information_schema.tables where table_name = '${schema.name}'`)
if (res.rowCount == 0) {
console.log(`Creating table ${schema.name}`);
const columnStr = schema.columns.map(c => `${c.name} ${c.type} ${c.constraint}`).join(', ');
await db.query(`create table ${schema.name} (${columnStr})`);
} else {
console.log(`Table ${schema.name} already exists`);
}
}
} catch (err) {
console.error(err);
await db.rollback();
} finally {
await db.commit();
}
const articleFiles: ArticleFileItem[] = [];
function scanDir(path: string) {
const files = fs.readdirSync(path);
for (const file of files) {
const dir = `${path}/${file}`;
const stat = fs.statSync(dir);
if (stat.isDirectory()) {
scanDir(dir);
} else {
articleFiles.push({ path: `${path}/${file}`, id: file.replace('.md', '') });
}
}
}
scanDir('./articles/article');
await db.query('update tag set ref_count = 0');
for (const { path, id } of articleFiles) {
await db.begin();
try {
const res = await db.query('select * from article where id = $1', [id]);
const compiled = await compile(fs.readFileSync(path, 'utf-8'));
const title = compiled.data.fm.title;
const category = path.split('/')[3];
const tags: string[] = compiled.data.fm.tags;
const released_at = new Date(compiled.data.fm.released_at);
const updated_at = (compiled.data.fm.updated_at !== null) ? new Date(compiled.data.fm.updated_at) : released_at;
const image = compiled.data.fm.image;
const publish = compiled.data.fm.publish;
const content = compiled.code
.replace(/>{@html `<code class="language-/g, '><code class="language-')
.replace(/<\/code>`}<\/pre>/g, '</code></pre>')
if (res.rowCount == 0) {
console.log(`New article: ${id}`);
await db.query(
'insert into article (id, title, category, released_at, updated_at, tags, image, publish, content) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)',
[id, title, category, released_at, updated_at, tags, image, publish, content]
);
} else if (res.rows[0].updated_at < updated_at) {
// } else if (true) {
console.log(`Update article: ${id}`);
await db.query(
'update article set title = $2, category = $3, released_at = $4, updated_at = $5, tags = $6, image = $7, publish = $8, content = $9 where id = $1',
[id, title, category, released_at, updated_at, tags, image, publish, content]
);
} else {
console.log(`Article ${id} is already up-to-date`);
}
for (const tag of tags) {
if ((await db.query('select * from tag where name = $1', [tag])).rowCount == 0) {
db.query('insert into tag (name, ref_count) values ($1, 1)', [tag]);
} else {
db.query('update tag set ref_count = ref_count + 1 where name = $1', [tag]);
}
}
} catch (err) {
console.log(err);
await db.rollback();
} finally {
await db.commit();
}
}
await db.release();
}
type ArticleFileItem = {
path: string,
id: string,
}
export type TableSchema = {
name: string,
columns: {
name: string,
type: string,
constraint: string,
}[],
}
+24
View File
@@ -0,0 +1,24 @@
import pg from 'pg';
const { Pool } = pg;
import {
PG_USER,
PG_PASS,
PG_HOST,
PG_PORT,
PG_DB,
} from '$env/static/private'
const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`;
let pool = new Pool({ connectionString });
export const getPool = pool;
export const reConnect = async () => {
pool = new Pool({ connectionString });
}
import { init as init_data} from '$lib/server/data_loader';
import PG from '$lib/server/database/postgres';
import git from '$lib/server/git';
await init_data(await PG(pool), git);
@@ -24,9 +24,21 @@ export class Postgres {
await this.client!.query('commit'); await this.client!.query('commit');
} }
async savepoint(name: string) {
await this.client!.query(`savepoint ${name}`);
}
async rollback() { async rollback() {
await this.client!.query('rollback'); await this.client!.query('rollback');
} }
async rollbackTo(name: string) {
await this.client!.query(`rollback to ${name}`);
}
async releaseSavepoint(name: string) {
await this.client!.query(`release savepoint ${name}`);
}
} }
export default async ( export default async (
+77
View File
@@ -0,0 +1,77 @@
export type TableSchema = {
name: string,
columns: {
name: string,
type: string,
constraint: string,
}[],
}
export default [
{
name: 'article',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'id', type: 'text', constraint: 'not null' },
{ name: 'released_at', type: 'timestamp', constraint: 'not null' },
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
{ name: 'author', type: 'text', constraint: 'not null' },
{ name: 'email', type: 'text', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'category', type: 'text', constraint: 'not null' },
{ name: 'tags', type: 'text[]', constraint: 'not null' },
{ name: 'image', type: 'text', constraint: '' },
{ name: 'publish', type: 'text', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'article_comment',
columns: [
{ name: 'id', type: 'serial', constraint: 'primary key' },
{ name: 'article', type: 'integer', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'id', type: 'text', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'category', type: 'text', constraint: 'not null' },
{ name: 'created_at', type: 'timestamp', constraint: 'not null' },
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
{ name: 'tags', type: 'text[]', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread_post',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'thread_id', type: 'integer', constraint: 'not null' },
{ name: 'title', type: 'text', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'thread_comment',
columns: [
{ name: 'id', type: 'serial', constraint: 'primary key' },
{ name: 'thread', type: 'integer', constraint: 'not null' },
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
{ name: 'content', type: 'text', constraint: 'not null' },
],
},
{
name: 'tag',
columns: [
{ name: 'seq', type: 'serial', constraint: 'primary key' },
{ name: 'name', type: 'text', constraint: 'not null' },
{ name: 'ref_count', type: 'integer', constraint: 'not null' },
],
}
] as TableSchema[];
+17
View File
@@ -0,0 +1,17 @@
import fs from 'fs';
import { simpleGit, } from 'simple-git';
import type { SimpleGit, SimpleGitOptions } from 'simple-git';
console.log(process.cwd()+'/articles');
fs.mkdirSync(process.cwd()+'/articles', { recursive: true });
export const gitOptions: Partial<SimpleGitOptions> = {
baseDir: process.cwd()+'/articles',
binary: 'git',
maxConcurrentProcesses: 6,
config: [
'core.sshCommand=ssh -i ../key -F /dev/null'
],
};
const git: SimpleGit = simpleGit(gitOptions);
export default git;
+13
View File
@@ -2,4 +2,17 @@
import '$lib/app.scss'; import '$lib/app.scss';
</script> </script>
<svelte:head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-6D99J1R7DX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-6D99J1R7DX');
</script>
</svelte:head>
<slot /> <slot />
+3 -2
View File
@@ -1,6 +1,7 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import PG from '$lib/server/database'; import PG from '$lib/server/database/postgres';
import { format } from '$lib/scripts/formatted_date';
let data: { let data: {
recent: { recent: {
@@ -28,7 +29,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
data.recent = recent_articles.rows.map((row) => ({ data.recent = recent_articles.rows.map((row) => ({
title: row.title, title: row.title,
image: row.image, image: row.image,
date: row.updated_at.toISOString().slice(0, 10), date: format(row.updated_at),
link: `/article/${row.category}/${row.id}`, link: `/article/${row.category}/${row.id}`,
tags: row.tags, tags: row.tags,
})); }));
+4 -4
View File
@@ -6,9 +6,9 @@ import {
TOKEN TOKEN
} from '$env/static/private' } from '$env/static/private'
import PG from '$lib/server/database'; import PG from '$lib/server/database/postgres';
// import { building } from '$app/environment'; import { building } from '$app/environment';
import init_db from '$lib/server/database/init'; import { reload as reload_data } from '$lib/server/data_loader';
export const POST: RequestHandler = async ({ url, locals }) => { export const POST: RequestHandler = async ({ url, locals }) => {
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
@@ -17,6 +17,6 @@ export const POST: RequestHandler = async ({ url, locals }) => {
return error(401, 'Unauthorized'); return error(401, 'Unauthorized');
} }
await init_db(await PG(locals.db)); if (!building) await reload_data(await PG(locals.db), locals.git);
return new Response(String(token)); return new Response(String(token));
}; };
@@ -1,6 +1,6 @@
import type { Article } from '$lib/article'; import type { Article } from '$lib/article';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import PG from '$lib/server/database'; import PG from '$lib/server/database/postgres';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => { export const load: PageServerLoad = async ({ params, locals }) => {
@@ -1,6 +1,6 @@
import type { Article } from '$lib/article'; import type { Article } from '$lib/article';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import PG from '$lib/server/database'; import PG from '$lib/server/database/postgres';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => { export const load: PageServerLoad = async ({ params, locals }) => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export function load() { export function load() {
throw redirect(308, 'https://discord.gg/9RfkJeQQfB'); throw redirect(308, 'https://discord.gg/hbHzw7JkFq');
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

-5
View File
@@ -3,14 +3,9 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} }
}; };