Notes

The schema

All pages are stored in the following dataformat, this is the same format as the API will present to you.

{
    "id": "string:id",     // Id of the article
    "author": "string:id", // Id of the author
    "category": "CATEGORY"/NULL, // A string for the category or NULL
    "favicon": "URL"/NULL,       // An url to a favicon or NULL
    "banner": "URL"/NULL,        // An url to a banner image or NULL
    "created": "yyyy-MM-ddThh:mm:ssZ", // ISO 8601 UTC
    "updated": "yyyy-MM-ddThh:mm:ssZ", // ISO 8601 UTC
    "hidden": BOOL,                    // Denotes whether this page is meant to be hidden, this will hide it on the public CMS page, however it can still be read through the /page or /pages?hidden=true endpoints
    "languages": {
        "string:lang": {                 // A language-code
            "title": "string",           // The title text of the page
            "preview": "string",         // Auto generated preview based of content or a set static string
            "content": "string:markdown" // The actuall markdown content of the page in the CMS-Markdown flavor.
        },
        ...
    },
    // Styling is optional, any non defined selectors or properties should just be defaulted by the frontend.
    "style": {
        "string:selector": {                  // The element selector
            "string:property": "string:value", // Setting the value for a property
            ...
        },
        ...
    },
    // Preferences are optional "feature flags" that can be used to specify how the author wants the page to be viewed. However it's up to the frontend to respect them.
    "prefers": {
        "string:preferenceKey": BOOL, // Defines that this page prefers something
        ...
    }
}

All responses allways additionally include the following fields:

{
    "success": BOOL,         // A boolean declaring if the request was successfull or not.
    "message": "string"/NULL // May contain a message describing what the API did.
}
or incase of an error
{
    "success": BOOL,
    "message": "string"/NULL,
    "error": "string"/NULL    // Incase of errors a description will be given here.
}

If message or error is an empty string that can by all accounts be treated as if it was null.

Styling

Styling use a CSS-like subset where selectors are only element-types.

Selectors

CMS-Markdown and the viewer generates the following tags:

These semantic tags are also spec-supported:

Properties

The following style properties are allowed:

Property value units

Granual selection

Selecting based on id or classes are not supported, rather to make a custom selection wrap your section in ... then use identifier as selector.

Styling sandboxed site

When sites have boxedallowjs prefered to true they get placed inside an iframe, to avoid the public CMS site styling your page background add data-override-page-bg="true" inside your body element.

Preferences

Currently the public CMS website respects:

Endpoints

Get all pages

GET https://www.simonk.ntigskovde.se/cms/api/pages?hidden=<bool>&content=<bool>&lang=<string:lang>&as=<string:format>

To fetch all pages of the CMS one can call the /pages endpoint, optionaly include ?hidden/?hidden=true to include hidden pages in the result and/or ?content/?content=true to include the content field of each language of each page.
One can also add ?lang=... to filter each page for a specific language code.
If content is enabled its by default returned as markdown in the custom CMS flavor, however using the ?as= parameter one can chose the format.


Example

GET https://www.simonk.ntigskovde.se/cms/api/pages
Response{
    "success": true,
    "message": null,
    "pages": [
        {
            "id": "985d542d",
            "author": "4da17ca9",
            "category": "bobs-container",
            "favicon": null,
            "banner": null,
            "created": "2025-10-06T13:00:42Z",
            "updated": null,
            "hidden": false,
            "languages": {
                "en-us": {
                    "title": "Bob's Blog",
                    "preview": "Hello world, This is a test page..."
                    // The "content" field would be here if ?content=true
                }
            },
            "style": {
                "article": {
                    "color": "blue",
                    "background-color": "red"
                }
            },
            "prefers": {
                "fullscreen": false
            }
        }
    ],
    ...
}

Get a page

GET https://www.simonk.ntigskovde.se/cms/api/page?id=<string:id>&lang=<string:lang>&as=<string:format>

Fetches all details for a page by id provided by the id parameter.
One can also add ?lang=... to filter the page for a specific language code.
By default content is returned as markdown in the custom CMS flavor, however using the ?as= parameter one can chose the format.


