• Content Count

  • Joined

  • Last visited

Community Reputation

36 Excellent

About schwarzdesign

  • Rank
    Jr. Member

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. schwarzdesign

    Thanks Yes we built multiple sites for them, all based on the same design with theme/color variations for individual sites. E.g. Osho UTA, UTA Akademie and Osho's Place. Those are built with Drupal though. Hi Bernhard, I agree that a module would be the more ProcessWire-friendly way, I mostly wrote it this way to save time. Since it is client work, we have to budget our development time; given that I haven't really started digging into module development yet, I didn't want the additional overhead. Since the site only has one template, I didn't need to duplicate any code, so I didn't think the benefits of building a module were worth the additional development time in this case. I thought about extending the Page class, but I went with composition over inheritance in this case to avoid unforseen side effects. Though it probably would've been cleaner to use inheritance. I'm not sure this class is general enough to be used in different sites at the moment, but if I have some free time at work or need it for another client site, I'll try to turn this into a configurable module!
  2. schwarzdesign

    We have relaunched the old website of design business expert Joachim Kobuss. The old site desperataly needed a facelift and a streamlined CMS that allowed our client to fill over 130 pages of content in a span of two weeks. We at schwarzdesign used ProcessWire + ProFields for the Backend and built a Bootstrap 4.1-based frontend. Features / Services provided Responsive design featuring a desktop layout with two fixed navigation areas and a fixed sidebar that form a frame around the content. Carefully set typography and spacing for a smooth reading experience for long texts on all devices. A hierarchical page tree with three levels of nested pages, all using the same simple set of fields. Password protection for individual pages and an optional password request form. Privacy by design: There's no tracking, no Google Map, no Facebook Like Button or anything like that on the page. Because we don't set any cookies or include third-party code, we don't need a cookie notice Modules used Profields (especially Repeater Matrix, Textareas and Functional Fields) Form Builder Tracy Debugger Wire Mail SMTP Duplicator Admin Links in Frontend Sitemap Optimizing for content The most distinguishing feature is the large amount of long texts our client has written. So it was important to provide a good editing experience in the frontend and have a content centered design that made long text sections easy to follow. We didn't want to give our client just a large CK Editor body text, since in our experience, those WYSIWYG editors aren't actually that easy to use for non-technical folk. It also reduces the amount of control we as designers and CSS-authors have over the presentation of the content. So instead, we built a flexible section system using the Repeater Matrix field of the ProFields module. There are three sections: text content, downloads and single image. Each section has a headline field and a radio selection field to switch between two levels of headline hierarchy (corresponding to h2 and h3). For the text section, there's a CK Editor textarea stripped to the bare minimum (strong, italic, lists, blockquotes and links), with no confusing styles or format dropdown. The image section features an additional option for full- or half-width images and an option to display the image description as a caption. And of course, the download section has a multivalue file field that is displayed as download buttons in the frontend. This simple sectioning system allowed us to build the entire site (except for the homepage) using just one template fieldset. There are three templates: Basic Page, Topic and Article. Basic pages are for generic pages like imprint and data policy. Topics may only be children of the homepage, while articles may be children of a topic or of another article, so the pages can be nested indefinitely. This provides the hierarchical structure used for the page layout. Topic and article inherit the fieldset of the basic page, so it's just one template under the hood. There's also smart Open Graph tag generation; title, description and preview image are automatically generated based on the first suitable sections on each page and can be manually overridden using the SEO tab available on all pages. Laying out the page Our design has three fixed areas: The top navigation, the left side navigation and the right sidebar, which shows some general contact info and a random image. The top navigation always shows the top-level topic pages, the sidebar navigation contains all articles in the current topic, displayed in a hierarchical structure. This was technically challenging, as the many fixed layout areas broke the normal pageflow and didn't work well for smaller devices. I solved this using four seperate Bootstrap containers, three of which are fixed. Only the container with the main page content flows normally. The left sidebar only fills the three leftmost columns of its container, the right sidebar the three rightmost columns, and the main content takes up the six center columns of it's container. All three have an offset to the top to make space for the fixed header navigation. This way, all the seperate layout areas can never overlap. The CSS that applies position: fixed to the containers is wrapped in a media query with a min-width, so on tablets and mobile devices, the areas simply stack. Check out the source code to see how those containers are laid out! The logo is text by the way, not an image. It's positioned using CSS3 transforms. Password protection Our client wanted to password protect some of his pages with varying passwords, so he can give different people access to different sets of pages. Also, he wanted to have a password request form, where people can submit their name and contact information to request access to a specific page. In the backend, we added a password field that activates the password protection for that page. There's also a checkbox to display the password request form. The form is built using the Form Builder module (and a simple hook to add the current page title to the outgoing mail). However, for the password protection, we built a custom solution. While there is a readymade module available, we didn't want something based on user accounts. A visitor should be able to simply input the password on a protected page and start reading, without bothering with registering an account. The authentification is stored in the session, so after closing the browser you have to input the password again, but that seemed to be a worthy tradeoff. Anyway, when visiting a password protected page, the template checks if the user is already authentificated to read this page; if so, it displays the page content, otherwise it displays the password input form and the password request form if it was also activated in the CMS. A nice-to-have feature that we built in addition is that if multiple pages use the same password and you input it on one of them, you are automatically authentificated to view all of them during the current session. A caveat of this method is that since the page content depends on the current session, we couldn't use the inbuilt page cache at all. So instead we used the cache API to cache rendered section markup and the navigation menues, which still results in a blazingly fast site. For the techies amongst you, here's the PasswordProtectedPage class we wrote for this functionality: <?php namespace schwarzdesign; use Processwire\Wire; use Processwire\Page; use Processwire\PageArray; use Processwire\User; class PasswordProtectedPage extends Wire { const PASSWORD_FIELD = 'page_password'; const PASSWORD_INPUT_NAME = 'page_password'; const STORAGE_KEY = 'authentificatedPages'; const STATUS_FREE_ACCESS = 'free_access'; const STATUS_AUTHENTIFICATED = 'authentificated'; const STATUS_NOT_AUTHENTIFICATED = 'not_authentificated_yet'; const STATUS_WRONG_PASSWORD = 'wrong_password'; /** * The status of the authentification. */ public $status = self::STATUS_NOT_AUTHENTIFICATED; private $page; private $authentificated_pages = []; public function __construct(Page $page) { $this->page = $page; $this->authentificated_pages = $this->getAuthentificatedPagesFromStorage(); } public function handleRequest() { // pages with no password set if (empty($this->page->get(self::PASSWORD_FIELD))) { return $this->status = self::STATUS_FREE_ACCESS; } // pages that the user is already authentificated to use if (in_array($this->page->id, $this->authentificated_pages)) { return $this->status = self::STATUS_AUTHENTIFICATED; } // if the user set the input, check the password if ($password = $this->getInputPassword()) { if ($this->passwordIsCorrect($password)) { $this->authentificatePagesByPassword($password); $this->storeAuthentificatedPages(); return $this->status = self::STATUS_AUTHENTIFICATED; } else { return $this->status = self::STATUS_WRONG_PASSWORD; } } else { return $this->status = self::STATUS_NOT_AUTHENTIFICATED; } } public function userCanAccess() { return in_array($this->status, [self::STATUS_AUTHENTIFICATED, self::STATUS_FREE_ACCESS]); } public function getInputPassword() { $password = $this->wire('input')->post(self::PASSWORD_INPUT_NAME); if (empty($password)) { return null; } return $password; } public function authentificatePagesByPassword(string $password) { $pages = $this->wire('pages')->find(self::PASSWORD_FIELD . '=' . $password); $ids = array_map(array($this, 'extractIdFromPage'), iterator_to_array($pages)); $this->addAuthentificatedPages($ids); } /** * Private helper Functions. */ private function passwordIsCorrect(string $password) { return (string) $this->page->get(self::PASSWORD_FIELD) === $password; } private function extractIdFromPage(Page $page) { return $page->id; } private function addAuthentificatedPages(array $page_ids) { $this->authentificated_pages = array_merge($this->authentificated_pages, $page_ids); } private function getAuthentificatedPagesFromStorage() { $ids = $this->wire('session')->get(self::STORAGE_KEY); return $ids ? $ids : []; } private function storeAuthentificatedPages() { $this->wire('session')->set(self::STORAGE_KEY, $this->authentificated_pages); } } This is used in the template like this: <?php namespace Processwire; use schwarzdesign\PasswordProtectedPage; $protectedPage = new PasswordProtectedPage($page); $protectedPage->handleRequest(); if (!$protectedPage->userCanAccess()) { if ($user->hasPermission('display-page-password')) { /* Display the password in plaintext for editors & admins */ } /* Display the password input form */ if ($page->show_password_request_form) { /* Display the password request form */ } } else { /* Display the page content */ } Screenshots
  3. schwarzdesign

    Thanks Yeah we are really happy with the speed, despite using using 'only' the normal page cache ... I was planning to utilize caching for some of the overview pages (those pull in data from several other pages), but it wasn't even necessary in the end, definitely a pro for ProcessWire.
  4. schwarzdesign

    We recently finished a relaunch of the website of the consulting firm Engfer Consulting: Built with ProcessWire 3 and Bootstrap 4. Features Bilingual website (UPDATE: The English page is now available! the English page is inactive at the moment, as the content is not ready yet) A strong, simple data structure for job offers and news, allowing for a custom search page and fulltext search Sectioned content-blocks built with a Repeater Matrix (ProFields) Reusable components available as page reference fields Automatic generation of open graph-tags, with manual overrides available on each page Contact forms with a custom recipient depending on the current page Modules used: ProFields ProForms Sitemap Cookie Management Banner Duplicator Tracy Debugger Wire Mail SMTP Admin Links in FrontEnd Technical insights Most of the content is built using a Repeater Matrix field. Each section has an optional heading and a select-field for background colors. Some of the sections available include: Text blocks (a repeater-field for multi-column text) Feeds / listing of the latest job offers or news (with a field to control the number of entries shown) Displaying a team member or quote (selected through a page reference field) Downloadable files (a multivalue field for file uploads) Those sections can be combined and stringed together arbitrarily, so any number of page templates showing different contents can be built. The job offers template, in contrast, features fixed fields for taxonomy assignment, description, contact person, preview image and so on, making the job offer pages uniform and easily accesible. The (technically) most interesting part of the site is the job overview page, which contains custom filters that are automatically generated from the available taxonomy terms. The stylesheets are written in SASS, built with Bootstrap 4. Only the base and grid SASS files of Bootstrap are included, along with the Bootstrap components that we ended up using. Using Bootstrap 4 as a framework in this way makes development & styling blazingly fast, once all the utility classes are commited to muscle memory ... > Our Agency. Screenshots
  5. schwarzdesign

    @jmartsch Thanks for the suggestion, unfortunately that didn't help ...
  6. schwarzdesign

    Quick question, the automatic backups through LazyCron aren't working for me. Manual backups are working fine (both locally and with remote FTP backup), but the automatic backups won't trigger at all. Here are my settings: Backup mode: LazyCron - Triggered by a pageview (installed) Event trigger: 1 hour Remove backups older than: 1 year I have waited for several hours, the site had traffic during that time, and I also visited some pages as a logged-in administrator and as an anonymous visitor. But no backups occured, there are also no log entries at all, so it doesn't look like it just bugged out. Do I need to configure anything else? LazyCron is installed, it doesn't have any module options though. Or do I need to manually call the LazyCron hooks in ready.php or something like that? Not sure what I am missing. Thanks!
  7. Ok, I couldn't figure out a way to do this with only selectors, so I built a custom solution. In case anyone has the same problem, here's my approach: I created a hidden text field "offer_taxonomy_cache" and hooked a function after the saveReady hook to automatically populate this field with a space-seperated list of the values of the selected options for the fields that I want to search, in both languages. This is somewhat crude, but has the advantage that it's even more performant than using the select option fields themselves, as this way only one table has to be searched for matches. It's basically the same idea as the FieldType Cache core module, but this one didn't work for me, since it only saved the ID's of the selected options, leaving me with the same problem. Here's my code: // /site/ready.php /** * Write taxonomy values to the cache field. */ $pages->addHookAfter('saveReady', function(HookEvent $event){ // page to be processed $page = $event->arguments(0); // only hook onto job offers if ($page->template->name !== 'job_offer') { return; } $page->of(false); $user = wire('user'); // all the fields to be included in the text cache $fields_to_cache = ['offer_area', 'offer_sector', 'offer_region', 'offer_rank', 'offer_type']; $taxonomy_cache_text = ''; // loop over all fields foreach ($fields_to_cache as $field) { $fields = $page->get($field); // save the current language for later $current_language = $user->language; foreach (wire('languages') as $language) { // set the user language and loop over all selected options, then // add the title in the current language to the taxonomy cache text $user->language = $language; foreach ($page->get($field) as $option) { $taxonomy_cache_text .= ' ' . $option->getTitle(); } } // reset the language $user->language = $current_language; } // trim the leading space $taxonomy_cache_text = trim($taxonomy_cache_text); // write the value to the cache field of the page $page->offer_taxonomy_cache = $taxonomy_cache_text; // write a log message wire('log')->message("Wrote taxonomy cache for page {$page->title} ({$page->id}): \"{$taxonomy_cache_text}\""); }); One problem with this is the triple nested loop, so this might become quite slow for sites with many languages, select option fields and options per field. But in this case using a real page for the select options would be preferable anyway, as suggested. Thanks @dragan for your help!
  8. @dragan Thanks for the reply! I do have some select-options on the filter page, but I need those fields to be searchable through fulltext search unfortunately. I have used the value|title approach, but since I need to support fulltext search, searching the values instead of the titles is not an option. I'll probably use pages from the start the next time, but for now I have to work with what I got. Regarding @heldercervantes's comment, I actually found a solution to that. Turns out that while the magic getter for $field->title and $field->value always return the default values, the proper methods $field->getTitle() and $field->getValue() return the correct values in the current user language. I did post a reply, it never got accepted though .. Still, I'm not sure how to get this fulltext search to work with the current setup.
  9. I'm looking for a way to search multilanguage titles in a couple of select option fields. I have looked through some threads here but couldn't find a solution that works for my specific problem. Any help is very much appreciated! My setup: I have a multilanguage site with two languages, English and German. The default language is German. I'm building a search form for the template "job_offer" that has a couple of category fields. Those fields are built with the Select Options FieldType Module. They are categories for the region, industry, department et c. for the job offer. They have titles defined in German and English. For example, for the field "offer_region" there are the options "Austria", "Switzerland" and "Germany" that have the German country names as their German option titles. The search field is supposed to search for text matches in the job description and category field titles. This is the selector is have so far: $pages->find("template=job_offer, title|offer_body|offer_region|offer_rank%={$input->whitelist('search')}"); While the selector works for the title and offer_body fields, it only matches the German Select Option titles. For example, there's a job offer with the area set to "Austria" ("Österreich" in German). When I search for "Österreich", it shows up as expected. However, when I search for "Austria", I get know results, regardless of whether the current user language is English or German (note that the default language is German on this site). Any ideas how to get it to work? Ideally, I would like to be able to filter through the Select Option titles in every language, i.e. I get the same results when I search for "Austria" or "Österreich" regardless of the current user language. However, for the moment I would be happy enough with a solution that will yield a result both when I search for "Austria" (while the user language is English) and when I search for "Österreich" (while the user language is German). Thanks in advance!
  10. schwarzdesign

    @Soma It's a multilanguage textarea field; the field is one field in a Repeater Matrix type. There are multiple Repeater Matrix items of that type on the page, so there are multiple instances of the field on the same page (this is why it's running in inline mode).
  11. schwarzdesign

    @dragan I'm not sure it's related, as the Github issue mentions the allowedExtraContent settings string to be missing from the source code. In my case it appears there correctly, only the id-attributes are still being filtered. @Robin S Is the id still there after you have saved the page? For me, it only disappears after I've saved the page. Come to think of it, doesn't CK Editor apply filtering after you close the source editor? In that case, maybe it's not an issue with CK Editor at all, but some other filtering applied by Processwire .. the id only disappears after I save the page.
  12. schwarzdesign

    I need to allow the id-attribute for the <a>-Element, but I can't seem to get it to work. I have tried setting the extraAllowedContent in the backend: a[!href,id]; I have also tried using the line without the semicolon, no effect either. I have tried setting the extraAllowedContent in the /site/modules/InputfieldCKEditor/config.js file instead as well: CKEDITOR.editorConfig = function( config ) { // Define changes to default configuration here. For example: // config.uiColor = '#AADC6E'; config.format_tags = 'p;h3;h4'; config.extraAllowedContent = 'a[!href,id];'; }; But that didn't work either. I have verified that the javascript-settings on the edit page include that line: "InputfieldCKEditor_body_block_text": { "baseHref": "/", "contentsCss": "/wire/modules/Inputfield/InputfieldCKEditor/contents.css", "extraAllowedContent": "a[!href,id];", "extraPlugins": "pwlink,sourcedialog", "removePlugins": "image,magicline", "toolbar": [ [ "Format", "Styles", "-", "Bold", "Italic" ], [ "NumberedList", "BulletedList", "Blockquote" ], [ "JustifyLeft", "JustifyCenter", "JustifyRight" ], [ "PWLink", "Unlink", "-", "HorizontalRule" ], [ "Sourcedialog" ] ], "format_tags": "p;h3;h4", "language": "en", "entities": false, "uploadUrl": "", "pwUploadField": "", "pwAssetPageID": 1167, "height": "20em", "stylesSet": "engfer-styles:/site/modules/InputfieldCKEditor/mystyles.js?nc=1529934076", "customConfig": "/site/modules/InputfieldCKEditor/config.js?nc=1530805498" } But every time I save, the id i add to a link in the source panel disappears. No idea how to debug this or what else I can try. Here are the rest of my settings for that field: Editor Mode is Inline (I tried switching to regular mode, doesn't work either). Use ACF is On Use HTML Purifier is On Any help is very much appreciated.