ApostropheCMS is a full-featured, open-source CMS built with Node.js that seeks to empower organizations by combining in-context editing and headless architecture in a full-stack JS environment.

Overview

CircleCI Chat on Discord

Logo

ApostropheCMS

ApostropheCMS is a full-featured, open source CMS built with Node.js that seeks to empower organizations by combining in-context editing and headless architecture in a full-stack JS environment.
Documentation »

Demo · Roadmap · Report Bug

Table of Contents

  1. About ApostropheCMS
  2. Getting Started
  3. Extensions and Integrations
  4. Community
  5. Contributing
  6. License

About ApostropheCMS

ApostropheCMS is content software for everyone in an organization. It helps teams of all sizes create dynamic digital experiences with elegance and efficiency by blending powerful features, developer happiness, and a low learning curve for content creators. Apostrophe has powered websites and web apps for organizations large and small for over a decade.

Built With

Getting Started

To get started with ApostropheCMS, follow these steps to set up a local development environment. For more detail, refer to the Getting Started tutorial in the documentation.

Prerequisites

We recommend installing the following with Homebrew on macOS. If you're on Linux, you should use your package manager (apt or yum). If you're on Windows, we recommend the Windows Subsystem for Linux.

Software Minimum Version Notes
Node.js 8.x Or better
npm 6.x Or better
MongoDB 3.6 Or better
Imagemagick Any Faster image uploads, GIF support (optional)

Installation

Our recomended way to start a new project with ApostropheCMS is to use the Apostrophe CLI.

Install the Apostrophe CLI
 npm install -g apostrophe-cli
Create a project
apos create-project wonderful-project --setup

You'll be prompted to create a password for the admin user.

Run
cd wonderful-project && node app.js

Navigate to localhost:3000/login to login with the admin credentials you created.

Next Steps

Check out our Getting Started tutorial in the documentation to start adding content. Be sure to reference our glossary of terms to get acquainted with the reference materials.

Extensions and Integrations

You can find a list of ApostropheCMS extensions on our website, but here are a few highlights:

Extend ApostropheCMS

Editor Extensions

Community

Discord - Twitter - Discussions

Contributing

We eagerly welcome open source contributions. Before submitting a PR, please read through our Contribution Guide

License

ApostropheCMS is released under the MIT License.

