2024-09-27 16:59:29 +02:00
|
|
|
import { dev } from "$app/environment";
|
2024-09-25 21:45:01 +02:00
|
|
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
2024-11-19 18:49:40 +01:00
|
|
|
import Index from "$lib/templates/Index.svelte";
|
|
|
|
|
import Article from "$lib/templates/Article.svelte";
|
2024-10-30 21:33:44 +01:00
|
|
|
import { type WebsiteOverview, hexToHSL } from "$lib/utils";
|
2024-11-19 18:49:40 +01:00
|
|
|
import { mkdir, writeFile, chmod, readdir, rm, readFile } from "node:fs/promises";
|
2024-09-27 16:59:29 +02:00
|
|
|
import { join } from "node:path";
|
|
|
|
|
import { render } from "svelte/server";
|
|
|
|
|
import type { Actions, PageServerLoad } from "./$types";
|
2024-08-03 18:07:27 +02:00
|
|
|
|
2024-10-30 21:33:44 +01:00
|
|
|
const getOverviewFetchUrl = (websiteId: string) => {
|
2024-11-19 18:49:40 +01:00
|
|
|
return `${API_BASE_PREFIX}/website?id=eq.${websiteId}&select=*,user!user_id(*),settings(*),header(*),home(*),footer(*),article(*,docs_category(*))`;
|
2024-10-30 21:33:44 +01:00
|
|
|
};
|
|
|
|
|
|
2024-10-19 17:55:02 +02:00
|
|
|
export const load: PageServerLoad = async ({ params, fetch, parent }) => {
|
2024-09-25 21:45:01 +02:00
|
|
|
const websiteOverview: WebsiteOverview = (
|
2024-10-30 21:33:44 +01:00
|
|
|
await apiRequest(fetch, getOverviewFetchUrl(params.websiteId), "GET", {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: "application/vnd.pgrst.object+json"
|
|
|
|
|
},
|
|
|
|
|
returnData: true
|
|
|
|
|
})
|
2024-09-25 21:45:01 +02:00
|
|
|
).data;
|
2024-08-04 16:15:09 +02:00
|
|
|
|
2024-10-04 17:09:51 +02:00
|
|
|
const { websitePreviewUrl, websiteProdUrl } = await generateStaticFiles(websiteOverview);
|
2024-11-19 18:49:40 +01:00
|
|
|
const prodIsGenerated = (await fetch(websiteProdUrl, { method: "HEAD" })).ok;
|
2024-09-07 15:07:31 +02:00
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
let currentMeta = null;
|
|
|
|
|
try {
|
|
|
|
|
const metaPath = join(
|
|
|
|
|
"/var/www/archtika-websites",
|
|
|
|
|
websiteOverview.user.username,
|
|
|
|
|
websiteOverview.slug as string,
|
|
|
|
|
".publication-meta.json"
|
|
|
|
|
);
|
|
|
|
|
const metaContent = await readFile(metaPath, "utf-8");
|
|
|
|
|
currentMeta = JSON.parse(metaContent);
|
|
|
|
|
} catch {
|
|
|
|
|
currentMeta = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { website, permissionLevel } = await parent();
|
2024-10-19 17:55:02 +02:00
|
|
|
|
2024-08-04 17:46:41 +02:00
|
|
|
return {
|
2024-08-10 22:20:57 +02:00
|
|
|
websiteOverview,
|
2024-08-20 19:17:05 +02:00
|
|
|
websitePreviewUrl,
|
2024-10-19 17:55:02 +02:00
|
|
|
websiteProdUrl,
|
2024-11-19 18:49:40 +01:00
|
|
|
permissionLevel,
|
|
|
|
|
prodIsGenerated,
|
|
|
|
|
currentMeta,
|
|
|
|
|
website
|
2024-08-04 17:46:41 +02:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2024-08-05 14:38:44 +02:00
|
|
|
export const actions: Actions = {
|
2024-11-19 18:49:40 +01:00
|
|
|
publishWebsite: async ({ fetch, params, locals }) => {
|
2024-09-25 21:45:01 +02:00
|
|
|
const websiteOverview: WebsiteOverview = (
|
2024-10-30 21:33:44 +01:00
|
|
|
await apiRequest(fetch, getOverviewFetchUrl(params.websiteId), "GET", {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: "application/vnd.pgrst.object+json"
|
|
|
|
|
},
|
|
|
|
|
returnData: true
|
|
|
|
|
})
|
2024-09-25 21:45:01 +02:00
|
|
|
).data;
|
2024-08-04 17:46:41 +02:00
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
let permissionLevel = 40;
|
2024-10-30 21:33:44 +01:00
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
if (websiteOverview.user_id !== locals.user.id) {
|
|
|
|
|
permissionLevel = (
|
|
|
|
|
await apiRequest(
|
|
|
|
|
fetch,
|
|
|
|
|
`${API_BASE_PREFIX}/collab?select=permission_level&website_id=eq.${params.websiteId}&user_id=eq.${locals.user.id}`,
|
|
|
|
|
"GET",
|
|
|
|
|
{
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: "application/vnd.pgrst.object+json"
|
|
|
|
|
},
|
|
|
|
|
returnData: true
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
).data.permission_level;
|
2024-10-30 21:33:44 +01:00
|
|
|
}
|
|
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
if (permissionLevel < 30) {
|
|
|
|
|
return { success: false, message: "Insufficient permissions" };
|
|
|
|
|
}
|
2024-10-30 21:33:44 +01:00
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
await generateStaticFiles(websiteOverview, false, fetch);
|
2024-09-20 15:56:07 +02:00
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
return { success: true, message: "Successfully published website" };
|
2024-08-04 17:46:41 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
const generateStaticFiles = async (
|
|
|
|
|
websiteData: WebsiteOverview,
|
|
|
|
|
isPreview = true,
|
|
|
|
|
customFetch: typeof fetch = fetch
|
|
|
|
|
) => {
|
2024-10-04 17:09:51 +02:00
|
|
|
const websitePreviewUrl = `${
|
|
|
|
|
dev
|
2025-01-04 20:33:00 +01:00
|
|
|
? "http://127.0.0.1:18000"
|
2024-10-04 17:09:51 +02:00
|
|
|
: process.env.ORIGIN
|
|
|
|
|
? process.env.ORIGIN
|
2025-01-04 20:33:00 +01:00
|
|
|
: "http://127.0.0.1:18000"
|
2024-10-04 17:09:51 +02:00
|
|
|
}/previews/${websiteData.id}/`;
|
|
|
|
|
|
|
|
|
|
const websiteProdUrl = dev
|
2025-01-04 20:33:00 +01:00
|
|
|
? `http://127.0.0.1:18000/${websiteData.user.username}/${websiteData.slug}`
|
2024-10-04 17:09:51 +02:00
|
|
|
: process.env.ORIGIN
|
2024-11-19 18:49:40 +01:00
|
|
|
? `${process.env.ORIGIN.replace("//", `//${websiteData.user.username}.`)}/${websiteData.slug}`
|
2025-01-04 20:33:00 +01:00
|
|
|
: `http://127.0.0.1:18000/${websiteData.user.username}/${websiteData.slug}`;
|
2024-10-04 17:09:51 +02:00
|
|
|
|
2024-09-10 17:29:57 +02:00
|
|
|
const fileContents = (head: string, body: string) => {
|
|
|
|
|
return `
|
2024-09-07 16:45:20 +02:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
${head}
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
${body}
|
|
|
|
|
</body>
|
|
|
|
|
</html>`;
|
2024-09-10 17:29:57 +02:00
|
|
|
};
|
|
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
const { head, body } = render(Index, {
|
2024-09-10 17:29:57 +02:00
|
|
|
props: {
|
|
|
|
|
websiteOverview: websiteData,
|
|
|
|
|
apiUrl: API_BASE_PREFIX,
|
2024-10-04 17:09:51 +02:00
|
|
|
websiteUrl: isPreview ? websitePreviewUrl : websiteProdUrl
|
2024-09-10 17:29:57 +02:00
|
|
|
}
|
|
|
|
|
});
|
2024-08-04 17:46:41 +02:00
|
|
|
|
|
|
|
|
let uploadDir = "";
|
|
|
|
|
|
|
|
|
|
if (isPreview) {
|
2024-08-18 13:48:36 +02:00
|
|
|
uploadDir = join("/", "var", "www", "archtika-websites", "previews", websiteData.id);
|
2024-11-19 18:49:40 +01:00
|
|
|
await mkdir(uploadDir, { recursive: true });
|
2024-08-04 17:46:41 +02:00
|
|
|
} else {
|
2024-09-20 15:56:07 +02:00
|
|
|
uploadDir = join(
|
|
|
|
|
"/",
|
|
|
|
|
"var",
|
|
|
|
|
"www",
|
|
|
|
|
"archtika-websites",
|
2024-11-19 18:49:40 +01:00
|
|
|
websiteData.user.username,
|
|
|
|
|
websiteData.slug ?? websiteData.id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const articlesDir = join(uploadDir, "articles");
|
|
|
|
|
let existingArticles: string[] = [];
|
|
|
|
|
try {
|
|
|
|
|
existingArticles = await readdir(articlesDir);
|
|
|
|
|
} catch {
|
|
|
|
|
existingArticles = [];
|
|
|
|
|
}
|
|
|
|
|
const currentArticleSlugs = websiteData.article?.map((article) => `${article.slug}.html`) ?? [];
|
|
|
|
|
|
|
|
|
|
for (const file of existingArticles) {
|
|
|
|
|
if (!currentArticleSlugs.includes(file)) {
|
|
|
|
|
await rm(join(articlesDir, file));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const latestChange = await apiRequest(
|
|
|
|
|
customFetch,
|
|
|
|
|
`${API_BASE_PREFIX}/change_log?website_id=eq.${websiteData.id}&order=tstamp.desc&limit=1`,
|
|
|
|
|
"GET",
|
|
|
|
|
{
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: "application/vnd.pgrst.object+json"
|
|
|
|
|
},
|
|
|
|
|
returnData: true
|
|
|
|
|
}
|
2024-09-20 15:56:07 +02:00
|
|
|
);
|
2024-11-19 18:49:40 +01:00
|
|
|
|
|
|
|
|
const meta = {
|
|
|
|
|
lastPublishedAt: new Date().toISOString(),
|
|
|
|
|
lastChangeLogId: latestChange?.data?.id
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await mkdir(uploadDir, { recursive: true });
|
|
|
|
|
await writeFile(join(uploadDir, ".publication-meta.json"), JSON.stringify(meta, null, 2));
|
2024-08-04 17:46:41 +02:00
|
|
|
}
|
2024-08-04 16:15:09 +02:00
|
|
|
|
2024-09-10 17:29:57 +02:00
|
|
|
await writeFile(join(uploadDir, "index.html"), fileContents(head, body));
|
2024-08-27 16:39:29 +02:00
|
|
|
await mkdir(join(uploadDir, "articles"), {
|
2024-08-18 18:17:59 +02:00
|
|
|
recursive: true
|
|
|
|
|
});
|
2024-08-04 16:15:09 +02:00
|
|
|
|
2024-09-10 17:29:57 +02:00
|
|
|
for (const article of websiteData.article ?? []) {
|
2024-11-19 18:49:40 +01:00
|
|
|
const { head, body } = render(Article, {
|
2024-09-10 17:29:57 +02:00
|
|
|
props: {
|
|
|
|
|
websiteOverview: websiteData,
|
|
|
|
|
article,
|
2024-10-04 17:09:51 +02:00
|
|
|
apiUrl: API_BASE_PREFIX,
|
|
|
|
|
websiteUrl: isPreview ? websitePreviewUrl : websiteProdUrl
|
2024-09-10 17:29:57 +02:00
|
|
|
}
|
|
|
|
|
});
|
2024-08-03 18:07:27 +02:00
|
|
|
|
2024-10-30 21:33:44 +01:00
|
|
|
await writeFile(join(uploadDir, "articles", `${article.slug}.html`), fileContents(head, body));
|
2024-08-03 18:07:27 +02:00
|
|
|
}
|
2024-08-17 20:21:23 +02:00
|
|
|
|
2024-10-04 17:09:51 +02:00
|
|
|
const variableStyles = await readFile(`${process.cwd()}/template-styles/variables.css`, {
|
|
|
|
|
encoding: "utf-8"
|
|
|
|
|
});
|
2024-08-20 19:17:05 +02:00
|
|
|
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
|
2024-08-17 20:21:23 +02:00
|
|
|
encoding: "utf-8"
|
|
|
|
|
});
|
2024-08-27 16:39:29 +02:00
|
|
|
const specificStyles = await readFile(
|
|
|
|
|
`${process.cwd()}/template-styles/${websiteData.content_type.toLowerCase()}-styles.css`,
|
|
|
|
|
{
|
|
|
|
|
encoding: "utf-8"
|
|
|
|
|
}
|
|
|
|
|
);
|
2024-09-27 16:59:29 +02:00
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
h: hDark,
|
|
|
|
|
s: sDark,
|
|
|
|
|
l: lDark
|
|
|
|
|
} = hexToHSL(websiteData.settings.background_color_dark_theme);
|
|
|
|
|
const {
|
|
|
|
|
h: hLight,
|
|
|
|
|
s: sLight,
|
|
|
|
|
l: lLight
|
|
|
|
|
} = hexToHSL(websiteData.settings.background_color_light_theme);
|
|
|
|
|
|
2024-08-25 16:31:12 +02:00
|
|
|
await writeFile(
|
2024-10-04 17:09:51 +02:00
|
|
|
join(uploadDir, "variables.css"),
|
|
|
|
|
variableStyles
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_DARK_THEME_H \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_DARK_THEME_H */ ${hDark};`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_DARK_THEME_S \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_DARK_THEME_S */ ${sDark}%;`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_DARK_THEME_L \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_DARK_THEME_L */ ${lDark}%;`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_LIGHT_THEME_H \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_LIGHT_THEME_H */ ${hLight};`
|
2024-08-25 16:31:12 +02:00
|
|
|
)
|
2024-10-04 17:09:51 +02:00
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_LIGHT_THEME_S \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_LIGHT_THEME_S */ ${sLight}%;`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* BACKGROUND_COLOR_LIGHT_THEME_L \*\/\s*.*?;/g,
|
|
|
|
|
`/* BACKGROUND_COLOR_LIGHT_THEME_L */ ${lLight}%;`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* ACCENT_COLOR_DARK_THEME \*\/\s*.*?;/g,
|
|
|
|
|
`/* ACCENT_COLOR_DARK_THEME */ ${websiteData.settings.accent_color_dark_theme};`
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
|
|
|
|
/\/\* ACCENT_COLOR_LIGHT_THEME \*\/\s*.*?;/g,
|
|
|
|
|
`/* ACCENT_COLOR_LIGHT_THEME */ ${websiteData.settings.accent_color_light_theme};`
|
2024-08-25 16:31:12 +02:00
|
|
|
)
|
|
|
|
|
);
|
2024-10-04 17:09:51 +02:00
|
|
|
await writeFile(join(uploadDir, "common.css"), commonStyles);
|
|
|
|
|
await writeFile(join(uploadDir, "scoped.css"), specificStyles);
|
|
|
|
|
|
2024-11-19 18:49:40 +01:00
|
|
|
await setPermissions(join(uploadDir, "../"));
|
2024-10-17 16:53:31 +02:00
|
|
|
|
2024-10-04 17:09:51 +02:00
|
|
|
return { websitePreviewUrl, websiteProdUrl };
|
2024-08-03 18:07:27 +02:00
|
|
|
};
|
2024-10-17 16:53:31 +02:00
|
|
|
|
|
|
|
|
const setPermissions = async (dir: string) => {
|
2024-12-08 14:33:33 +01:00
|
|
|
const mode = dev ? 0o777 : process.env.ORIGIN ? 0o770 : 0o777;
|
|
|
|
|
await chmod(dir, mode);
|
2024-10-17 16:53:31 +02:00
|
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const fullPath = join(dir, entry.name);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
await setPermissions(fullPath);
|
|
|
|
|
} else {
|
2024-12-08 14:33:33 +01:00
|
|
|
await chmod(fullPath, mode);
|
2024-10-17 16:53:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|