diff --git a/nix/dev-vm.nix b/nix/dev-vm.nix index 42aef6c..c54902c 100644 --- a/nix/dev-vm.nix +++ b/nix/dev-vm.nix @@ -99,6 +99,14 @@ }; }; + systemd.services.postgresql = { + path = with pkgs; [ + # Tar and gzip are needed for tar.gz exports + gnutar + gzip + ]; + }; + services.getty.autologinUser = "dev"; system.stateVersion = "24.05"; diff --git a/nix/module.nix b/nix/module.nix index 84eeb8e..ffdf468 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -169,6 +169,14 @@ in extraPlugins = with pkgs.postgresql16Packages; [ pgjwt ]; }; + systemd.services.postgresql = { + path = with pkgs; [ + # Tar and gzip are needed for tar.gz exports + gnutar + gzip + ]; + }; + services.nginx = { enable = true; recommendedProxySettings = true; diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index fd0ef7c..aadffe2 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -1,4 +1,6 @@ -- migrate:up +CREATE EXTENSION unaccent; + CREATE SCHEMA internal; CREATE SCHEMA api; @@ -27,6 +29,17 @@ GRANT USAGE ON SCHEMA internal TO authenticated_user; ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; +CREATE FUNCTION internal.immutable_unaccent (TEXT) + RETURNS TEXT + AS $$ + SELECT + unaccent ($1); +$$ +LANGUAGE sql +IMMUTABLE; + +GRANT EXECUTE ON FUNCTION internal.immutable_unaccent TO authenticated_user; + CREATE TABLE internal.user ( id UUID PRIMARY KEY DEFAULT gen_random_uuid (), username VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(username) >= 3 AND username ~ '^[a-zA-Z0-9_-]+$'), @@ -91,7 +104,7 @@ CREATE TABLE internal.docs_category ( id UUID PRIMARY KEY DEFAULT gen_random_uuid (), website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL, user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID, - category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''), + category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != '' AND category_name != 'Uncategorized'), category_weight INT CHECK (category_weight >= 0) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), @@ -105,6 +118,7 @@ CREATE TABLE internal.article ( website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL, user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID, title VARCHAR(100) NOT NULL CHECK (TRIM(title) != ''), + slug VARCHAR(100) GENERATED ALWAYS AS (REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(TRIM(REGEXP_REPLACE(internal.immutable_unaccent (title), '\s+', '-', 'g'))), '[^\w-]', '', 'g'), '-+', '-', 'g'), '^-+', '', 'g'), '-+$', '', 'g')) STORED, meta_description VARCHAR(250) CHECK (TRIM(meta_description) != ''), meta_author VARCHAR(100) CHECK (TRIM(meta_author) != ''), cover_image UUID REFERENCES internal.media (id) ON DELETE SET NULL, @@ -115,6 +129,7 @@ CREATE TABLE internal.article ( created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL, + UNIQUE (website_id, slug), UNIQUE (website_id, category, article_weight) ); @@ -168,6 +183,8 @@ DROP TABLE internal.user; DROP SCHEMA api; +DROP FUNCTION internal.immutable_unaccent; + DROP SCHEMA internal; DROP ROLE anon; @@ -180,3 +197,5 @@ DROP ROLE authenticator; ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC; +DROP EXTENSION unaccent; + diff --git a/rest-api/db/migrations/20240810115846_image_upload_function.sql b/rest-api/db/migrations/20240810115846_image_upload_function.sql index a3e9495..b6668b8 100644 --- a/rest-api/db/migrations/20240810115846_image_upload_function.sql +++ b/rest-api/db/migrations/20240810115846_image_upload_function.sql @@ -1,5 +1,5 @@ -- migrate:up -CREATE DOMAIN "*/*" AS bytea; +CREATE DOMAIN "*/*" AS BYTEA; CREATE FUNCTION api.upload_file (BYTEA, OUT file_id UUID) AS $$ diff --git a/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql b/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql index cfdd78f..b7a96d5 100644 --- a/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql +++ b/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql @@ -1,7 +1,7 @@ -- migrate:up CREATE TABLE internal.domain_prefix ( website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE, - prefix VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(prefix) >= 3 AND prefix ~ '^[a-z]+(-[a-z]+)*$'), + prefix VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(prefix) >= 3 AND prefix ~ '^[a-z]+(-[a-z]+)*$' AND prefix != 'previews'), created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL @@ -14,9 +14,9 @@ SELECT FROM internal.domain_prefix; -GRANT SELECT, INSERT (website_id, prefix), UPDATE (website_id, prefix), DELETE ON internal.domain_prefix TO authenticated_user; +GRANT SELECT ON internal.domain_prefix TO authenticated_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON api.domain_prefix TO authenticated_user; +GRANT SELECT ON api.domain_prefix TO authenticated_user; ALTER TABLE internal.domain_prefix ENABLE ROW LEVEL SECURITY; @@ -24,17 +24,64 @@ CREATE POLICY view_domain_prefix ON internal.domain_prefix FOR SELECT USING (internal.user_has_website_access (website_id, 10)); -CREATE POLICY update_domain_prefix ON internal.domain_prefix - FOR UPDATE - USING (internal.user_has_website_access (website_id, 30)); +CREATE FUNCTION api.set_domain_prefix (website_id UUID, prefix VARCHAR(16), OUT was_set BOOLEAN) +AS $$ +DECLARE + _has_access BOOLEAN; + _old_domain_prefix VARCHAR(16); + _base_path CONSTANT TEXT := '/var/www/archtika-websites/'; + _old_path TEXT; + _new_path TEXT; +BEGIN + _has_access = internal.user_has_website_access (set_domain_prefix.website_id, 30); + SELECT + d.prefix INTO _old_domain_prefix + FROM + internal.domain_prefix AS d + WHERE + d.website_id = set_domain_prefix.website_id; + INSERT INTO internal.domain_prefix (website_id, prefix) + VALUES (set_domain_prefix.website_id, set_domain_prefix.prefix) + ON CONFLICT ON CONSTRAINT domain_prefix_pkey + DO UPDATE SET + prefix = EXCLUDED.prefix; + _old_path = _base_path || COALESCE(_old_domain_prefix, set_domain_prefix.website_id::TEXT); + _new_path = _base_path || set_domain_prefix.prefix; + IF _old_path != _new_path THEN + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''mv -T %s %s''', _old_path, _new_path); + END IF; + was_set := TRUE; +END; +$$ +LANGUAGE plpgsql +SECURITY DEFINER; -CREATE POLICY delete_domain_prefix ON internal.domain_prefix - FOR DELETE - USING (internal.user_has_website_access (website_id, 30)); +GRANT EXECUTE ON FUNCTION api.set_domain_prefix TO authenticated_user; -CREATE POLICY insert_domain_prefix ON internal.domain_prefix - FOR INSERT - WITH CHECK (internal.user_has_website_access (website_id, 30)); +CREATE FUNCTION api.delete_domain_prefix (website_id UUID, OUT was_deleted BOOLEAN) +AS $$ +DECLARE + _has_access BOOLEAN; + _old_domain_prefix VARCHAR(16); + _base_path CONSTANT TEXT := '/var/www/archtika-websites/'; + _old_path TEXT; + _new_path TEXT; +BEGIN + _has_access = internal.user_has_website_access (delete_domain_prefix.website_id, 30); + DELETE FROM internal.domain_prefix AS d + WHERE d.website_id = delete_domain_prefix.website_id + RETURNING + prefix INTO _old_domain_prefix; + _old_path = _base_path || _old_domain_prefix; + _new_path = _base_path || delete_domain_prefix.website_id; + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''mv -T %s %s''', _old_path, _new_path); + was_deleted := TRUE; +END; +$$ +LANGUAGE plpgsql +SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION api.delete_domain_prefix TO authenticated_user; CREATE TRIGGER update_domain_prefix_last_modified BEFORE INSERT OR UPDATE OR DELETE ON internal.domain_prefix @@ -51,6 +98,10 @@ DROP TRIGGER track_changes_domain_prefix ON internal.domain_prefix; DROP TRIGGER update_domain_prefix_last_modified ON internal.domain_prefix; +DROP FUNCTION api.set_domain_prefix; + +DROP FUNCTION api.delete_domain_prefix; + DROP VIEW api.domain_prefix; DROP TABLE internal.domain_prefix; diff --git a/rest-api/db/migrations/20241006165029_administrator.sql b/rest-api/db/migrations/20241006165029_administrator.sql index 4e28b0b..3d151f7 100644 --- a/rest-api/db/migrations/20241006165029_administrator.sql +++ b/rest-api/db/migrations/20241006165029_administrator.sql @@ -42,9 +42,7 @@ BEGIN w.user_id = $1 GROUP BY w.id, - w.title - ORDER BY - storage_size_bytes DESC', _union_queries); + w.title', _union_queries); RETURN QUERY EXECUTE _query USING _user_id; END; diff --git a/rest-api/db/migrations/20241011092744_filesystem_triggers.sql b/rest-api/db/migrations/20241011092744_filesystem_triggers.sql index 7c396f5..52fe174 100644 --- a/rest-api/db/migrations/20241011092744_filesystem_triggers.sql +++ b/rest-api/db/migrations/20241011092744_filesystem_triggers.sql @@ -8,6 +8,7 @@ DECLARE _base_path CONSTANT TEXT := '/var/www/archtika-websites/'; _preview_path TEXT; _prod_path TEXT; + _article_slug TEXT; BEGIN IF TG_TABLE_NAME = 'website' THEN _website_id := OLD.id; @@ -17,7 +18,7 @@ BEGIN SELECT d.prefix INTO _domain_prefix FROM - internal.domain_prefix d + internal.domain_prefix AS d WHERE d.website_id = _website_id; _preview_path := _base_path || 'previews/' || _website_id; @@ -25,11 +26,20 @@ BEGIN IF TG_TABLE_NAME = 'website' THEN EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf %s''', _preview_path); EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf %s''', _prod_path); - ELSE + ELSIF TG_TABLE_NAME = 'article' THEN + SELECT + a.slug INTO _article_slug + FROM + internal.article AS a + WHERE + a.id = OLD.id; + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -f %s/articles/%s.html''', _preview_path, _article_slug); + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -f %s/articles/%s.html''', _prod_path, _article_slug); + ELSIF TG_TABLE_NAME = 'legal_information' THEN EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -f %s/legal-information.html''', _preview_path); EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -f %s/legal-information.html''', _prod_path); END IF; - RETURN OLD; + RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql @@ -40,6 +50,11 @@ CREATE TRIGGER _cleanup_filesystem_website FOR EACH ROW EXECUTE FUNCTION internal.cleanup_filesystem (); +CREATE TRIGGER _cleanup_filesystem_article + BEFORE UPDATE OR DELETE ON internal.article + FOR EACH ROW + EXECUTE FUNCTION internal.cleanup_filesystem (); + CREATE TRIGGER _cleanup_filesystem_legal_information BEFORE DELETE ON internal.legal_information FOR EACH ROW @@ -48,6 +63,8 @@ CREATE TRIGGER _cleanup_filesystem_legal_information -- migrate:down DROP TRIGGER _cleanup_filesystem_website ON internal.website; +DROP TRIGGER _cleanup_filesystem_article ON internal.article; + DROP TRIGGER _cleanup_filesystem_legal_information ON internal.legal_information; DROP FUNCTION internal.cleanup_filesystem; diff --git a/rest-api/db/migrations/20241029160539_export_articles.sql b/rest-api/db/migrations/20241029160539_export_articles.sql new file mode 100644 index 0000000..a4eb7a2 --- /dev/null +++ b/rest-api/db/migrations/20241029160539_export_articles.sql @@ -0,0 +1,49 @@ +-- migrate:up +CREATE FUNCTION api.export_articles_zip (website_id UUID) + RETURNS "*/*" + AS $$ +DECLARE + _has_access BOOLEAN; + _headers TEXT; + _article RECORD; + _markdown_dir TEXT := '/tmp/website-' || export_articles_zip.website_id; +BEGIN + _has_access = internal.user_has_website_access (export_articles_zip.website_id, 20); + + SELECT + FORMAT('[{ "Content-Type": "application/gzip" },' + '{ "Content-Disposition": "attachment; filename=\"%s\"" }]', + 'archtika-export-articles-' || export_articles_zip.website_id || '.tar.gz') INTO _headers; + PERFORM + SET_CONFIG('response.headers', _headers, TRUE); + + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''mkdir -p %s''', _markdown_dir || '/articles'); + + FOR _article IN ( + SELECT a.id, a.website_id, a.slug, a.main_content + FROM internal.article AS a + WHERE a.website_id = export_articles_zip.website_id) + LOOP + EXECUTE FORMAT( + 'COPY (SELECT %L) TO ''%s''', + _article.main_content, + _markdown_dir || '/articles/' || _article.slug || '.md' + ); + END LOOP; + + EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''tar -czf %s -C %s articles && rm %s''', + _markdown_dir || '/export.tar.gz', + _markdown_dir, + _markdown_dir || '/articles/*.md' + ); + + RETURN pg_read_binary_file(_markdown_dir || '/export.tar.gz'); +END; +$$ +LANGUAGE plpgsql +SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION api.export_articles_zip TO authenticated_user; + +-- migrate:down +DROP FUNCTION api.export_articles_zip; diff --git a/web-app/package.json b/web-app/package.json index 8715dbd..352d9bb 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -11,7 +11,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . && eslint .", "format": "prettier --write .", - "gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal" + "gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal --datesAsStrings" }, "devDependencies": { "@playwright/test": "1.47.0", diff --git a/web-app/src/lib/components/DateTime.svelte b/web-app/src/lib/components/DateTime.svelte index b2ba649..3682077 100644 --- a/web-app/src/lib/components/DateTime.svelte +++ b/web-app/src/lib/components/DateTime.svelte @@ -1,16 +1,30 @@ - - {new Date(date).toLocaleString("en-us", { ...options })} + + {calcTimeAgo(dateObject)} diff --git a/web-app/src/lib/components/Modal.svelte b/web-app/src/lib/components/Modal.svelte index 967ad75..8931084 100644 --- a/web-app/src/lib/components/Modal.svelte +++ b/web-app/src/lib/components/Modal.svelte @@ -9,6 +9,14 @@ }: { children: Snippet; id: string; text: string; isWider?: boolean } = $props(); const modalId = `${id}-modal`; + + $effect(() => { + window.addEventListener("keydown", (e) => { + if (e.key === "Escape" && window.location.hash === `#${modalId}`) { + window.location.hash = "!"; + } + }); + }); {text} diff --git a/web-app/src/lib/db-schema.ts b/web-app/src/lib/db-schema.ts index 08845aa..4c59020 100644 --- a/web-app/src/lib/db-schema.ts +++ b/web-app/src/lib/db-schema.ts @@ -17,15 +17,16 @@ export interface Article { website_id: string; user_id: string | null; title: string; + slug: string | null; meta_description: string | null; meta_author: string | null; cover_image: string | null; - publication_date: Date | null; + publication_date: string | null; main_content: string | null; category: string | null; article_weight: number | null; - created_at: Date; - last_modified_at: Date; + created_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface ArticleInput { @@ -33,15 +34,16 @@ export interface ArticleInput { website_id: string; user_id?: string | null; title: string; + slug?: string | null; meta_description?: string | null; meta_author?: string | null; cover_image?: string | null; - publication_date?: Date | null; + publication_date?: string | null; main_content?: string | null; category?: string | null; article_weight?: number | null; - created_at?: Date; - last_modified_at?: Date; + created_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const article = { @@ -51,6 +53,7 @@ const article = { "website_id", "user_id", "title", + "slug", "meta_description", "meta_author", "cover_image", @@ -81,7 +84,7 @@ export interface ChangeLog { website_id: string | null; user_id: string | null; username: string; - tstamp: Date; + tstamp: string; table_name: string; operation: string; old_value: any | null; @@ -92,7 +95,7 @@ export interface ChangeLogInput { website_id?: string | null; user_id?: string | null; username?: string; - tstamp?: Date; + tstamp?: string; table_name: string; operation: string; old_value?: any | null; @@ -126,16 +129,16 @@ export interface Collab { website_id: string; user_id: string; permission_level: number; - added_at: Date; - last_modified_at: Date; + added_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface CollabInput { website_id: string; user_id: string; permission_level?: number; - added_at?: Date; - last_modified_at?: Date; + added_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const collab = { @@ -166,8 +169,8 @@ export interface DocsCategory { user_id: string | null; category_name: string; category_weight: number; - created_at: Date; - last_modified_at: Date; + created_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface DocsCategoryInput { @@ -176,8 +179,8 @@ export interface DocsCategoryInput { user_id?: string | null; category_name: string; category_weight: number; - created_at?: Date; - last_modified_at?: Date; + created_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const docs_category = { @@ -207,15 +210,15 @@ const docs_category = { export interface DomainPrefix { website_id: string; prefix: string; - created_at: Date; - last_modified_at: Date; + created_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface DomainPrefixInput { website_id: string; prefix: string; - created_at?: Date; - last_modified_at?: Date; + created_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const domain_prefix = { @@ -235,13 +238,13 @@ const domain_prefix = { export interface Footer { website_id: string; additional_text: string; - last_modified_at: Date; + last_modified_at: string; last_modified_by: string | null; } export interface FooterInput { website_id: string; additional_text: string; - last_modified_at?: Date; + last_modified_at?: string; last_modified_by?: string | null; } const footer = { @@ -263,7 +266,7 @@ export interface Header { logo_type: string; logo_text: string | null; logo_image: string | null; - last_modified_at: Date; + last_modified_at: string; last_modified_by: string | null; } export interface HeaderInput { @@ -271,7 +274,7 @@ export interface HeaderInput { logo_type?: string; logo_text?: string | null; logo_image?: string | null; - last_modified_at?: Date; + last_modified_at?: string; last_modified_by?: string | null; } const header = { @@ -300,14 +303,14 @@ export interface Home { website_id: string; main_content: string; meta_description: string | null; - last_modified_at: Date; + last_modified_at: string; last_modified_by: string | null; } export interface HomeInput { website_id: string; main_content: string; meta_description?: string | null; - last_modified_at?: Date; + last_modified_at?: string; last_modified_by?: string | null; } const home = { @@ -333,15 +336,15 @@ const home = { export interface LegalInformation { website_id: string; main_content: string; - created_at: Date; - last_modified_at: Date; + created_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface LegalInformationInput { website_id: string; main_content: string; - created_at?: Date; - last_modified_at?: Date; + created_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const legal_information = { @@ -365,7 +368,7 @@ export interface Media { blob: string; mimetype: string; original_name: string; - created_at: Date; + created_at: string; } export interface MediaInput { id?: string; @@ -374,7 +377,7 @@ export interface MediaInput { blob: string; mimetype: string; original_name: string; - created_at?: Date; + created_at?: string; } const media = { tableName: "media", @@ -397,7 +400,7 @@ export interface Settings { background_color_dark_theme: string; background_color_light_theme: string; favicon_image: string | null; - last_modified_at: Date; + last_modified_at: string; last_modified_by: string | null; } export interface SettingsInput { @@ -407,7 +410,7 @@ export interface SettingsInput { background_color_dark_theme?: string; background_color_light_theme?: string; favicon_image?: string | null; - last_modified_at?: Date; + last_modified_at?: string; last_modified_by?: string | null; } const settings = { @@ -440,7 +443,7 @@ export interface User { password_hash: string; user_role: string; max_number_websites: number; - created_at: Date; + created_at: string; } export interface UserInput { id?: string; @@ -448,7 +451,7 @@ export interface UserInput { password_hash: string; user_role?: string; max_number_websites?: number; - created_at?: Date; + created_at?: string; } const user = { tableName: "user", @@ -468,8 +471,8 @@ export interface Website { title: string; max_storage_size: number; is_published: boolean; - created_at: Date; - last_modified_at: Date; + created_at: string; + last_modified_at: string; last_modified_by: string | null; } export interface WebsiteInput { @@ -479,8 +482,8 @@ export interface WebsiteInput { title: string; max_storage_size?: number; is_published?: boolean; - created_at?: Date; - last_modified_at?: Date; + created_at?: string; + last_modified_at?: string; last_modified_by?: string | null; } const website = { diff --git a/web-app/src/lib/templates/blog/BlogArticle.svelte b/web-app/src/lib/templates/blog/BlogArticle.svelte index a2f334b..5edc7f8 100644 --- a/web-app/src/lib/templates/blog/BlogArticle.svelte +++ b/web-app/src/lib/templates/blog/BlogArticle.svelte @@ -19,6 +19,7 @@ nestingLevel={1} {apiUrl} title={article.title} + slug={article.slug as string} metaDescription={article.meta_description} {websiteUrl} /> diff --git a/web-app/src/lib/templates/blog/BlogIndex.svelte b/web-app/src/lib/templates/blog/BlogIndex.svelte index 9424d8f..28863fb 100644 --- a/web-app/src/lib/templates/blog/BlogIndex.svelte +++ b/web-app/src/lib/templates/blog/BlogIndex.svelte @@ -2,7 +2,7 @@ import Head from "../common/Head.svelte"; import Nav from "../common/Nav.svelte"; import Footer from "../common/Footer.svelte"; - import { md, slugify, type WebsiteOverview } from "$lib/utils"; + import { md, type WebsiteOverview } from "$lib/utils"; const { websiteOverview, @@ -62,7 +62,7 @@ {/if} - {article.title} + {article.title} {#if article.meta_description} diff --git a/web-app/src/lib/templates/common/Head.svelte b/web-app/src/lib/templates/common/Head.svelte index f25b977..a878c51 100644 --- a/web-app/src/lib/templates/common/Head.svelte +++ b/web-app/src/lib/templates/common/Head.svelte @@ -1,11 +1,12 @@ diff --git a/web-app/src/lib/templates/common/Nav.svelte b/web-app/src/lib/templates/common/Nav.svelte index fb7a558..14118fc 100644 --- a/web-app/src/lib/templates/common/Nav.svelte +++ b/web-app/src/lib/templates/common/Nav.svelte @@ -1,5 +1,5 @@
- {article.title} + {article.title}