{"id":28226054,"url":"https://github.com/rh1tech/zspa","last_synced_at":"2026-05-15T02:04:44.790Z","repository":{"id":86194846,"uuid":"449237744","full_name":"rh1tech/zspa","owner":"rh1tech","description":"ZOIA Single Page Application (ZSPA)","archived":false,"fork":false,"pushed_at":"2022-01-25T09:00:06.000Z","size":664,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-13T07:43:40.132Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rh1tech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-18T10:24:39.000Z","updated_at":"2025-03-11T23:29:09.000Z","dependencies_parsed_at":"2023-03-01T03:15:49.832Z","dependency_job_id":null,"html_url":"https://github.com/rh1tech/zspa","commit_stats":null,"previous_names":["rh1tech/zspa"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rh1tech/zspa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rh1tech%2Fzspa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rh1tech%2Fzspa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rh1tech%2Fzspa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rh1tech%2Fzspa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rh1tech","download_url":"https://codeload.github.com/rh1tech/zspa/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rh1tech%2Fzspa/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272267873,"owners_count":24903792,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-26T02:00:07.904Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-18T11:10:12.568Z","updated_at":"2026-05-15T02:04:44.507Z","avatar_url":"https://github.com/rh1tech.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ZSPA (ZOIA Single Page Application)\n\nZSPA is an engine which allows you to build a blazing fast and modern static website which doesn't require any server part to run. It's perfect to build static websites which won't require any server interaction such as admin panel, authentication etc.\n\nIt's a perfect use when you don't need any of advanced [ZOIA](https://github.com/xtremespb/zoia) features like database support, authentication, server-side rendering etc.\n\nYou may also wish to use ZSPA as a *boilerplate* to build a SPA based on Marko because it contains all the necessary build configuration and dependencies.\n\n## Features\n\n* Based on [Marko.js](https://markojs.com), a language for building dynamic and reactive user interfaces\n* Using [Bulma](https://bulma.io/), a free, open source framework that provides ready-to-use frontend components that you can easily combine to build responsive web interfaces\n* Using Webpack 5 to build an optimized, GZipped chunks and load every part of the site on demand\n* Built-in Routing and internationalization support\n\n## Demo\n\nCheck out the demo website at [zspa.zoiajs.org](https://zspa.zoiajs.org). It's currently not a part of CI/CD process so is not regularly updated.\n\n## Usage\n\nFirst, you need to clone the ZSPA from Github repository:\n\n```\ngit clone https://github.com/xtremespb/zspa.git\n```\n\nThen, you will need to install the required NPM modules and start the build process:\n\n```\nnpm i\nnpm run build-production\n```\n\nWhen successful, a demo website will be generated in the *./dist* directory.\n\n## Configuration files\n\nTo build your own website, you will need to change the configuration files according to your needs. All the required configuration files are located in the *./etc* directory.\n\n### routes.json\n\nDefine your routes here. [Router5](https://router5.js.org/) is used in the background, so you will need to use the corresponding syntax. Each route has the following structure:\n\n```\n{\n    \"name\": \"home\",\n    \"path\": \":language\u003c([a-z]{2}-[a-z]{2})?\u003e/\",\n    \"defaultParams\": {\n        \"language\": \"\"\n    }\n}\n```\n\nThe *:language* part is important in order for internationalization to work as it provides the current locale as part of an URL.\n\n### navigation.json\n\nDefine a list of routes which will be displayed at the top of the page as part of *navbar* component.\n\n```\n{\n    \"defaultRoute\": \"home\",\n    \"routes\": [\"home\", \"license\"]\n}\n```\n\n* **\"routes\"** parameter is an array of strings, each string represents a route ID which has been previously defined in *routes.json* configuration file.\n* **\"defaultRoute\"** defines a default route ID.\n\n### languages.json\n\nThis file is a starting point to define your website internationalization settings because it contains a list of available languages:\n\n```\n{\n    \"en-us\": \"English\",\n    \"ru-ru\": \"Русский\"\n}\n```\n\nEach language ID shall contain 5 characters and shall look like \"xx-xx\".\n\nThe first language in this list is the *default* language.\n\n### translations/xx-xx.json\n\nTranslation file for each language should be placed to the *./translations* directory. It's a key-value JSON format which is easy to read and modify:\n\n```\n{\n    \"title\": \"ZSPA\",\n    \"home\": \"Home Page\",\n    \"license\": \"License\"\n}\n```\n\nEvery language defined in *languages.json* shall be represented in this directory.\n\n### translations/core/xx-xx.json\n\nSame as above, every template-specific (core) translations shall be placed there. Every language defined in *languages.json* shall also be represented in this directory.\n\n### i18n-loader.js\n\nThis JavaScript file exports an async function called *loadLanguageFile* which is used to load the corresponding translation file. The switch operator is being used to choose between languages and import the required ones on demand.\n\nIn order to generate the chunks, Webpack is using the following syntax (used in this script) to generate the correct chunk names:\n\n```Javascript\ntranslationCore = await import(/* webpackChunkName: \"lang-core-en-us\" */ `./translations/core/en-us.json`);\ntranslationUser = await import(/* webpackChunkName: \"lang-en-us\" */ `./translations/en-us.json`);\n```\n\nIf you need to add a new translation language or to remove a not used one, you will need to modify this file accordingly.\n\n### pages-loader.js\n\nSame as *i18n-loader.js*, this file is intended to generate proper Webpack chunks. This loader exports an async function called *loadComponent* which chooses a proper Marko component based on a route name:\n\n```Javascript\nswitch (route) {\n    case \"home\":\n        return import(/* webpackChunkName: \"page.home\" */ \"../src/zoia/pages/home\");\n    case \"license\":\n            return import(/* webpackChunkName: \"page.license\" */ \"../src/zoia/pages/license\");\n    default:\n            return import(/* webpackChunkName: \"page.404\" */ \"../src/zoia/errors/404\");\n}\n```\n\nIf a route is unknown, the \"Not Found\" (error/404) component is loaded.\n\n## Configuring Bulma components\n\nIn order to reduce the size of CSS bundles, you may wish to configure the Bulma components which are really used in your project.\n\nTo do this, you will need to edit the *./etc/bulma.scs* and uncomment the imports you will need. By default, all modules available in Bulma are imported.\n\n## Creating Pages\n\nThe pages are just ordinary Marko components which shall be placed to the *./src/zoia/pages* directory. The directory of each page may contain the following files:\n\n* **index.marko**: a Marko template which contains the contents to be displayed when this route is loaded.\n* **marko.json**: a configuration file which defines the options, e.g. where to find the custom tags.\n* **component.js**: a JavaScript file which defines the page logic\n* **style.scss** (optional): a SCSS file which describes specific styles for the corresponding page.\n\nA minimal *index.marko* file may look like this:\n\n```HTML\n$ const { t } = out.global.i18n;\n\u003cdiv\u003e\n    \u003ch1 class=\"title\"\u003e${t(\"home\")}\u003c/h1\u003e\n    \u003cp\u003eThis is the home page!\u003c/p\u003e\n\u003c/div\u003e \n```\n\nYou may import the required methods or properties from the *i18n* library using the global scope and use them in your Marko file as shown above.\n\nThe *i18n* library exports the following:\n\n* **t(id)**: translate a variable using the locale files\n* **setLanguage(language)**: set a new language ID\n* **getLanguage()**: get an active language ID\n* **loadDefaultLanguage()**: load default language\n* **languages**: returns the contents of *languages.json* configuration file\n* **defaultLanguage**: ID of the default language\n\nA minimal *component.js* file may look like this:\n\n```Javascript\n/* eslint-disable import/no-unresolved */\nmodule.exports = class {\n    onCreate(input, out) {\n        const state = {\n            language: out.global.i18n.getLanguage(),\n        };\n        this.state = state;\n        this.i18n = out.global.i18n;\n        this.parentComponent = input.parentComponent;\n    }\n\n    async updateLanguage(language) {\n        if (language !== this.state.language) {\n            setTimeout(() =\u003e {\n                this.setState(\"language\", language);\n            });\n        }\n    }\n};\n```\n\nThe *updateLanguage* method is called when the user selects another locale in order do perform the necessary render actions to a page component.\n\nIf you need to apply the internationalization to the parts of your page, you will probably need to chunk every localized part. To do this, you will need the following changes in your *component.js* file.\n\nFirst, create a new state for a \"localized\" component:\n\n```Javascript\nconst state = {\n    language: out.global.i18n.getLanguage(),\n    currentComponent: null,\n};\n```\n\nThen, add a new async method to load your components as chunks:\n\n```Javascript\nasync loadComponent(language = this.i18n.getLanguage()) {\n    let component = null;\n    const timer = this.parentComponent.getAnimationTimer();\n    try {\n        switch (language) {\n        case \"ru-ru\":\n            component = await import(/* webpackChunkName: \"page.home.ru-ru\" */ \"./home-ru-ru\");\n            break;\n        default:\n            component = await import(/* webpackChunkName: \"page.home.en-us\" */ \"./home-en-us\");\n        }\n        this.parentComponent.clearAnimationTimer(timer);\n    } catch {\n        this.parentComponent.clearAnimationTimer(timer);\n        this.parentComponent.setState(\"500\", true);\n    }\n    this.setState(\"currentComponent\", component);\n}\n```\n\nYou will need to load those components when the page component is mounted:\n\n```Javascript\nonMount() {\n    this.loadComponent();\n}\n```\n\nAnd you will need to call the *loadComponent* method each time a user selects a different locale:\n\n```Javascript\nasync updateLanguage(language) {\n    if (language !== this.state.language) {\n        setTimeout(() =\u003e {\n            this.setState(\"language\", language);\n        });\n    }\n    this.loadComponent(language);\n}\n```\n\nThen, you need to modify *index.marko* in order to include the chunk:\n\n```HTML\n$ const { t } = out.global.i18n;\n\u003cdiv\u003e\n    \u003ch1 class=\"title\"\u003e${t(\"home\")}\u003c/h1\u003e\n    \u003c${state.currentComponent}/\u003e\n\u003c/div\u003e\n```\n\nFinally, you need to create the \"localized\" components:\n\n```\nhome-en-us\n- index.marko\nhome-ru-ru\n- index.marko\n```\n\nAnd modify the *home-xx-xx/index.marko* accordingly.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frh1tech%2Fzspa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frh1tech%2Fzspa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frh1tech%2Fzspa/lists"}