Example

GET https://www.simonk.ntigskovde.se/cms/api/page?id=985d542d
Response{
    "success": true,
    "message": "",
    "page": {
        "id": "985d542d",
        "author": "4da17ca9",
        "category": "bobs-container",
        "favicon": null,
        "banner": null,
        "created": "2025-10-06T13:00:42Z",
        "updated": null,
        "hidden": false,
        "languages": {
            "en-us": {
                "title": "Bob's Blog",
                "preview": "Hello world, This is a test page...",
                "content": "# Hello world\nThis is a test page in english."
            }
        },
        "style": {
            "article": {
                "color": "blue",
                "background-color": "red"
            }
        },
        "prefers": {
            "fullscreen": false
        }
    }
}

Add a page

HeadersAuthorization: Bearer 1234567890abcdef1234567890abcdef
Request
POST https://www.simonk.ntigskovde.se/cms/api/page/add
{
    "languages": {
        "string:lang": {
            "content": "string:markdown"
        }
    }
}

To add a page one must provide atleast the languages field with atleast one entry with the content field. The other fields will be defaulted according to the schema,except for id, created, updated and author since they are added by the API for you.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/page/add
{
    "languages": {
        "en-us": {
            "content": "MARKDOWN"
        }
    }
}
Response{
    "success": true,
    "message": "Successfully added page",
    "id": "1234abcd"
}

Edit a page

HeadersAuthorization: Bearer 1234567890abcdef1234567890abcdef
Request
POST https://www.simonk.ntigskovde.se/cms/api/page/edit
{
    "id": "string:id",
    "fields": {
        ...
    }
}

To edit a page one provides a diff of the fields you want to change, so one includes only the fields they want to change with their new values. (fields according to the schema)
One can also unset a field by setting the value to null which will cause the API to default the value. This also works for some higher level fields like style.
The diff is placed under the fields field, in addition to it one must also provide the id field with the ID of the page to change.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/page/edit
{
    "id": "1234abcd",
    "fields": {
        "languages": {
            "en-us": {
                "content": "# This is markdown!"
            }
        }
    }
}
Response{
    "success": true,
    "message": "Page edited successfully"
}

Remove a page

HeadersAuthorization: Bearer 1234567890abcdef1234567890abcdef
Request
POST https://www.simonk.ntigskovde.se/cms/api/page/rem
{
    "id": "string:id",
}

To remove a page one can send a request to the /page/rem endpoint with a field id with the page ID you want to remove.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/page/rem
{
    "id": "1234abcd",
}
Response{
    "success": true,
    "message": "Page removed successfully"
}

Account Endpoints

Signup and request a key

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/request
{
    "username": "string",
    "passwordhash": "string:sha256"
}