Comments
  • widget won't even attempt to display on custom module

    widget won't even attempt to display on custom module

    I'm trying to use apostrophe-twitter as a basis for a module. I removed everything that looks twitter specific, and tried to make a little "Hello World" module.

    It registers, the widgets seems to be embeddable, the "Edit" button shows up, and even shows the template for the editor. However, the widget itself simply won't even call renderWidget.

    Here is my index.js:

    var extend = require('extend');
    var _ = require('lodash');
    var qs = require('qs');
    
    module.exports = function(options, callback) {
      return new Construct(options, callback);
    };
    
    module.exports.Construct = Construct;
    
    function Construct(options, callback) {
      var apos = options.apos;
      var app = options.app;
      var self = this;
      self._apos = apos;
      self._app = app;
      self._apos.mixinModuleAssets(self, 'comments', __dirname, options);
    
      // This widget should be part of the default set of widgets for areas
      // (this isn't mandatory)
      // apos.defaultControls.push('comments');
    
      // Include our editor template in the markup when aposTemplates is called
      self.pushAsset('template', 'commentsEditor', { when: 'user' });
    
      // Make sure that aposScripts and aposStylesheets summon our assets
      self.pushAsset('script', 'content', { when: 'always' });
      self.pushAsset('script', 'editor', { when: 'user' });
      self.pushAsset('stylesheet', 'content', { when: 'always' });
    
      var url;
    
      app.get('/apos-comments/:pageId', function(req, res) {
        var pageId = req.params.pageId;
    
        /* var reader = ;
    
        return reader.get(url, function(err, results) {
          if (err) {
            results = '[]';
          }
          results = JSON.parse(results);
          if (results.statuses) {
            results = results.statuses;
          }
          tweetCache[url] = { when: (new Date()).getTime(), results: results };
          return res.send(results);
        }); */
      });
    
      self.widget = true;
      self.label = 'Comments';
      self.css = 'comments';
      self.icon = 'icon-comments';
      self.sanitize = function(item) {
        var matches = item.account.match(/\w+/);
        item.account = matches[0];
      };
      self.renderWidget = function(data) {
        console.log('rendering widget');
        return self.render('comments', data);
      };
      self._apos.addWidgetType('comments', self);
    
      return setImmediate(function() { return callback(null); });
    }
    

    In app.js, I do: 'apostrophe-comments': {}

    In my comments.html I simply have: Hello World

    opened by israelidanny 49
  • As a developer, I can show and hide fields based on the values of other fields in an intuitive way

    As a developer, I can show and hide fields based on the values of other fields in an intuitive way

    Apostrophe 3.x doesn't have showFields support so far. This 2.x feature needs to be ported, or reconsidered.

    The backend logic for showFields, which ensures a required field is only really required if it is visible, is already present.

    We depend rather heavily on this feature across many projects, as do others. I don't think we can leave it out in 3.x, maybe not even for alpha.

    However, this is our last chance for 3.x to decide if we change from showFields to something like if (or ifFieldEquals), which some have proposed because it is more common in other systems. It also is a bit familiar to Vue developers because it works a little like v-if.

    The big difference is that if is set on the field that will be shown or hidden, and makes reference to another field's value, as opposed to the other way around.

    I have a bias against making any more changes in A3 because we're due for a release and there will be an A4 one day.

    However, if it turns out that if is less complicated to implement than showFields, it would be reasonable to do it. I think this is the case.

    if could look like this:

    ship: { 
      type: 'boolean'
    },
    shippingAddress: {
      type: 'string',
      if: {
        ship: true
      }
    }
    

    Like showFields, if would need to imply that a field is not required when it is not visible. Ditto for its subfields.

    There are situations where a field could become visible in two or more ways. showFields supports this, so if potentially should too. We might not have to have parity on that right away, but if we do need it, we could support a mongo-style $or query.

    As for implementation, it is tempting to restore the context prop to our input components, but that would give fields more information than they need to have and could have negative impacts on reactivity in Vue.

    And in fact the responsibility for showing or hiding a field is not on the field, it is on AposSchemas. I say that because it is the same logic for every field, so AposSchemas can implement it in one place without making our mixin more complicated. Also, AposSchemas is the right place to use v-if to control whether the component is present.

    Since the responsibility is on AposSchemas, which can see all the fields, we could implement either showFields or if. But, it seems to me that the implementation of if requires less code.

    So discussion is needed to settle on an approach here. And I think we can make that decision on the merits of showFields versus if from the website developer's standpoint, because both are reasonable to implement. Your input is welcome.

    enhancement v3 
    opened by boutell 39
  • Feature: Make it possible to configure if admin bar loads on page load

    Feature: Make it possible to configure if admin bar loads on page load

    This PR introduces two new configuration options in app.js:

    modules: {
      'apostrophe-admin-bar': {
        openOnLoad: false,          // default true
        openOnHomepageLoad: true,           // default true
        closeDelay: 5000           // default 3000 (ms)
      },
        ......
    };
    

    Features:

    • It's possible to set openOnLoad to false, making the admin bar stay closed on page loads. If not set defaults to true, opening the admin bar on every page load.
    • It's possible to configure the timeout until the admin bar closes after load if openOnLoad is true or undefined / not set. closeDelay configures the time in milliseconds before the admin bar closes. If not set or set to 0, it will default back to 3000.
    • Makes it possible to open on the homepage using openOnHomepage, but still keep it closed on subpages of the site.

    Thoughts:

    I thought about making it possible to set closeDelay to 0 and make it stay open infinite or until the user clicks the Apostrophe logo, but decided to not complicate the code further unless there's a want from core devs for this. In that case, let me know and I will update this PR.

    This needs a bit of documentation, but I am not sure how that works, I hope the above text helps adding it for whoever has access/knowledge.

    opened by houmark 39
  • module inheritance (moog) is difficult to master, developers don't know where to put their code in modules

    module inheritance (moog) is difficult to master, developers don't know where to put their code in modules

    Here is the planned replacement for the current index.js syntax for modules. This is not backwards compatible, it's the 3.0 format. However a conversion tool is under construction and has already been used to help convert apostrophe core itself.

    In general, we are deprecating the imperative, "build the module by calling stuff" style and replacing it with a declarative style, while avoiding technical terms and invented language.

    // lib/modules/shoes/index.js, a pieces subclass
    
    module.exports = {
    
      extends: 'apostrophe-pieces',
    
      // "improve" would also go at this level
    
      // set up schema fields. If there are subclasses with fields they will
      // merge sensibly with what is done here.
    
      // fields may just be an object if you don't have any conditional fields to add, or not add, based
      // on options
    
      fields(self, options) => ({
        add: {
          shoeSize: {
            type: 'integer',
            label: 'Shoe Size'
          },
          price: {
            type: 'float'
          },
          // ES2015 makes option-dependent fields easy
          ...(options.specialField ? {
            special: {
              type: 'whatever'
            }
          } : {})
        },
        remove: [ 'tags' ],
        groups: {
          shoes: {
            label: 'Shoes',
            fields: [ 'shoeSize' ]
          },
          // The "utility rail" at right which appears no matter what tab is active.
          // Overuse of this space is discouraged
          utility: {
            fields: [ 'title', 'slug' ]
          }
        }
      }),
    
      options: {
        // "Plain old options" now go in their own distinct section. They
        // override straightforwardly in subclasses
        searchable: false
        // "name" option for pieces is dead, defaults to module name
        // as it should have in the first place, sorry
      },
    
      async init(self, options) {
        // options, fields and methods are ready for use here
        await self.connectToShoestoreBackend();
      },
    
      async afterAllSections(self, options) {
        // You'll probably never write one of these! But if you were adding support for an
        // entirely new section of module config (like routes or methods, but something
        // else) you might need to run code at this point to attach the routes to express, etc.
        // "init" has already resolved at this point and all of the sections have been attached
        // to `self`.
      },
    
      methods: (self, options) => ({
        async fetchMonkeys(req, doc) {
          ... code for this method ...
          // having self in scope is key here and in pretty much all other sections
          // containing functions
          self.doSomething();
        },
        doThing() {
          ... code for this method ...
        },
        doOtherThing() {
          ... code for this method ...
        },
        // Middleware we'll add selectively to certain routes
        requireAdmin(req, res, next) {
          if (!self.apos.permissions.can('admin')) {
            return res.status(403).send('forbidden');
          }
        }
      }),
    
      extendMethods: (self, options) => ({
        async adjustHovercraft(_super, req, doc, options) {
          await _super(req, doc, options);
          doSomethingMore();
        }
      }),
    
      handlers: (self, options) => ({
        'apostrophe-pages:beforeSend': {
          // Named so they can be extended
          async addPopularProducts(req) { ... },
          async addBoringBooks(req) { ... }
        }
      }),
    
      extendHandlers: (self, options) => ({
        'apostrophe-pages:beforeSend': {
          // Inherited from base class, we can use _super to
          // invoke the original and do more work
          async addNavigation(_super, req) { ... }
        }
      }),
    
      helpers: (self, options) => ({
        includes(arr, item) {
          return arr.includes(item);
        }
      }),
    
      extendHelpers: (self, options) => ({
        // Extend a base class helper called colorCode with a new default
        colorCode(_super, item) {
          const color = _super(item);
          return color || 'red';
        }
      }),
    
      // middleware registered in the middleware block runs on ALL requests
      middleware(self, options) => ({
        ensureData(req, res, next) {
          req.data = req.data || {};
          return next();
        }),
        // This middleware needs to run before that provided by another module,
        // so we need to pass an option as well as a function
        reallyEarlyThing: {
          before: 'other-module-name',
          middleware: (req, res, next) => { }
        }
      }),
    
      // there is no extendMiddleware because the middleware pattern does not lend itself to the
      // "super" pattern
    
      apiRoutes: (self, options) => ({
        post: {
          // This route needs middleware so we pass an array ending with the route function
          upload: [
            // Pass a method as middleware
            self.requireAdmin,
            // Actual route function
            async (req) => { ... }
          ],
          async insert(req) {
            // No route-specific middleware, but global middleware always applies
          }
          // Leading / turns off automatic css-casing and automatic mapping to a module URL.
          // Presto, public API at a non-Apostrophe-standard URL
          '/track-hit': async (req) => {
            // route becomes /modules/modulename/track-hit, auto-hyphenation so we can
            // use nice camelcase method names to define routes
            return self.apos.docs.db.update({ _id: req.body._id },
              { $inc: { hits: 1 } }
            );
          }
        }
      }),
    
      extendApiRoutes: (self, options) => ({
        post: {
          // insert route is inherited from base class, let's
          // extend the functionality
          async insert(_super, req) {
            await _super(req);
            // Now do something more...
          }
        }
      }),
    
      // REST API URLs can be done with `apiRoutes` but they would
      // look a little weird with `apiRoutes` because you
      // would need to specify the empty string as the name of the "get everything"
      // GET route, for instance. So let's provide a separate section
      // to make it less confusing to set them up
    
      restApiRoutes: (self, options) => ({
        async getAll(req) { returns everything... },
        async getOne(req, id) { returns one thing... },
        async post(req) { inserts one thing via req.body... },
        async put(req, id) { updates one thing via req.body and id... },
        async delete(req, id) { deletes one thing via id... }
        async patch(req, id) { patches one thing via req.body and id... }
      }),
      
      // extendRestApiRoutes works like extendApiRoutes of course
    
      components: (self, options) => ({
        // In template: {% component 'shoes:brandBrowser' with { color: 'blue' } %}
        async brandBrowser(req, data) {
          // Renders the `brandBrowser.html` template of this module,
          // with `data.brands` available containing the results of this
          // third party API call
          return {
            // Pass on parameter we got from the component tag in the template
            brands: await rq('https://shoebrands.api', { color: data.color })
          };
        }
      }),
    
      extendComponents: (self, options) => ({
        // Extend a component's behavior, reusing the original to do most of the work 
        async brandBrowser(_super, req, data) {
          const result = await _super(req, data); 
          if (result.color === 'grey') {
            result.color = 'gray';
          }
          return result;
        }
      }),
    
      // Typically used to adjust `options` before the base class sees it, if you need
      // to do that programmatically in ways the `options` property doesn't allow for.
      // Pretty rare now that `fields` is available
    
      beforeSuperClass(self, options) {
        // ...
      },
    
      // Add features to database queries, i.e. the objects returned by self.find() in this module.
      // `self` is the module, `query` is the individual query object
      queries(self, query) {
        return {
          // Query builders. These are chainable methods; they get a chainable setter method for free,
          // you only need to specify what happens when the query is about to execute
          builders: {
            free: {
              finalize() {
                const free = query.get('free');
                const criteria = query.get('criteria');
                query.set('criteria', { $and: [ criteria, { price: free ? 0 : { $gte: 0 } } ] });
              }
            }
          },
          methods: {
            // Return a random object matching the query
            async toRandomObject() {
              const subquery = query.clone();
              const count = await subquery.toCount();
              query.skip(Math.floor(count * Math.random));
              query.limit(1);
              const results = await query.toArray();
              return results[0];
            }
          }
        };
      },
    };
    

    Why do we think this is better?

    • An explicit self, options function for each section that contains functions provides a scope with access to the module. Separating those functions by section is a little wordy, but attempting to merge them in a single function leads to significant confusion. For instance, you can't pass a method as the function for a route unless methods are initialized first in a separate call. And properties like extend must be sniffable before construction of the object begins, which means we can't just export one big function that returns one object, or else we'd have to invoke it twice; the first time it would be without a meaningful self or options, leading to obvious potential for bugs.
    • methods is a simple and descriptive name, familiar from Vue, which has been very successful in achieving developer acceptance, even though Vue also does not use ES6 classes for not entirely dissimilar reasons. In general, Vue components have been designed with simple and descriptive language wherever possible, and we can learn from that and avoid inside baseball jargon.
    • extendMethods is a similar, however here each method's first argument is _super, where _super is a reference to the method we're overriding from a parent class. We now have complete freedom to call _super first, last, or in the middle in our new function. It is much less verbose than our current super pattern. Organizing all of these extensions in extendMethods makes the intent very clear. Note that if you just want to "override" (replace) a method, you declare it in methods and that straight up crushes the inherited method. extendMethods is for scenarios where you need reuse of the original method as part of the new one. We use _super because super is a reserved word.
    • handlers and extendHandlers provide similar structure for promise event handlers. Again, these get grouped together, making them easier to find, just like Vue groups together computed, methods, etc. As always, handlers must be named. Handlers for the same event are grouped beneath it. This is loosely similar to Vue lifecycle hooks, but intentionally not identical because Apostrophe involves inheritance, and everything needs to be named uniquely so it can be overridden or extended easily.
    • helpers and extendHelpers: you get the idea. For nunjucks helpers.
    • apiRoutes and extendApiRoutes: you'd actually be able to add apiRoutes, htmlRoutes and plain old routes. see recent Apostrophe changelogs if this is unfamiliar. Note subproperties separating routes of the same name with different HTTP methods.
    • fields: just... just look at it. This clobbers addFields/removeFields with tons of beforeConstruct boilerplate.
    • middleware and extendMiddleware: replaces the current expressMiddleware property, which is a bit of a hack, with a clear way to define and activate middleware globally. Note the before option which covers some of the less common but most important uses of expressMiddleware in 2.x. As for middleware that is only used by certain routes, methods are a good way to deliver that, as shown here. Note that methods will completely initialize, from base class through to child class, before routes start to initialize, so they will see the final version of each method.
    • init is a common name in other frameworks for a function that runs as soon as the module is fully constructed and ready to support method calls, etc. This replaces the inside-baseball name afterConstruct.
    • beforeSuperClass is very explicit and hopefully, finally, clarifies when this function runs: before the base class is constructed. It is the only function that runs "bottom to top," i.e. the one in the subclass goes first. We used to call it beforeConstruct which says nothing about the timing relative to the base class. It is used to manipulate options before the parent class sees them, however most 2.x use cases have been eliminated by the introduction of the fields section.
    • queries replaces what people used to put in cursor.js files, specifically addFilters calls as well as custom methods. It obviates the need for a separate moog type for cursors, moog is now used only to instantiate modules and ceases to have its own "brand." queries only makes sense in a module that inherits from apostrophe-doc-type-manager.

    "What about breaking a module into multiple files?" Well that's a good question, we do this now and it's a good thing. But, nobody's stopping anyone from using require in here. It would work like it does today, you'd pass in self or self, options to a function in the required file.

    enhancement v3 
    opened by boutell 36
  • On-page, in-context editing of areas nested in array elements does not save

    On-page, in-context editing of areas nested in array elements does not save

    This turns out to be a longstanding issue:

    If I have an area inside a field of type array and I want to edit that area contextually on the page, I can't.

    I hoped to fix this simply, but there's a problem. The contextualConvert mechanism depends on scoping the search for relevant elements via a parent element, and array items don't necessarily have one on the front end of websites, or it's site-dependent.

    We could address it by saying something ilke this:

    "If you want contextual editing of the content of the array elements, you have to introduce a [data-apos-array-item="item._id"] attribute on a wrapper element. You don't have to do this, but you can't have contextual editing unless you do."

    This would require minimum change to the code.

    Or, we could address it by locating contextually editable fields by their full dot paths, which would mean it wouldn't matter if the intervening structures had a representation in the DOM or not. This would involve tricky fallbacks for bc and could complicate things if someone actually introduces array editing in context (adding/removing/etc).

    The first solution makes the most sense to me. We could provide a helper to make it less awkward. People are going to forget to do it and complain. Hmm.

    cc @stuartromanek

    bug 
    opened by boutell 36
  • 3.0: Intermittent error : [Vue warn]: Cannot find element: #apos-modals

    3.0: Intermittent error : [Vue warn]: Cannot find element: #apos-modals

    The error: [Vue warn]: Cannot find element: #apos-modals shown in the below snapshot appears intermittently between page refreshes.

    image

    I have cloned the apostrophe-3 boilerplate following the apostrophe3 docs, removed the default markup from the templates and added jquery to the page.

    Page source of the home url where the error occurs:

    
    <!DOCTYPE html>
    <html lang="en" >
      <head>
        
        
        
    
        <title>
      Home
      
    </title>
        
        
        <meta name="viewport" content="width=device-width, initial-scale=1">
        
        
    
        
      
    
    
    
    
      </head>
      
      <body class=" apos-theme--primary-purple " data-apos-level='0' data-apos='{"modules":{"@apostrophecms/admin-bar":{"items":[{"name":"@apostrophecms/user","action":"@apostrophecms/user:manager","label":"Users","permission":{"action":"edit","type":"@apostrophecms/user"},"options":{}},{"name":"@apostrophecms/image","action":"@apostrophecms/image:manager","label":"Images","permission":{"action":"edit","type":"@apostrophecms/image"},"options":{}},{"name":"@apostrophecms/image-tag","action":"@apostrophecms/image-tag:manager","label":"Image Tags","permission":{"action":"edit","type":"@apostrophecms/image-tag"},"options":{}},{"name":"@apostrophecms/file","action":"@apostrophecms/file:manager","label":"Files","permission":{"action":"edit","type":"@apostrophecms/file"},"options":{}},{"name":"@apostrophecms/file-tag","action":"@apostrophecms/file-tag:manager","label":"File Tags","permission":{"action":"edit","type":"@apostrophecms/file-tag"},"options":{}}],"components":{"the":"TheAposAdminBar"},"openOnLoad":true,"openOnHomepageLoad":true,"closeDelay":3000,"context":{"_id":"ckiohubqr0005bkpnyymf3bhv","title":"Home","type":"@apostrophecms/home-page","_url":"/","slug":"/"},"contextId":"ckiohubqr0005bkpnyymf3bhv","htmlPageId":"ckiq0i07x004ue8pn1hdl5ply","contextEditorName":"@apostrophecms/page:editor","alias":"adminBar"},"@apostrophecms/notification":{"action":"/api/v1/@apostrophecms/notification","alias":"notification"},"@apostrophecms/login":{"action":"/api/v1/@apostrophecms/login","user":{"_id":"ckiohue7i000abkpndz4gqjnv","firstName":"admin","title":"admin","username":"admin"},"alias":"login"},"@apostrophecms/schema":{"components":{"fields":{"area":"AposInputArea","string":"AposInputString","slug":"AposInputSlug","boolean":"AposInputBoolean","checkboxes":"AposInputCheckboxes","select":"AposInputSelect","integer":"AposInputString","float":"AposInputString","email":"AposInputString","color":"AposInputColor","range":"AposInputRange","url":"AposInputString","date":"AposInputString","time":"AposInputString","password":"AposInputPassword","group":"AposInputGroup","array":"AposInputArray","object":"AposInputObject","relationship":"AposInputRelationship","relationshipReverse":false,"attachment":"AposInputAttachment","oembed":"AposInputOembed"}},"alias":"schema"},"@apostrophecms/doc":{"action":"/api/v1/@apostrophecms/doc","alias":"doc"},"@apostrophecms/version":{"action":"/api/v1/@apostrophecms/version","alias":"version"},"@apostrophecms/modal":{"modals":[{"itemName":"@apostrophecms/global:manager","componentName":"AposPiecesManager","props":{"moduleName":"@apostrophecms/global"}},{"itemName":"@apostrophecms/global:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/global"}},{"itemName":"@apostrophecms/page:manager","componentName":"AposPagesManager","props":{"moduleName":"@apostrophecms/page"}},{"itemName":"@apostrophecms/page:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/page"}},{"itemName":"@apostrophecms/user:manager","componentName":"AposPiecesManager","props":{"moduleName":"@apostrophecms/user"}},{"itemName":"@apostrophecms/user:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/user"}},{"itemName":"@apostrophecms/image:manager","componentName":"AposMediaManager","props":{"moduleName":"@apostrophecms/image"}},{"itemName":"@apostrophecms/image:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/image"}},{"itemName":"@apostrophecms/image-tag:manager","componentName":"AposPiecesManager","props":{"moduleName":"@apostrophecms/image-tag"}},{"itemName":"@apostrophecms/image-tag:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/image-tag"}},{"itemName":"@apostrophecms/file:manager","componentName":"AposPiecesManager","props":{"moduleName":"@apostrophecms/file"}},{"itemName":"@apostrophecms/file:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/file"}},{"itemName":"@apostrophecms/file-tag:manager","componentName":"AposPiecesManager","props":{"moduleName":"@apostrophecms/file-tag"}},{"itemName":"@apostrophecms/file-tag:editor","componentName":"AposDocEditor","props":{"moduleName":"@apostrophecms/file-tag"}}],"components":{"the":"TheAposModals","confirm":"AposModalConfirm"},"alias":"modal"},"@apostrophecms/attachment":{"action":"/api/v1/@apostrophecms/attachment","fileGroups":[{"name":"images","label":"Images","extensions":["gif","jpg","png"],"extensionMaps":{"jpeg":"jpg"},"image":true},{"name":"office","label":"Office","extensions":["txt","rtf","pdf","xls","ppt","doc","pptx","sldx","ppsx","potx","xlsx","xltx","csv","docx","dotx"],"extensionMaps":{},"image":false}],"name":"attachment","uploadsUrl":"/uploads","croppable":{"gif":true,"jpg":true,"png":true},"sized":{"gif":true,"jpg":true,"png":true},"alias":"attachment"},"@apostrophecms/oembed":{"action":"/api/v1/@apostrophecms/oembed","alias":"oembed"},"@apostrophecms/any-doc-type":{"name":"@apostrophecms/any-doc-type","action":"/api/v1/@apostrophecms/any-doc-type","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","following":"title","required":true,"group":{"name":"utility"},"_id":"29cc3aee08a22c7b069370939c93c4eb"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]},"@apostrophecms/global":{"name":"@apostrophecms/global","label":"Global","pluralLabel":"Global","action":"/api/v1/@apostrophecms/global","schema":[],"filters":[{"name":"visibility","label":"Visibility","inputType":"radio","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"},{"value":null,"label":"Any"}],"allowedInChooser":false,"def":true},{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"},{"name":"visibility","label":"Visibility"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}}],"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposPiecesManager"},"_id":"ckiohuboi0002bkpnimfq5eb7","alias":"global"},"@apostrophecms/polymorphic-type":{"name":"@apostrophecms/polymorphic","action":"/api/v1/@apostrophecms/polymorphic-type","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","following":"title","required":true,"group":{"name":"utility"},"_id":"29cc3aee08a22c7b069370939c93c4eb"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]},"@apostrophecms/page":{"action":"/api/v1/@apostrophecms/page","label":"Page","pluralLabel":"Pages","components":{"insertModal":"AposDocEditor","managerModal":"AposPagesManager"},"page":{"title":"Home","slug":"/","_id":"ckiohubqr0005bkpnyymf3bhv","type":"@apostrophecms/home-page","_url":"/","ancestors":[]},"name":"@apostrophecms/page","quickCreate":true,"alias":"page"},"@apostrophecms/home-page":{"name":"@apostrophecms/home-page","label":"Home Page","action":"/api/v1/@apostrophecms/home-page","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"main","type":"area","options":{"widgets":{"@apostrophecms/rich-text":{"toolbar":["styles","|","bold","italic","strike","link","|","bullet_list","ordered_list"],"styles":[{"tag":"p","label":"Paragraph (P)"},{"tag":"h3","label":"Heading 3 (H3)"},{"tag":"h4","label":"Heading 4 (H4)"}]},"@apostrophecms/image":{},"@apostrophecms/video":{}}},"group":{"name":"basics","label":"Basics"},"label":"Main","_id":"48bd41d0120a143082e962db5bd9ac25"},{"name":"slug","type":"slug","label":"Slug","required":true,"page":true,"following":"title","group":{"name":"utility"},"_id":"59ffb66aa916fb4541003ad46a49ab52"},{"name":"type","type":"select","label":"Type","required":true,"choices":[{"value":"default-page","label":"Default"},{"value":"@apostrophecms/home-page","label":"Home"}],"group":{"name":"utility"},"_id":"f30a526b60680a9fd84d2251f87a1ef6"},{"name":"orphan","type":"boolean","label":"Hide in Navigation","def":false,"group":{"name":"utility"},"_id":"88ef6252cb330cc8aa30c1dd6c9bd726"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]},"@apostrophecms/trash":{"name":"@apostrophecms/trash","action":"/api/v1/@apostrophecms/trash","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","required":true,"page":true,"following":"title","group":{"name":"utility"},"_id":"59ffb66aa916fb4541003ad46a49ab52"},{"name":"type","type":"select","label":"Type","required":true,"choices":[{"value":"default-page","label":"Default"},{"value":"@apostrophecms/home-page","label":"Home"}],"group":{"name":"utility"},"_id":"f30a526b60680a9fd84d2251f87a1ef6"},{"name":"orphan","type":"boolean","label":"Hide in Navigation","def":false,"group":{"name":"utility"},"_id":"88ef6252cb330cc8aa30c1dd6c9bd726"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]},"@apostrophecms/search":{"name":"@apostrophecms/search","action":"/api/v1/@apostrophecms/search","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","required":true,"page":true,"following":"title","group":{"name":"utility"},"_id":"59ffb66aa916fb4541003ad46a49ab52"},{"name":"type","type":"select","label":"Type","required":true,"choices":[{"value":"default-page","label":"Default"},{"value":"@apostrophecms/home-page","label":"Home"}],"group":{"name":"utility"},"_id":"f30a526b60680a9fd84d2251f87a1ef6"},{"name":"orphan","type":"boolean","label":"Hide in Navigation","def":false,"group":{"name":"utility"},"_id":"88ef6252cb330cc8aa30c1dd6c9bd726"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}],"alias":"search"},"@apostrophecms/any-page-type":{"name":"@apostrophecms/page","pluralLabel":"Pages","action":"/api/v1/@apostrophecms/any-page-type","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","following":"title","required":true,"group":{"name":"utility"},"_id":"29cc3aee08a22c7b069370939c93c4eb"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]},"@apostrophecms/area":{"components":{"editor":"AposAreaEditor","widgets":{"@apostrophecms/rich-text":"AposRichTextWidget","@apostrophecms/html":"AposWidget","@apostrophecms/image":"AposWidget","@apostrophecms/video":"AposWidget"},"widgetEditors":{"@apostrophecms/rich-text":"AposRichTextWidgetEditor","@apostrophecms/html":"AposWidgetEditor","@apostrophecms/image":"AposWidgetEditor","@apostrophecms/video":"AposWidgetEditor"}},"widgetIsContextual":{"@apostrophecms/rich-text":true},"contextualWidgetDefaultData":{"@apostrophecms/rich-text":{"content":""}},"widgetManagers":{"@apostrophecms/rich-text":"@apostrophecms/rich-text-widget","@apostrophecms/html":"@apostrophecms/html-widget","@apostrophecms/image":"@apostrophecms/image-widget","@apostrophecms/video":"@apostrophecms/video-widget"},"action":"/api/v1/@apostrophecms/area","alias":"area"},"@apostrophecms/rich-text-widget":{"components":{"widgetEditor":"AposRichTextWidgetEditor","widget":"AposRichTextWidget"},"tools":{"styles":{"component":"AposTiptapStyles","label":"Styles"},"|":{"component":"AposTiptapDivider"},"bold":{"component":"AposTiptapButton","label":"Bold","icon":"format-bold-icon"},"italic":{"component":"AposTiptapButton","label":"Italic","icon":"format-italic-icon"},"horizontal_rule":{"component":"AposTiptapButton","label":"Horizontal Rule","icon":"minus-icon"},"link":{"component":"AposTiptapLink","label":"Link","icon":"link-icon"},"bullet_list":{"component":"AposTiptapButton","label":"Bulleted List","icon":"format-list-bulleted-icon"},"ordered_list":{"component":"AposTiptapButton","label":"Ordered List","icon":"format-list-numbered-icon"},"strike":{"component":"AposTiptapButton","label":"Strike","icon":"format-strikethrough-variant-icon"},"blockquote":{"component":"AposTiptapButton","label":"Blockquote","icon":"format-quote-close-icon"},"code_block":{"component":"AposTiptapButton","label":"Code Block","icon":"code-tags-icon"},"undo":{"component":"AposTiptapButton","label":"Undo","icon":"undo-icon"},"redo":{"component":"AposTiptapButton","label":"Redo","icon":"redo-icon"}},"name":"@apostrophecms/rich-text","label":"Rich Text","action":"/api/v1/@apostrophecms/rich-text-widget","schema":[],"contextual":true,"className":"bp-rich-text"},"@apostrophecms/html-widget":{"name":"@apostrophecms/html","label":"Raw HTML","action":"/api/v1/@apostrophecms/html-widget","schema":[{"name":"code","type":"string","label":"Raw HTML (Code)","textarea":true,"help":"Be careful when embedding third-party code, as it can break the website editing functionality. If a page becomes unusable, add \"?safe_mode=1\" to the URL to make it work temporarily without the problem code being rendered.","group":{"name":"ungrouped","label":"Ungrouped"},"_id":"6d14ba74dd851d3eead317311c6adf01"}],"className":false},"@apostrophecms/image-widget":{"name":"@apostrophecms/image","label":"Image","action":"/api/v1/@apostrophecms/image-widget","schema":[{"name":"_image","type":"relationship","label":"Image","max":1,"required":true,"withType":"@apostrophecms/image","group":{"name":"ungrouped","label":"Ungrouped"},"idsStorage":"imageIds","_id":"7c3b499b648493f5f16762b7539acf53"}],"className":"bp-image-widget"},"@apostrophecms/oembed-field":{"name":"oembed","action":"/api/v1/@apostrophecms/oembed-field","alias":"oembedFields"},"@apostrophecms/video-widget":{"name":"@apostrophecms/video","label":"Video","action":"/api/v1/@apostrophecms/video-widget","schema":[{"name":"video","type":"oembed","oembedType":"video","label":"Video URL","help":"Enter the URL for a media source you wish to embed (e.g., YouTube, Vimeo, or other hosted video URL).","required":true,"group":{"name":"ungrouped","label":"Ungrouped"},"_id":"d6e37049bac95fa59524d50042d063e4"}],"className":"bp-video-widget"},"@apostrophecms/user":{"name":"@apostrophecms/user","label":"User","pluralLabel":"Users","action":"/api/v1/@apostrophecms/user","schema":[{"name":"firstName","type":"string","label":"First Name","group":{"name":"basics","label":"Basics"},"_id":"ef72b6324da86db687e80065a04d7397"},{"name":"lastName","type":"string","label":"Last Name","group":{"name":"basics","label":"Basics"},"_id":"608d83e210ce180ed6afb370ea577d29"},{"name":"title","type":"string","label":"Full Name","following":["firstName","lastName"],"required":true,"group":{"name":"basics","label":"Basics"},"_id":"f293f5e525c4a7eec5dc3786f25e17c7"},{"name":"slug","type":"slug","label":"Slug","following":"title","required":true,"group":{"name":"basics","label":"Basics"},"prefix":"user-","_id":"0fb598ca2a1b3d284c3fbc9f385ce1a9"},{"name":"username","type":"string","label":"Username","required":true,"group":{"name":"utility"},"_id":"c80424197b3459aca45dc2672f6f1222"},{"name":"email","type":"string","label":"Email","group":{"name":"utility"},"_id":"2ad069d900d46059f67b410d0eaad873"},{"name":"password","type":"password","label":"Password","group":{"name":"utility"},"_id":"afb74060766fe3ae7a374e70ad10c321"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"},{"name":"disabled","type":"boolean","label":"Login Disabled","def":false,"group":{"name":"permissions","label":"Permissions"},"_id":"e66f85dfa3fe3212f3ae97033820520f"}],"filters":[{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}}],"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposPiecesManager"},"alias":"user"},"@apostrophecms/image":{"name":"@apostrophecms/image","label":"Image","pluralLabel":"Images","action":"/api/v1/@apostrophecms/image","schema":[{"name":"attachment","type":"attachment","label":"Image File","fileGroup":"images","required":true,"group":{"name":"basics","label":"Basics"},"_id":"9878a650abacd5ea90bf988976913b7a","accept":".gif,.jpg,.png,.jpeg"},{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"alt","type":"string","label":"Alt Text","help":"Image description used for accessibility","group":{"name":"basics","label":"Basics"},"_id":"9cde7b59b3f53b5a0f22a12caa4b05ba"},{"name":"_tags","type":"relationship","label":"Tags","withType":"@apostrophecms/image-tag","group":{"name":"basics","label":"Basics"},"idsStorage":"tagsIds","_id":"43999955ff45523140816aa43c468480"},{"name":"credit","type":"string","label":"Credit","group":{"name":"basics","label":"Basics"},"_id":"26bf531fd175dcb22e9a31d9f99ac7f6"},{"name":"creditUrl","type":"url","label":"Credit URL","group":{"name":"basics","label":"Basics"},"_id":"e1393e987696a8f263ea607b3a60c708"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"basics","label":"Basics"},"_id":"97b78fe94e3742640944886e9a67f171"},{"name":"slug","type":"slug","label":"Slug","prefix":"image-","required":true,"following":"title","group":{"name":"basics","label":"Basics"},"_id":"d23b2789bad2cc06effedc67ea905e36"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"basics","label":"Basics"},"_id":"dc499a0fefc4f1a4cc81587e35734484"}],"filters":[{"name":"visibility","label":"Visibility","inputType":"radio","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"},{"value":null,"label":"Any"}],"allowedInChooser":false,"def":true},{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true},{"name":"_tags","label":"Tags","inputType":"select","nullLabel":"Choose One"}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"},{"name":"visibility","label":"Visibility"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}},{"name":"visibility","label":"Visibility","requiredField":"visibility","fields":{"add":{"visibility":{"type":"select","label":"Who can view this?","def":"public","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}]}}}}],"insertViaUpload":true,"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposMediaManager"},"alias":"image"},"@apostrophecms/image-tag":{"name":"@apostrophecms/image-tag","label":"Image Tag","pluralLabel":"Image Tags","action":"/api/v1/@apostrophecms/image-tag","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","required":true,"following":"title","group":{"name":"utility"},"_id":"d73f1233ca14ae04eb01f6661de27eea"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"}],"filters":[{"name":"visibility","label":"Visibility","inputType":"radio","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"},{"value":null,"label":"Any"}],"allowedInChooser":false,"def":true},{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"},{"name":"visibility","label":"Visibility"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}}],"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposPiecesManager"}},"@apostrophecms/file":{"name":"@apostrophecms/file","label":"File","pluralLabel":"Files","action":"/api/v1/@apostrophecms/file","schema":[{"name":"attachment","type":"attachment","label":"File","required":true,"group":{"name":"basics","label":"Basics"},"_id":"9155b49041e8424f9368197709020e06","accept":".gif,.jpg,.png,.txt,.rtf,.pdf,.xls,.ppt,.doc,.pptx,.sldx,.ppsx,.potx,.xlsx,.xltx,.csv,.docx,.dotx,.jpeg"},{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","prefix":"file-","required":true,"following":"title","group":{"name":"basics","label":"Basics"},"_id":"a8e2937805da2eab992448b7dc494a62"},{"name":"_tags","type":"relationship","label":"Tags","withType":"@apostrophecms/file-tag","group":{"name":"basics","label":"Basics"},"idsStorage":"tagsIds","_id":"e3b69eb3bc337c38a951caf5bf306754"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"},{"name":"description","type":"string","label":"Description","textarea":true,"group":{"name":"details","label":"Details"},"_id":"4661f859a1d38baa8f1839c580085614"},{"name":"credit","type":"string","label":"Credit","group":{"name":"details","label":"Details"},"_id":"26bf531fd175dcb22e9a31d9f99ac7f6"},{"name":"creditUrl","type":"url","label":"Credit URL","group":{"name":"details","label":"Details"},"_id":"e1393e987696a8f263ea607b3a60c708"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}],"filters":[{"name":"visibility","label":"Visibility","inputType":"radio","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"},{"value":null,"label":"Any"}],"allowedInChooser":false,"def":true},{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"},{"name":"visibility","label":"Visibility"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}},{"name":"visibility","label":"Visibility","requiredField":"visibility","fields":{"add":{"visibility":{"type":"select","label":"Who can view this?","def":"public","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}]}}}}],"insertViaUpload":true,"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposPiecesManager"},"alias":"file"},"@apostrophecms/file-tag":{"name":"@apostrophecms/file-tag","label":"File Tag","pluralLabel":"File Tags","action":"/api/v1/@apostrophecms/file-tag","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"slug","type":"slug","label":"Slug","required":true,"following":"title","group":{"name":"utility"},"_id":"d73f1233ca14ae04eb01f6661de27eea"},{"name":"trash","type":"boolean","label":"Trash","contextual":true,"def":false,"group":{"name":"utility"},"_id":"dc499a0fefc4f1a4cc81587e35734484"}],"filters":[{"name":"visibility","label":"Visibility","inputType":"radio","choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"},{"value":null,"label":"Any"}],"allowedInChooser":false,"def":true},{"name":"trash","label":"Trash","inputType":"radio","choices":[{"value":false,"label":"Live"},{"value":true,"label":"Trash"}],"allowedInChooser":false,"def":false,"required":true}],"columns":[{"name":"title","label":"Title"},{"name":"updatedAt","label":"Edited on"},{"name":"visibility","label":"Visibility"}],"batchOperations":[{"name":"trash","label":"Trash","inputType":"radio","unlessFilter":{"trash":true}},{"name":"rescue","label":"Rescue","unlessFilter":{"trash":false}}],"quickCreate":false,"components":{"insertModal":"AposDocEditor","managerModal":"AposPiecesManager"}},"@apostrophecms/busy":{"busy":false,"components":{"the":"TheAposBusy"},"alias":"busy"},"default-page":{"name":"default-page","label":"Default Page","action":"/api/v1/default-page","schema":[{"name":"title","type":"string","label":"Title","required":true,"sortify":true,"group":{"name":"basics","label":"Basics"},"_id":"f9deb4307b14ec0199642c67bc7849f2"},{"name":"main","type":"area","options":{"widgets":{"@apostrophecms/rich-text":{"toolbar":["styles","|","bold","italic","strike","link","|","bullet_list","ordered_list"],"styles":[{"tag":"p","label":"Paragraph (P)"},{"tag":"h3","label":"Heading 3 (H3)"},{"tag":"h4","label":"Heading 4 (H4)"}]},"@apostrophecms/image":{},"@apostrophecms/video":{}}},"group":{"name":"basics","label":"Basics"},"label":"Main","_id":"48bd41d0120a143082e962db5bd9ac25"},{"name":"slug","type":"slug","label":"Slug","required":true,"page":true,"following":"title","group":{"name":"utility"},"_id":"59ffb66aa916fb4541003ad46a49ab52"},{"name":"type","type":"select","label":"Type","required":true,"choices":[{"value":"default-page","label":"Default"},{"value":"@apostrophecms/home-page","label":"Home"}],"group":{"name":"utility"},"_id":"f30a526b60680a9fd84d2251f87a1ef6"},{"name":"orphan","type":"boolean","label":"Hide in Navigation","def":false,"group":{"name":"utility"},"_id":"88ef6252cb330cc8aa30c1dd6c9bd726"},{"name":"visibility","type":"select","label":"Who can view this?","def":"public","required":true,"choices":[{"value":"public","label":"Public"},{"value":"loginRequired","label":"Login Required"}],"group":{"name":"permissions","label":"Permissions","last":true},"_id":"97b78fe94e3742640944886e9a67f171"}]}},"prefix":"","csrfCookieName":"bestdrillmachine.csrf","htmlPageId":"ckiq0i07x004te8pnym3cpsjz","scene":"apos","user":{"title":"admin","_id":"ckiohue7i000abkpndz4gqjnv","username":"admin"}}'>
        
    
        
          
            <div id="apos-busy"></div>
            <div id="apos-admin-bar"></div>
          
        
        
          
            <div id="apos-context-menu"></div>
            <div id="apos-notification"></div>
          
        
        <div class="apos-refreshable" data-apos-refreshable>
          
          <a name="main"></a>
          
    
          
      
    
          
    
          
    
      <!-- javascript ================================================== -->
      <!-- Placed at the end of the document so the pages load faster -->
    
      <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    
    
    
        </div>
        
    <script src="/apos-frontend/apos-bundle.js"></script>
    
        
        <script data-apos-refresh-on-restart="/api/v1/@apostrophecms/asset/restart-id">
    (function() {
      // Note: this script will not appear in the page in production.
      //
      // eslint-disable-next-line no-var
      var reloadId;
      // eslint-disable-next-line no-var
      var fast = '1';
      check();
      function check() {
        apos.http.get(document.querySelector('[data-apos-refresh-on-restart]').getAttribute('data-apos-refresh-on-restart'), {
          qs: {
            fast: fast
          }
        }, function(err, result) {
          if (err) {
            fast = '1';
            setTimeout(check, 1000);
            return;
          }
          fast = '';
          if (!reloadId) {
            reloadId = result;
          } else if (result !== reloadId) {
            console.log('Apostrophe restarted, refreshing the page');
            window.location.reload();
            return;
          }
          setTimeout(check, 1000);
        });
      }
    })();
    </script>
        
    
        
        
          <div id="apos-modals"></div>
        
      </body>
    </html>
    
    
    bug v3 
    opened by hussainb 34
  • 3.0 proposal: remove the need for developers to think about idsField and relationshipsField when working with joins

    3.0 proposal: remove the need for developers to think about idsField and relationshipsField when working with joins

    I'm working on the technical design for editing relationships in Apostrophe 3.x.

    The hardest part of this is the way joins are represented in the database, and how we make developers think about it.

    idsField: 'productsIds' , relationshipsField: 'productsRelationships' and all that stuff. These things have magic defaults but under the hood it’s complicated.

    All other schema field types in apostrophe stay in their lane. The data is stored in the property of the same name in the database. But joins scatter their ids to one field, their relationships to another, and the actual join gets loaded dynamically under the field’s name.

    This is pretty good if you’re not working on the guts, because you only care about the actual join anyway. But if you’re working on the guts it’s pretty crazymaking.

    To make matters worse, our REST interface doesn’t really understand joins when writing, so you have to patch the idsField and relationshipsField directly.

    I think we should definitely fix our REST interface so that if you PUT, POST or PATCH a join field by its name, like _products , and pass in an actual array of existing product pieces, it’s smart enough to say “oh they are updating the join, let’s just grab the _id and any _relationship property if present from each one. Cool.” It's inefficient if they send the entire array fully populated, but it should work.

    But beyond that, there’s a tougher call to make. Do we want to make the data model easy to understand internally, and make it easy to implement the backend UI? Or do we want to continue to make sacrifices there for slightly less frontend code?

    I see two ways forward:

    1. We continue to sweat idsField and relationshipsField, but we do all of that in the AposSchema Vue component, where it is a special case in order to allow the interface to the individual field components, including AposInputJoin, to stay simple and v-model based.

    2. We reboot the whole approach to store joins under their own name. Want to join your salesperson with some products? Write this in your schema:

    // Note no _ anymore
    products: {
      type: 'join',
      withType: 'product,
      // use the optional relationship feature. Not always needed
      relationship: {
        // units of this product sold by this particular salesperson
        sold: {
          type: 'integer'
        }
      }
    }
    

    Apostrophe stores this in your salesperson doc:

    {
      title: 'Willy Loman',
      type: 'salesperson',
      products: {
        ids: [ 1, 2, 7 ],
        relationships: {
          '2': {
            sold: 5
          }
        }
      }
    }
    

    And when you find salespeople, or use our GET REST API, you get back this:

    {
      title: 'Willy Loman',
      type: 'salesperson',
      products: {
        // _docs subproperty is populated on the fly at load time
        _docs: [
          {
            title: 'Hair Tonic',
            type: 'product',
            _id: 2,
            _relationship: {
              sold: 5
            }
          },
          ...
        ],
        // the data that powers the join is here too
        ids: [ 1, 2, 7 ],
        relationships: {
          '2': {
            sold: 5
          }
        }
      }
    }
    

    What this does is give us a super clear model for how joins are stored. They are concretely stored under a property of the same name, just like other fields.

    However, it makes frontend developers do something new. In a template, you would now write:

    <h3>{{ data.salesperson.title }}: Assigned Products</h3>
    {% for product of data.salesperson.products._docs %}
      <h4><a href="{{ product._url }}">{{ product.title }} (sold: {{ product._relationship.sold }})</a></h4>
    {% endfor %}
    

    You see what’s different here: we have to go the extra step to ._docs when we work with these joins in our frontend code.

    This is this price we would pay for everything being stored in a sensible and intuitive way.

    But, it’s a price. And, it’s a change for people to learn about.

    What do you think? Input welcome!

    enhancement v3 
    opened by boutell 28
  • Add apos.images.srcset method

    Add apos.images.srcset method

    Here's the apos.attachments.srcset method (with an accompanying test) discussed in #803. The srcset attribute is fairly simple, so there's not much to it. I changed my mind on the naming of the sizes attribute passed to apostrophe-images-widgets, though, so it's now sizesAttr. It seemed like this would cause less confusion with the existing size option.

    One thing that I think warrants some discussion is the possible inclusion of the picturefill.js polyfill. The only browser that doesn't support srcset is IE, but picturefill.js really just works and weighs in at a pretty small 11.5 kB when minified. Maybe this is the kind of thing better left to individual developers (perhaps the possibility of including it could be mentioned in the docs), but I wanted to throw it out there.

    I'll go ahead and submit a PR to the docs for this as well.

    Btw, it looks like some trailing whitespace was removed automatically by my editor, but I didn't think that caused any harm.

    opened by fredrikekelund 28
  • Can't edit an area from an array field in the schema contextually on the page

    Can't edit an area from an array field in the schema contextually on the page

    Hey, for some reason I cant' edit/save content from the show.html but it does work in the control panel of the snippet. I do see the content I've added from the control, but when I edit the content in the show.html (in the frontend editor) it dosent get saved.

    thanks.

    opened by dannyblv 28
  • CSRF cookies don't set SameSite Attribute. Will soon be rejected by browsers.

    CSRF cookies don't set SameSite Attribute. Will soon be rejected by browsers.

    Install from Git, last week.

    Developer tools console shows:

    Cookie “multisite-ckkclehap000k3i4sxv7zp87b.csrf” will be soon rejected because it has the “SameSite” attribute set to “None” or an invalid value, without the “secure” attribute. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite

    To Reproduce

    Step by step instructions to reproduce the behavior:

    1. Turn on Developer Console in FF or Chrome, etc.
    2. Observe warnings.

    Expected behavior

    SameSite attribute set?

    Describe the bug

    The SameSite Attribute is missing.

    Details

    Version of Node.js:

    12.20.1

    Server Operating System:

    Debian Buster

    bug 
    opened by poetaster 26
  • Alt Text field for apostrophe-images

    Alt Text field for apostrophe-images

    Create a new dedicated field for alt text for apostrophe-images. Currently the title or description field is being used for alt text, but improving the the schema to include a dedicated field for alt text would be good, as well as adding explicit help text to each field. To avoid any sort of BC break and confusion on previous implementations of these fields, we can create a feature flag (an option passed to the apostrophe-images module) that would activate these new fields.

    Alt text would be made available the same way other image fields are here:

    https://github.com/apostrophecms/apostrophe/blob/81cdcc51dd0ec925754ead08bf8b97f34f826a65/lib/modules/apostrophe-attachments/lib/api.js#L449-L460

    screen shot 2018-10-04 at 3 31 05 pm enhancement v3 
    opened by plantainrain 25
  • Pro 3247 keyboard shortcuts conflicts

    Pro 3247 keyboard shortcuts conflicts

    Summary

    If multiple keyboard shortcuts using the same combination of keys are registered, the system must display a warning inside the keyboard shortcut list modal about the conflict. This should not prevents other shortcuts from working, nor triggers multiples action using a single key combination.

    Only the last identical keyboard shortcut registered must be available.

    This matters only if they are available in the same context.

    What are the specific steps to test this change?

    1. npm link apostrophe on testbed
    2. Conflicts will be notified once logged in
    3. Run unit tests

    What kind of change does this PR introduce?

    (Check at least one)

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactor
    • [ ] Documentation
    • [ ] Build-related changes
    • [ ] Other

    Make sure the PR fulfills these requirements:

    • [x] It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
    • [x] The changelog is updated
    • [ ] Related documentation has been updated
    • [x] Related tests have been updated
    opened by falkodev 1
  • Add bare minimum requirements to paste table into rich text editor

    Add bare minimum requirements to paste table into rich text editor

    Summary

    Per conversations in Discord, I am submitting a PR with only the bare minimum components to use TipTap tables. More conversations are needed for a good UI/UX solution for adding a new table.

    What are the specific steps to test this change?

    1. Run the website and log in as an admin
    2. Open a rich text editor
    3. Paste in a table and Update

    What kind of change does this PR introduce?

    (Check at least one)

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactor
    • [ ] Documentation
    • [ ] Build-related changes
    • [ ] Other

    Make sure the PR fulfills these requirements:

    • [ ] It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
    • [ ] The changelog is updated
    • [ ] Related documentation has been updated
    • [ ] Related tests have been updated

    If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

    Other information:

    opened by jmiller-rise8 1
  • Tailwind plugins not working?

    Tailwind plugins not working?

    To Reproduce

    Step by step instructions to reproduce the behavior:

    1. Install Tailwind the usual way (don't forget postcss.config.js and include tailwindcss there) and init to have tailwind.config.js 2.1 npm i -D tailwindcss && npx tailwindcss init --postcss
    2. Go into the tailwind.config.js
    3. add a plugin 4.1 via npm
      const plugin = require("tailwindcss/plugin");
      
      module.exports = {
        content: ["./views/**/*.{html,js}", "./modules/**/*.{html,js}"],
        presets: [],
        plugins: [
          require('@tailwindcss/typography'),
        ]
        ...
      };
    

    4.2 or manually

      const plugin = require("tailwindcss/plugin");
      
      module.exports = {
        content: ["./views/**/*.{html,js}", "./modules/**/*.{html,js}"],
        presets: [],
        plugins: [
          plugin(function ({ addComponents }) {
            addComponents({ ".i-am-a-class": { color: "black" } });
          }),
        ],
        ...
      };  
    
    1. To be safe, add class(es) to safelist via
      const plugin = require("tailwindcss/plugin");
      
      module.exports = {
        content: ["./views/**/*.{html,js}", "./modules/**/*.{html,js}"],
        presets: [],
        safelist: [
          'i-am-a-class'
        ],
        plugins: [
          plugin(function ({ addComponents }) {
            addComponents({ ".i-am-a-class": { color: "black" } });
          }),
        ],
        ...
      };
    
    1. Run APOS_DEV=1 npm run dev
    2. Open http://localhost:3000/apos-frontend/default/apos-bundle.css
    3. Search for a class, for example i-am-a-class

    Expected behavior

    The class i-am-a-class to be present in the bundled CSS file.

    Describe the bug

    The class i-am-a-class is missing from the bundled CSS file.

    Details

    Version of Node.js: 14.18.0

    Server Operating System: I am running this on my dev laptop with MacOS Monitery 12.6.

    bug 
    opened by waldemar-p 5
  • add default widgets to an area

    add default widgets to an area

    Summary

    Summarize the changes briefly, including which issue/ticket this resolves. If it closes an existing Github issue, include "Closes #[issue number]"

    What are the specific steps to test this change?

    For example:

    1. Run the website and log in as an admin
    2. Open a piece manager modal and select several pieces
    3. Click the "Archive" button on the top left of the manager and confirm that it should proceed
    4. Check that all pieces have been archived properly

    What kind of change does this PR introduce?

    (Check at least one)

    • [ ] Bug fix
    • [ ] New feature
    • [ ] Refactor
    • [ ] Documentation
    • [ ] Build-related changes
    • [ ] Other

    Make sure the PR fulfills these requirements:

    • [ ] It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
    • [ ] The changelog is updated
    • [ ] Related documentation has been updated
    • [ ] Related tests have been updated

    If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

    Other information:

    opened by ETLaurent 1
  • Apos2.222.0 - Invalid RegEx path.sep missing

    Apos2.222.0 - Invalid RegEx path.sep missing "\"

    Even though it isn't recommended to run ApostropheCMS on Windows, I've found a small bug which is worth opening a PR for.

    To Reproduce

    Step by step instructions to reproduce the behavior:

    1. Running ApostropheCMS 2 branch release-2.222.0 on Windows
    2. npm i installs packages fine on Node 14,16 and 18
    3. npm start is throwing an error with an invalid regular expression

    Expected behavior

    Apostrophe should start as expected

    Describe the bug

    Getting the following error when starting Apostrophe on a Windows environment:

    kiichiro-toyoda-cms\node_modules\apostrophe\index.js:58
    throw err;
    ^
    
    SyntaxError: Invalid regular expression: /\node_modules\mocha\/: \ at end of pattern
    at new RegExp (<anonymous>)
    at getRoot (C:\Users\user\WebStormProjects\kiichiro-toyoda-cms\node_modules\apostrophe\index.js:259:35)
    at module.exports (C:\Users\user\WebStormProjects\kiichiro-toyoda-cms\node_modules\apostrophe\index.js:35:33)
    at Object.<anonymous> (C:\Users\user\WebStormProjects\kiichiro-toyoda-cms\app.js:334:35)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
    at internal/main/run_main_module.js:17:47
    [nodemon] app crashed - waiting for file changes before starting...
    

    A small update is required to a specific line within the root index.js file of the apostrophe release-2.222.0 branch, the following on line 259 should be updated from:

    if (m.parent.filename.match(new RegExp(`${path.sep}node_modules${path.sep}mocha${path.sep}`))) {
    

    to:

    if (m.parent.filename.match(new RegExp(`${path.sep}node_modules${path.sep}mocha\${path.sep}`))) {
    

    Details

    Windows uses

    Version of Node.js: Tested on Node 14, 16 and 18

    Server Operating System: Microsoft Windows

    Additional context: This is specific to Microsoft Windows, after forking at testing the above fix Apostrophe starts as expected including on both Linux and Mac.

    bug 
    opened by nicholasbester 1
  • Make __t() globally available per request

    Make __t() globally available per request

    Summary

    This patch adds the __t() as global helper so that it is available for macros too. This is possible because getEnv() handler receives a real request object when called by our parsers on request. This should be 100% backward compatible change - the fragments are using custom context which takes precedence over the global context.

    What are the specific steps to test this change?

    Use {{ __t('Some phrase') }} inside a macro. It should render properly (with no parse errors).

    What kind of change does this PR introduce?

    (Check at least one)

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactor
    • [ ] Documentation
    • [ ] Build-related changes
    • [ ] Other

    Make sure the PR fulfills these requirements:

    • [x] It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
    • [x] The changelog is updated
    • [ ] Related documentation has been updated
    • [x] Related tests have been updated
    opened by myovchev 3
Owner
Apostrophe Technologies
Apostrophe Technologies
Tina is an open source editor that brings visual editing into React websites. Tina empowers developers to give their teams a contextual and intuitive editing experience without sacrificing code quality.

Tina is an open source editor that brings visual editing into React websites. Tina empowers developers to give their teams a contextual and intuitive editing experience without sacrificing code quality.

Tina 8.3k Jan 9, 2023
🚀 Open source Node.js Headless CMS to easily build customisable APIs

API creation made simple, secure and fast. The most advanced open-source headless CMS to build powerful APIs with no effort. Try live demo Strapi is a

strapi 50.8k Dec 27, 2022
The most powerful headless CMS for Node.js — built with GraphQL and React

A scalable platform and CMS to build Node.js applications. schema => ({ GraphQL, AdminUI }) Keystone Next is a preview of the next major release of Ke

KeystoneJS 7.3k Dec 31, 2022
👻 The #1 headless Node.js CMS for professional publishing

Ghost.org | Features | Showcase | Forum | Docs | Contributing | Twitter Love open source? We're hiring Node.js Engineers to work on Ghost full-time Th

Ghost 42.1k Jan 5, 2023
👻 The #1 headless Node.js CMS for professional publishing

Ghost.org | Features | Showcase | Forum | Docs | Contributing | Twitter Love open source? We're hiring Node.js Engineers to work on Ghost full-time Th

Ghost 37k Apr 5, 2021
🎉 Next Generation API-first CMS for developers. Generate an API-first CMS from a GraphQL schema with offline prototyping and an inline editor

Tipe Next Generation API-first CMS Design your content Shape and design content for any project you and your team are working on. Create your content

Tipe 2.2k Oct 22, 2021
Reaction is an API-first, headless commerce platform built using Node.js, React, GraphQL. Deployed via Docker and Kubernetes.

Reaction Commerce Reaction is a headless commerce platform built using Node.js, React, and GraphQL. It plays nicely with npm, Docker and Kubernetes. G

Reaction Commerce 11.9k Jan 3, 2023
A Node.js CMS written in CoffeeScript, with a user friendly backend

Nodizecms A Node.js CMS written in CoffeeScript, with a user friendly backend Status NodizeCMS is still under heavy development, there's a ton of unim

Nodize CMS 176 Sep 24, 2022
Minimalistic, lean & mean, node.js cms

enduro.js Enduro is minimalistic, lean & mean, node.js cms. See more at enduro.js website Other repositories: Enduro • samples • Enduro admin • enduro

Martin Gottweis 688 Dec 31, 2022
Drag and drop page builder and CMS for React, Vue, Angular, and more

Drag and drop page builder and CMS for React, Vue, Angular, and more Use your code components and the stack of your choice. No more being pestered for

Builder.io 4.3k Jan 9, 2023
The official CMS of OwnStore suite.

This project is part of OwnStore suite. Learn more here: https://ownstore.dev The suite contains the following projects: Website API CMS Doc Apps TWA

OwnStore 15 Nov 12, 2022
We.js, extensible Node.js MVC framework - CLI

We.js ;) We.js is a extensible node.js MVC framework For information and documentation see: http://wejs.org This repository (wejs/we) have the We.js C

We.js 208 Nov 10, 2022
Javascript Content Management System running on Node.js

Cody CMS A Javascript Content Management System running on Node.js We finally took upon the task, we are happy to announce the transition to Express 4

Johan Coppieters 669 Oct 31, 2022
Business class content management for Node.js (plugins, server cluster management, data-driven pages)

PencilBlue A full featured Node.js CMS and blogging platform (plugins, server cluster management, data-driven pages) First and foremost: If at any poi

PencilBlue, LLC. 1.6k Dec 30, 2022
Business class content management for Node.js (plugins, server cluster management, data-driven pages)

PencilBlue A full featured Node.js CMS and blogging platform (plugins, server cluster management, data-driven pages) First and foremost: If at any poi

PencilBlue, LLC. 1.6k Dec 30, 2022
We.js, extensible Node.js MVC framework - CLI

We.js ;) We.js is a extensible node.js MVC framework For information and documentation see: http://wejs.org This repository (wejs/we) have the We.js C

We.js 208 Nov 10, 2022
AdminBro is an admin panel for apps written in node.js

Admin Bro AdminBro is An automatic admin interface which can be plugged into your application. You, as a developer, provide database models (like post

Software Brothers 6.5k Jan 2, 2023
A Node.js Express backend for a Stackoverflow like answering forum, with RESTful endpoints

A Node.js Express backend for a Stackoverflow like answering forum, with RESTful endpoints, written in es6 style with linted and comprehensively unit-tested code. Utilizes a local json database using fs but has full separation of concern to implement anything else.

Dhiman Seal 3 Jan 9, 2022
FOSS app for the Diatum self-hosting network, supporting messaging and media sharing.

FOSS app for the Diatum self-hosting network, supporting messaging and media sharing.

Roland Osborne 23 Dec 23, 2022