{"id":26234915,"url":"https://github.com/flowpack/flowpack.decoupledcontentstore","last_synced_at":"2025-04-22T14:53:07.485Z","repository":{"id":38616792,"uuid":"392638367","full_name":"Flowpack/Flowpack.DecoupledContentStore","owner":"Flowpack","description":"This is the 2nd generation of a Two-Stack CMS package for Neos.","archived":false,"fork":false,"pushed_at":"2025-04-07T10:54:48.000Z","size":710,"stargazers_count":7,"open_issues_count":7,"forks_count":3,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-04-14T04:40:31.838Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Flowpack.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":"2021-08-04T10:02:39.000Z","updated_at":"2025-04-07T07:48:31.000Z","dependencies_parsed_at":"2024-01-17T13:49:15.924Z","dependency_job_id":"f176cb8e-c532-4b45-80d7-f9c567bd137c","html_url":"https://github.com/Flowpack/Flowpack.DecoupledContentStore","commit_stats":{"total_commits":105,"total_committers":9,"mean_commits":"11.666666666666666","dds":0.4380952380952381,"last_synced_commit":"f2ab47f291e62ec788d0135b2249f0fb35b0581a"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowpack%2FFlowpack.DecoupledContentStore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowpack%2FFlowpack.DecoupledContentStore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowpack%2FFlowpack.DecoupledContentStore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowpack%2FFlowpack.DecoupledContentStore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Flowpack","download_url":"https://codeload.github.com/Flowpack/Flowpack.DecoupledContentStore/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250263713,"owners_count":21401934,"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","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-03-13T02:30:03.618Z","updated_at":"2025-04-22T14:53:07.464Z","avatar_url":"https://github.com/Flowpack.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Decoupled Content Store based on Redis\n\nThis is the 2nd generation of a Two-Stack CMS package for Neos.\n\n**This Package is used in production in a bigger instance.**\n\nThe Content Store package is one part of a [Two-Stack CMS](https://martinfowler.com/articles/two-stack-cms/)\nsolution with Neos. A Two-Stack architecture separates editing and publishing\nfrom the delivery of content. This is also an architecture that's suitable to+\nintegrate Neos content in various other systems without adding overhead during\ndelivery.\n\nThe first iteration was not open source; developed jointly by [Networkteam](https://networkteam.com/) and [Sandstorm](https://sandstorm.de/)\nand is in use for several large customers. The second iteration (this project) is developed from scratch, in an open-source\nway, based on the learnings of the first iteration. Especially the robustness has been greatly increased.\n\n## What does it do?\n\nThe Content Store package publishes content from Neos to a Redis database as\nimmutable _content releases_. These releases can be atomically switched and\na _current release_ points to the active release.\n\nThe _delivery layer_ in the Two-Stack architecture uses the _current release_\nand looks for matching URLs in the _content store_ and delivers the pre-rendered\ncontent. A _delivery layer_ is decoupled from the actual Neos CMS and can be\nimplemented in any language or framework. It is also possible to integrate the\ndelivery layer part in another software (e.g. a shop system) as an extension.\n\n## Features\n\n- Publish a full, read-only snapshot of your live content to Redis in a so-called *Content Release*\n- allows for *incremental publishing*; so if a change is made, only the needed pages are re-rendered. This is\n  *integrated with the Neos Content Cache*; so cache flushings work correctly.\n- Integration with Neos workspace publishing for automatic incremental\n  publishing to the Content Store\n- Configurable Content Store format, decoupled from the internal representation in Neos.\n- Extensibility: Enrich content releases with your custom data.\n- Allows parallel rendering\n- Allows copying the content releases to different environments.\n- Allows rsyncing persistent assets around (should you need it)\n- Backend module with overview of _content releases_ (current release, switching\n  releases, manual publish)\n\nThis project is using the go-package [prunner](https://github.com/Flowpack/prunner) and [its Flow Package wrapper](https://github.com/Flowpack/Flowpack.Prunner)\nas the basis for orchestrating and executing a content release.\n\n## Requirements\n\n- Redis\n- Sandstorm.OptimizedCacheBackend is required when this package is used with Neos 7.3 (Neos 8 already has an optimized Redis backend)\n- Prunner\n\nStart up prunner via the following command:\n\n```bash\nprunner/prunner --path Packages --data Data/Persistent/prunner\n```\n\nCopy the `pipelines_template.yml` file into your project and adjust it as needed (see below and the comments in the file for explanation).\n\n## Approach to Rendering\n\nThe following flow chart shows the rendering pipeline for creating a content release.\n\n```                                                                                                 \n                   ┌─────────────────────┐                                                      \n                   │   Node Rendering    │                                                      \n ┌───────────┐     │   ┌─────────────┐   │     ┌───────────┐     ┌───────────┐     ┌───────────┐\n │   Node    │     │   │Orchestrator │   │     │  Release  │     │Transfer to│     │  Atomic   │\n │Enumeration│────▶│   └─────────────┘   │────▶│Validation │────▶│  Target   │────▶│  Switch   │\n └───────────┘     │┌────────┐ ┌────────┐│     └───────────┘     └───────────┘     └───────────┘\n                   ││Renderer│ │Renderer││                                                      \n                   └┴────────┴─┴────────┴┘                                                      \n```\n\n- At the beginning of every render, all nodes are **enumerated**. The Node Enumeration contains all pages\n  which need to be in the final content release.\n\n- Then, the rendering takes place. In parallel, the **orchestrator** checks if pages are already fully rendered. If no,\n  he creates rendering jobs. If yes, the rendered page is added to the in-progress content release.\n  \n  The **renderers** simply render the pages as instructed by the orchestrator.\n\n  The **orchestrator** tries to render multiple times: It can happen that after a render, the rendering did not\n  successfully work, because an editor has changed pages at the same time; leading to content cache flushes and\n  \"holes\" in the output.\n\n- During **validation**, checks can happen to see whether the content release is fully complete; to check whether\n  it really can go online.\n\n- During the **transfer** phase, the finished content release is copied to the production Redis instance if needed.\n  This includes copying of assets if needed.\n\n- In the **switch** phase, the content release goes live.\n\nThe above pipeline is implemented with [prunner](https://github.com/Flowpack/prunner) which is orchestrating\nthe different steps.\n\n## Infrastructure\n\nHere, we explain the different infrastructure and setup constraints for using the content store.\n\n- The Neos Content Cache must use Redis. It can use the OptimizedRedisCacheBackend.\n- The Content Store needs a separate Redis Database, but it can run on the same server.\n\n**It is crucial that Redis is available via lowest latency for Neos AND the Delivery Layer.** See the different\nsetup scenarios below for how this can be done.\n\n### Minimal Setup\n\nThe minimal setup looks as follows:\n\n- Neos writes into the Content Store Redis Database, and the Delivery Layer reads from the Content Store Redis Database.\n- Assets (persistent resources) are written directly to a publicly available Cloud Storage such as S3.\n\n```\n┌──────────────┐   ┌──────────────┐            \n│ Neos Content │   │Content Store │            \n│Cache Redis DB│   │   Redis DB   │◀───┐       \n└──────────────┘   └──────────────┘    │       \n        ▲                  ▲           │       \n        └────────┬─────────┘           │       \n                 │                     │       \n             ╔══════╗          ╔══════════════╗\n             ║ Neos ║          ║Delivery Layer║\n             ╚══════╝          ╚══════════════╝\n                 │                             \n                 │                             \n                 │       ┌──────────────┐      \n                 │       │Asset Storage │      \n                 └──────▶│   (S3 etc)   │      \n                         └──────────────┘      \n```\n\nIn this case, the *transfer* phase does not need to do anything, and you need to configure Neos to use the cloud\nstorage (e.g. via [Flownative.Google.CloudStorage](https://github.com/flownative/flow-google-cloudstorage) or\n[Flownative.Aws.S3](https://github.com/flownative/flow-aws-s3/)) for resources.\n\n**This is implemented in the default `pipelines_template.yml`.**\n\n**This Setup should be used if:**\n- the Delivery Layer and Neos are in the same data center (or host), so both can access Redis via lowest latencies\n- you want the easiest possible setup.\n\nIf you use Cloud Asset Storage, ensure that you **never delete** assets from there. For `Flownative.Aws.S3`,\nyou can [follow the guide on \"Preventing Unpublishing of Resources in the Target\"](https://github.com/flownative/flow-aws-s3/#preventing-unpublishing-of-resources-in-the-target).\n\n### Manually Sync Assets to the Delivery Layer via RSync\n\nIf you can not to use a Cloud Asset Storage, there's a built-in feature to manually sync assets to the delivery\nlayer(s) via RSync.\n\nTo enable this, you need to follow the following steps:\n\n1. Configure in `Settings.yaml`:\n\n    ```yaml\n    Flowpack:\n      DecoupledContentStore:\n        resourceSync:\n          targets:\n            -\n              host: localhost\n              port: ''\n              user: ''\n              directory: '../nginx/frontend/resources/'\n    ```\n\n2. In `pipelines.yml`, underneath `4) TRANSFER`, comment-in the `transfer_resources` task.\n\n### Copy Content Releases to a different Redis instance\n\n**This Setup should be used if:**\n- the Delivery Layer and Neos are in *different* data centers, so that there is a higher latency between one of the instances toward Redis\n- Or you need multiple delivery layers with different content states, with e.g. a *staging* delivery layer and a *live* delivery layer.\n\n```\n┌──────────────┐   ┌──────────────┐                   ┌──────────────┐\n│ Neos Content │   │Content Store │                   │Content Store │\n│Cache Redis DB│   │   Redis DB   │  ┌ ─ ─ ─ ─ ─ ─ ─ ▶│   Redis DB   │\n└──────────────┘   └──────────────┘    Higher         └──────────────┘\n        ▲                  ▲         │ Latency                ▲       \n        └────────┬─────────┘                                  │       \n                 │                   │                        │       \n             ╔══════╗                                 ╔══════════════╗\n             ║ Neos ║─ ─ ─ ─ ─ ─ ─ ─ ┘                ║Delivery Layer║\n             ╚══════╝                                 ╚══════════════╝\n                 │                                                    \n                 │                                                    \n                 │       ┌──────────────┐                             \n                 │       │Asset Storage │                             \n                 └──────▶│   (S3 etc)   │                             \n                         └──────────────┘                                                 \n```\n\nIn this case, the content store Redis DB is **explicitly synced** by Neos to another Delivery layer.\n\nTo enable this feature, do the following:\n\n1. Configure the additional Content Stores in `Settings.yaml` underneath `Flowpack.DecoupledContentStore.redisContentStores`.\n   The key is the internal identifier of the content store:\n\n    ```yaml\n    Flowpack:\n      DecoupledContentStore:\n        redisContentStores:\n          live:\n            label: 'Live Site'\n            hostname: my-redis-hostname\n            port: 6379\n            database: 11\n          staging:\n            label: 'Staging Site'\n            hostname: my-staging-redis-hostname\n            port: 6379\n            database: 11\n    ```\n\n2. In `pipelines.yml`, underneath `4) TRANSFER`, comment-in and adjust the `transfer_content` task.\n\n3. In `pipelines.yml`, underneath `5) TRANSFER`, comment-in the additional `contentReleaseSwitch:switchActiveContentRelease` commands.\n\n\u003e **Alternative: Redis Replication**\n\u003e \n\u003e Instead of the explicit synchronization described here, you can also use [Redis Replication](https://redis.io/topics/replication)\n\u003e to synchronize the primary Redis to the other instances.\n\u003e\n\u003e Using Redis replication is transparent to Neos or the Delivery Layer.\n\u003e \n\u003e To be able to use Redis replication, the Redis *secondary* (i.e. the delivery-layer's instance)\n\u003e needs to connect to the primary Redis instance.\n\u003e \n\u003e For the explicit synchronization described here, the Redis instances do not need to communicate directly\n\u003e with each other; but Neos needs to be able to reach all instances.\n\n## Incremental Rendering\n\nAs a big improvement for stability (compared to v1), the rendering pipeline does not make a difference whether\nit is a full or an incremental render. To trigger a full render, the content cache is flushed before\nthe rendering is started.\n\n### Options\nAfter changing an Asset (e.g. in the Media Module) an incremental rendering is triggered.\nYou can opt out of this behavior by setting the following configuration:\n````yaml\nFlowpack:\n  DecoupledContentStore:\n    startIncrementalReleaseOnAssetChange: false\n````\n\n### What happens if edits happen during a rendering?\n\nIf a change by an editor happens during a rendering, the content cache is flushed (by tag) as a result of\nthis content modification. Now, there are two possible cases:\n\n- the document (which was modified) has not been rendered yet inside the current rendering. In this case,\n  the rendered document would contain the recent changes.\n- the document was already rendered and added to the content release. **In this case, the rendered\n  document would *not* contain the recent changes**.\n\nThe 2nd case is a bit dangerous, in the sense that we need a re-render to happen soon; otherwise we would\nnot converge to a consistent state.\n\nFor use cases like scheduling re-renders, `prunner` supports a *concurrency limit* (i.e. how many\njobs can run in parallel) - and if this limit is reached, it supports an additional *queue* which can\nbe also limited.\n\nSo the following lines from `pipelines.yml` are crucial:\n\n```yaml\npipelines:\n  do_content_release:\n    concurrency: 1\n    queue_limit: 1\n    queue_strategy: replace\n```\n\nSo, if a content release is currently running, and we try to start a new content release, then this task is\nadded to the queue (but not yet executed). In case there is already a rendering task queued, this gets replaced\nby the newer rendering task.\n\n**This ensures that we have at most one content release running at any given time; and at most one content-release\nin the wait-list waiting to be rendered.** Additionally, we can be sure that scheduled content releases will be\neventually executed, because that's prunner's job.\n\n## Extensibility\n\n### Custom `pipelines.yml`\n\nCrafting a custom `pipelines.yml` is the main extension point for doing additional work (f.e. additional enumeration\nor rendering).\n\n### Custom Document Metadata, integrated with the Content Cache\n\nSometimes, you need to build additional data structures for every individual document. Ideally, you'll want this\nstructure to be integrated with the content cache; i.e. only refresh it if the page has changed.\n\nPerformance-wise, it is clever to do this at the same time as the rendering itself, as the content nodes\n(which you'll usually need) are already loaded in memory. You can register a\n`Flowpack\\DecoupledContentStore\\NodeRendering\\Extensibility\\DocumentMetadataGeneratorInterface` in `Settings.yaml`:\n\n```yaml\nFlowpack:\n  DecoupledContentStore:\n    extensions:\n      documentMetadataGenerators:\n        'yourMetadataGenerator':\n          className: 'Your\\Extra\\MetadataGenerator'\n```\n\nWhen you implement this class, you can add additional Metadata which is serialized to the Neos content cache\nfor every rendered document.\n\nOften, you'll also want to add another `contentReleaseWriter` which reads the newly added metadata and adds\nit to the final content release. Read the next section how this works.\n\n### Custom Content Release Writer\n\nYou can completely define how a content release is laid out in Redis for consumption by your delivery layer.\n\nBy implementing a custom `ContentReleaseWriter`, you can specify how the rendered content is stored in Redis.\n\nAgain, this is registered in `Settings.yaml`:\n\n```yaml\nFlowpack:\n  DecoupledContentStore:\n    extensions:\n      contentReleaseWriters:\n        'yourMetadataReleaseWriter':\n          className: 'Your\\Extra\\MetadataWriter'\n```\n\n### Writing Custom Data to the Content Release\n\nIn case you write custom data to the content release (using `$redisKeyService-\u003egetRedisKeyForPostfix($contentReleaseIdentifier, 'foo')`), you need to register\nthe custom key also in the settings:\n\n```yaml\nFlowpack:\n  DecoupledContentStore:\n    redisKeyPostfixesForEachRelease:\n      foo:\n        transfer: true\n```\n\nThis is needed so that the system knows which keys should be synchronized between the different content stores,\nand what data to delete if a release is removed.\n\n### Rendering additional nodes with arguments (e.g. pagination or filters)\n\nIf you render a paginated list or have filters (with a predictable list of values) that can be\nadded to a document via arguments, you can implement a slot for the `nodeEnumerated` signal to enumerate additional\nnodes with arguments.\n\n\u003e **Note:** Request arguments must be mapped to URIs via custom routes, since we do not support HTTP query parameters for rendered documents.\n\n#### Example\n\nAdd a slot for the `nodeEnumerated` signal via `Package.php`:\n\n```php\n\u003c?php\nclass Package extends BasePackage\n{\n    public function boot(Bootstrap $bootstrap)\n    {\n        $dispatcher = $bootstrap-\u003egetSignalSlotDispatcher();\n\n        $dispatcher-\u003econnect(NodeEnumerator::class, 'nodeEnumerated', MyNodeListsEnumerator::class, 'enumerateNodeLists');\n    }\n}\n```\n\nImplement the slot and enumerate additional nodes depending on the node type:\n\n```php\n\u003c?php\nclass NodeListsEnumerator\n{\n    public function enumerateNodeLists(EnumeratedNode $enumeratedNode, ContentReleaseIdentifier $releaseIdentifier, ContentReleaseLogger $logger)\n    {\n        $nodeTypeName = $enumeratedNode-\u003egetNodeTypeName();\n        $nodeType = $this-\u003enodeTypeManager-\u003egetNodeType($nodeTypeName);\n        if ($nodeType-\u003eisOfType('Vendor.Site:Document.Blog.Folder')) {\n            // Get the node and count the number of pages to render\n            // $pageCount = ...\n\n            $pageCount = ceil($postCount / (float)$this-\u003eperPage);\n            if ($pageCount \u003c= 1) {\n                return;\n            }\n\n            // Start after the first page, because the first page will be the document without arguments\n            for ($page = 2; $page \u003c= $pageCount; $page++) {\n                $enumeratedNodes[] = EnumeratedNode::fromNode($documentNode, ['page' =\u003e $page]);\n            }\n\n            $this-\u003eredisEnumerationRepository-\u003eaddDocumentNodesToEnumeration($releaseIdentifier, ...$enumeratedNodes);\n        }\n    }\n}\n```\n\nThe actual logic will depend on your use of the node. Having the actual filtering logic implemented in PHP is\nbeneficial, because it allows you to use it in the rendering process as well as in the additional enumeration.\n\n### Extending the backend module\n\n- You need a Views.yaml in your package, looking like this:\n```\n-\n  requestFilter: 'isPackage(\"Flowpack.DecoupledContentStore\")'\n  viewObjectName: 'Neos\\Fusion\\View\\FusionView'\n  options:\n    fusionPathPatterns:\n      - 'resource://Flowpack.DecoupledContentStore/Private/BackendFusion'\n      - 'resource://Vendor.Site/Private/DecoupledContentStoreFusion'\n```\n- Ensure that your package depends on `flowpack/decoupledcontentstore` in composer.json (so that your Views.yaml \"wins\" because the DecoupledContentStore-Package comes with its own Views.yaml)\n- Add a Root.fusion in `Vendor.Site/Resources/Private/DecoupledContentStoreFusion` which can contain your modifications\n- We currently support the following adjustments:\n  - Adding a button to the footer\n    ```\n    prototype(Flowpack.DecoupledContentStore:ListFooter) {\n        test = '\u003cspan class=\"align-middle inline-block text-sm pr-4 pl-16\"\u003eTEST\u003c/span\u003e'\n        test.@position = 'before reload'\n    }\n    ```\n  - Adding a flash message\n    ```\n    // ActionController\n    $this-\u003eaddFlashMessage('sth important you have to say');\n    ```\n  \n### Using different sets of config\n\nIn some cases it might be necessary to make fundamental adjustments to some configuration properties that would be\nreally hard to handle (safely, non-breaking) on the consuming site of the content store. Therefore we added the config\nproperty `configEpoch` that can contain a current and previous config version. The `current` value (that should be used\non the consuming site) gets published to the content store.\n\nWe decided to save the configEpoch on content store level instead of content release level for simplicity reasons on the\nconsuming site. If you need to switch back to an older release that was rendered with the previous config epoch version\nand would not match the currently published one, you may manually toggle between current and previous config epoch.\nThere is a button for this in the backend module for each target content store. Obviously this button should be used\nwith extra care as the config epoch needs to fit the current release at all times.\n\nExample:\n\n- We need to make a bigger change to the contentDimensions config, let's say we need to add uriPrefixes that weren't\n  there before. We adjust the config accordingly and in the same deployment we configure the config epoch as follows:\n\n    ```yml\n    Flowpack:\n      DecoupledContentStore:\n        configEpoch:\n          current: '2'\n          previous: '1'\n    ```\n\n- Now on the consuming site we can take action to handle both the old and new config and decide based on the value in\n  redis which case is executed.\n\n    ```php\n    $configEpoch = (int) $redisClient-\u003eget('contentStore:configEpoch');\n    $contentStoreUrl = 'https://www.vendor.de/' . ($configEpoch \u003e 1 ? 'de-de/' : '');\n    ```\n\n## Development\n\n- You need [pnpm](https://github.com/pnpm/pnpm) as package panager installed: `curl -f https://get.pnpm.io/v6.js | node - add --global pnpm`\n- Run `pnpm install` in this folder\n- Then run `pnpm watch` for development and `pnpm build` for prod build.\n\nWe use esbuild combined with tailwind.css for building.\n\n### Rendering Deep Dive\n\nTODO write\n\nCacheUrlMappingAspect - * NOTE: This aspect is NOT active during interactive page rendering; but only when a content release is built\n* through Batch Rendering (so when {@see DocumentRenderer} has invoked the rendering. This is to keep complexity lower\n* and code paths simpler: The system NEVER re-uses content cache entries created by editors while browsing the page; but\n* ONLY re-uses content cache entries created by previous Batch Renderings.\n\n\n### Debugging\n\nIf you need to debug single steps of the pipeline just run the corresponding commands from CLI, \ne.g. `./flow nodeEnumeration:enumerateAllNodes {{ .contentReleaseId }}`.\n\n### Testing the Rendering\n\nFor executing behavioral tests, install the `neos/behat` package and run `./flow behat:setup`. Then:\n\n```bash\ncd Packages/Application/Flowpack.DecoupledContentStore/Tests/Behavior\n../../../../bin/behat -c behat.yml.dist\n```\n\nBehat also supports running single tests or single files - they need to be specified after the config file, e.g.\n\n```bash\n\n# run all scenarios in a given folder\n../../../../bin/behat -c behat.yml.dist Features/ContentStore/\n\n# run all scenarios in the single feature file\n../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature\n\n# run the scenario starting at line 66\n../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature:66\n```\n\nIn case of exceptions, it might be helpful to run the tests with `--stop-on-failure`, which stops the test cases at the first\nerror. Then, you can inspect the testing database and manually reproduce the bug.\n\nAdditionally, `-vvv` is a helpful CLI flag (extra-verbose) - this displays the full exception stack trace in case of errors.\n\n## TODO\n\n- clean up of old content releases\n  - in Content Store / Redis\n- generate the old content format\n- (SK) error handling tests\n- force-switch possibility\n- (AM) UI\n- check for TODOs :)\n\n## Missing Features from old\n\ndata-url-next-page (or so) not supported\n\n## License\n\nGPL v3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflowpack%2Fflowpack.decoupledcontentstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflowpack%2Fflowpack.decoupledcontentstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflowpack%2Fflowpack.decoupledcontentstore/lists"}