To use the api you must retrieve a API access token/key, to this one can call the /acc/request endpoint or use the public CMS site.
When using the endpoint one should provide two fields username and passwordhash, where the hash is a sha256 of the password.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/request
// Signup that also requests a key
{
    "username": "test",
    "passwordhash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
Response{
    "success": true,
    "message": "Successfully added account with key for: test",
    "key": "1234567890abcdef1234567890abcdef", // Your access token/key
    "id": "1234abcd" // Your new account ID
}

Reroll a key

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/refresh
{
    "username": "string",
    "passwordhash": "string:sha256"
}

Sometimes you need to get a fresh access token/key, this can be done either through the /acc/refresh endpoint or the public CMS site.
When using the endpoint one should provide two fields username and passwordhash.
Note! Refreshing your key will invalidate your previous one!


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/refresh
// Not a signup, but a login
{
    "username": "test",
    "passwordhash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
Response{
    "success": true,
    "message": "Successfully refreshed access key for: bob",
    "key": "1234567890abcdef1234567890abcdef", // This is the new key
}

Change username

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/username
{
    "username": "string",
    "passwordhash": "string:sha256",
    "new_username": "string",
}

Changing your username can be done through an endpoint or the public CMS site, API wise one can call /acc/username with your current username under the username field, your current passwordhash under passwordhash and finally your new username under the new_username field.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/username
{
    "username": "test",
    "passwordhash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "new_username": "test2",
}
Response{
    "success": true,
    "message": "Successfully changed username from 'test' to 'test2'"
}

Change password

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/password
{
    "username": "string",
    "passwordhash": "string:sha256",
    "new_passwordhash": "string:sha256",
}

Changing your password can be done through an endpoint or the public CMS site, API wise one can call /acc/password with your current passwordhash under the passwordhash field, your username under the username field and your new passwordhash under new_passwordhash.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/password
{
    "username": "test",
    "passwordhash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "new_passwordhash": "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752",
}
Response{
    "success": true,
    "message": "Successfully changed password for: test"
}

Remove your account and invalidate your key

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/unlink
{
    "username": "string",
    "passwordhash": "string:sha256",
}

To remove your API account and access key/token one can go to the public CMS site or call the /acc/unlink endpoint with your current credentials. (username and passwordhash fields)
Note! This action is irreverisble and won't currently remove your pages, plesae remove your pages manuall before removing your account!


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/unlink
{
    "username": "test",
    "passwordhash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
}
Response{
    "success": true,
    "message": "Successfully removed account: test"
}

Get the username linked to an account ID

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/query
{
    "id": "string:id"
}

To fetch the username for an ID one can call the /acc/query endpoint with the id field set to the account ID you want to fetch.


Example

Request
POST https://www.simonk.ntigskovde.se/cms/api/acc/query
{
    "id": "1234abcd"
}
Response{
    "success": true,
    "message": "Successfully retrieved username for account id '1234abcd'",
    "username": "test" // The username you fethed
}

CMS Markdown Flavor

The CMS Markdown flavor supports all the basic markdown syntax with some additions:



CMS Markdown JSON Representation

The JSON representation is an attempt to represent the content as a generic format, meaning its not as tied to web technologies. (Note however that many naming conventions are the same)

The JSON is structured as an array of elements each element follows the syntax of {"<type>": <content>, ...properties...} where <content> is either regular text as a string or an array of other elements.

When <content> is an array of other elements that array is called an encapsulation.

Elements can also be simple and complex, simple elements have no content and their <type> is the literal string type, for example {"type":"hr"}.
Complex elements have their content as an object/dictionary with fields.

Since CMS markdown can handle custom tags so can the JSON representation, in that case the identifier is used as the <type>, for example <identifier>content</identifier> becomes {"identifier": "content"}.

If an element would have had properties in HTML they can be represented by adding more fields to the element, example {"a": "Example", "href": "example.com"}.

One special element is {"type": "n"} this is a newline, but there is also {"type": "br"}?
Yes, {"type": "br"} stems from the HTML element <br> which is allowed to be used inside CMS markdown, thus it has a tag.
Meanwhile {"type": "n"} is for the literal newline character, and is generated for line breaks inside paragraphs.


Elements

<Type> Has Content Properties ElementType (simple, complex) Description
type No simple br is newline, and hr is horizontal rule, n is a literal newline
h1 Yes regular Header 1
h2 Yes regular Header 2
h3 Yes regular Header 3
h4 Yes regular Header 4
h5 Yes regular Header 5
h6 Yes regular Header 6
b Yes regular Makes text bold
i Yes regular Makes text italic
s Yes regular Makes text strikethrough
u Yes regular Makes text underlined
sub Yes regular Makes text subscript
sup Yes regular Makes text superscript
code Yes regular Inline code elemenets like i am code
codeblock Yes lang if codeblock has lang regular Multiline codeblocks optionally with a langauge
link Yes alt regular Link to urls
img Yes alt regular Embed images
video Yes alt controls regular Embed Videos
audio Yes alt controls regular Embed Audios
li Yes (always encapsulation) decoration regular list decoration is either line or point or an alphanumerical
table Yes (object/dictionary) complex Has two fields header and rows both are arrays of elements
blockqoute Yes (always encapsulation) regular Render a line as a part of a blockqoute


Example

[
    {"h1": "Heading Level 1"},
    {"h2": "Heading Level 2"},
    {"h3": "Heading Level 3"},
    {"h4": "Heading Level 4"},
    {"h5": "Heading Level 5"},
    {"h6": "Heading Level 6"},
    // Any tag whos type is "type" is a simple-tag, meaning it has no content
    {"type": "hr"},
    // Bellow is a paragraph thus an array with multiple elements inside, inside here we can find {"type":"n"} newlines which are not nessecary between paragraph boundaries
    [
        // As you can se regular text is just represented as strings
        "Paragraph with ",
        {"b": "bold"},
        ", ",
        {"i": "italic"},
        ", ",
        // Here the italic is encapsulated inside the bold's content and thus bolds contant is an array
        {"b": [
            {"i": "bold+italic"}
        ]},
        ", ",
        {"s": "strikethrough"},
        ", ",
        {"type": "n"}, // n = newline
        {"u": "underline"},
        ", ",
        {"sup": "superscript"},
        ", and ",
        {"sub": "subscript"},
        "."
    ],
    [
        "Mixed inline formatting: ",
        // Here the alt is an array instead of a string, alt is property of link type
        {"link": "example.com", "alt": [
            {"i": "italic link text"}
        ]},
        " and ",
        {"type": "n"},
        {"b": [
            {"i": "bold+italic"}
        ]},
        " inside a link: ",
        {"link": "openai.com", "alt": [
            {"b": [
                {"i": "important"}
            ]}
        ]},
        "."
    ],
    [
        "Inline code: ",
        {"code": "print(\"Hello World\")"} // code is inline code, codeblock is precode and has "lang" property optionally
    ],
    {"type": "hr"},
    {"h2": "Links and Media"},
    [
        "Standard link: ",
        {"link": "openai.com", "alt": "OpenAI"},
        {"type": "n"},
        "Image: ",
        {"img": "placekitten.com/200/300", "alt": "Alt text"},
        {"type": "n"},
        "Video: ",
        {"video": "example.com/video.mp4", "alt": "A video file", "controls": true},
        {"type": "n"},
        "Audio: ",
        {"audio": "example.com/audio.mp3", "alt": "An audio file", "controls": true}
    ],
    {"type": "hr"},
    {"h2": "Code Blocks"},
    {"codeblock": "Non-language code block\n    indentation preserved"},
    {"codeblock": "{\n  \"note\": \"json multiline codeblock\",\n  \"list\": [1, 2, 3],\n  \"nested\": {\"key\": \"value\"}\n}", "lang": "json"},
    {"codeblock": ", "lang": "php"},
    {"type": "hr"},
    {"h2": "Tables"},
    // Tables are complex-tags meaning their content is an object.
    {
        "table": {
            "header": ["Name", "Age", "Role"],
            "rows": [
                ["Alice", "25", "Engineer"],
                ["Bob", "30", "Designer"],
                ["Charlie", "35", "Manager"]
            ]
        }
    },
    "Another table with inline formatting:",
    {
        "table": {
            // Header and rows are encapsulations of proper elements meaning they can contain regular-text or format-elements
            "header": ["Feature", "Example"],
            "rows": [
                ["Bold", {"b": "text"}],
                ["Italic", {"i": "text"}],
                ["Link", {"link": "example.com", "alt": "link"}],
                ["Inline Code", {"code": "code"}]
            ]
        }
    },
    {"type": "hr"},
    {"h2": "Custom Tags"},
    [
        // Custom tags are represented with their identifier as their type. so content becomes {"identifier": "content"}
        //   and any properties also gets stored example: content becomes {"identifier": "content", "prop": "value"}
        {"custom": "inside-custom"},
        " custom tags are handled just like HTML inside markdown",
        {"type": "n"},
        {"custom": [
            {"i": "inside-custom"}
        ]},
        " markdown may exist inside HTML-style-tags"
    ],
    [
        // HTML elements are treated just like customs
        {"span": "html-tags-work-aswell"}
    ],
    [
        "See even self-closing",
        {"type": "br"},
        "(explicit and not)",
        {"type": "br"}
    ],
    {"type": "hr"},
    {"h2": "Edge Cases"},
    // As li's in HTML are under a parent ul/ol element we encapsulate them in an array
    [
        // If list-item content is just regular text the content becomes a string
        {"li": "Hyphenated-word (should not become strikethrough)."},
        // If list-item content has multiple elements the content becomes an encapsulation-array
        {"li": [
            "Multiple ",
            {"b": [
                "nested ",
                {"i": "formats"}
            ]},
            " in one phrase."
        ]},
        {"li": [
            "Triple asterisk boundary ",
            {"b": [
                {"i": "test"}
            ]},
            "."
        ]},
        {"li": [
            "Code span with symbols: ",
            {"code": "**not bold** -not strike-"},
            "."
        ]},
        {"li": [
            "Placeholder check with ",
            {"link": "example.com", "alt": [
                {"img": "placekitten.com/50/50", "alt": "nested image"}
            ]},
            "."
        ]}
    ],
    {"type": "hr"},
    {"h1": "Header1"},
    {"h1": "Header1NoSpace"},
    {"h2": "Header2"},
    {"h2": "Header2NoSpace"},
    {"h3": "Header3"},
    {"h3": "Header3NoSpace"},
    {"h4": "Header4"},
    {"h4": "Header4NoSpace"},
    {"h5": "Header5"},
    {"h5": "Header5NoSpace"},
    {"h6": "Header6"},
    {"h6": "Header6NoSpace"},
    "text",
    {"i": "italic"},
    {"b": "bold"},
    {"b": [
        {"i": "bold+italic"}
    ]},
    {"s": "strikethrough"},
    {"u": "underline"},
    {"sup": "superscript"},
    {"sub": "subscript"},
    {"code": "inline code"},
    {"codeblock": "Non language code block\n    indentation preserved"},
    {"codeblock": "{\n  \"note\": \"json multiline codeblock, indentation preserved\",\n}", "lang": "json"},
    {"link": "url", "alt": "text"},
    {"img": "image-url", "alt": "alt text"},
    {"video": "video-url", "alt": "alt", "controls": true},
    {"audio": "audio-url", "alt": "alt", "controls": true},
    {"link": "url", "alt": [
        {"i": "alt can be styled too"}
    ]},
    [
        // li's are by default line-lists else can be specified with decoration property
        {"li": "line lists"},
        {"li": "point lists", "decoration": "point"},
        // If a number or alphabet we set decoration to the number/alphabet, meanwhile "point" and "line" are hardcoded.
        {"li": "numbered lists", "decoration": "1"},
        {"li": "numbered lists with letters", "decoration": "a"},
    ],
    {
        "table": {
            "header": ["Header1", "Header2", "Header3"],
            "rows": [
                ["row1col1", "row1col2", "row1col3"],
                [
                    {"i": "row2col1-styled"},
                    "row2col2",
                    "row2col3"
                ]
            ]
        }
    },
    {"custom": "custom tags"},
    {"span": "html tags work as well"},
    [
        "See even self-closing",
        {"type": "br"},
        "(explicit and not)",
        {"type": "br"}
    ],
    // Since "--- horizontal rule" is not a valid horizontal rule we treat it as regular text
    "--- horizontal rule",
    [
        "any compatible styles can of course be nested: ",
        {"b": [
            "bold and ",
            {"i": "italic"},
            " and ",
            {"s": "strikethrough"},
            " and ",
            {"u": "underline"},
            " and ",
            {"sup": "superscript"},
            " and ",
            {"sub": "subscript"}
        ]}
    ],
    // blockqoute is also an encapsaulation type
    {"blockqoute": [
        "This text is a blockquote.",
        "this too. (lvl1)",
        {"blockqoute": [
            "This is a nested blockquote. (lvl2)",
            {"blockqoute": [
                "This is a nested blockquote. (lvl3)",
                {"blockqoute": [
                    "This is a nested blockquote. (lvl4)",
                    "this too (lvl4)",
                    {"blockqoute": [
                        "This is a nested blockquote. (lvl5)",
                        {"blockqoute": [
                            "This is a nested blockquote. (lvl6)",
                            {"blockqoute": [
                                "This is a nested blockquote. (lvl7)"
                            ]}
                        ]}
                    ]}
                ]}
            ]}
        ]}
    ]}
]